// script.aculo.us effects.js v1.8.0_pre1, Fri Oct 12 21:34:51 +0200 2007

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
  var color = '#';
  if (this.slice(0,4) == 'rgb(') {
    var cols = this.slice(4,this.length-1).split(',');
    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
  } else {
    if (this.slice(0,1) == '#') {
      if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
      if (this.length==7) color = this.toLowerCase();
    }
  }
  return (color.length==7 ? color : (arguments[0] || this));
};

/*--------------------------------------------------------------------------*/

Element.collectTextNodes = function(element) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
  }).flatten().join('');
};

Element.collectTextNodesIgnoreClass = function(element, className) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
        Element.collectTextNodesIgnoreClass(node, className) : ''));
  }).flatten().join('');
};

Element.setContentZoom = function(element, percent) {
  element = $(element);
  element.setStyle({fontSize: (percent/100) + 'em'});
  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
  return element;
};

Element.getInlineOpacity = function(element){
  return $(element).style.opacity || '';
};

Element.forceRerendering = function(element) {
  try {
    element = $(element);
    var n = document.createTextNode(' ');
    element.appendChild(n);
    element.removeChild(n);
  } catch(e) { }
};

/*--------------------------------------------------------------------------*/

var Effect = {
  _elementDoesNotExistError: {
    name: 'ElementDoesNotExistError',
    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  },
  Transitions: {
    linear: Prototype.K,
    sinoidal: function(pos) {
      return (-Math.cos(pos*Math.PI)/2) + 0.5;
    },
    reverse: function(pos) {
      return 1-pos;
    },
    flicker: function(pos) {
      var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
      return pos > 1 ? 1 : pos;
    },
    wobble: function(pos) {
      return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
    },
    pulse: function(pos, pulses) {
      pulses = pulses || 5;
      return (
        ((pos % (1/pulses)) * pulses).round() == 0 ?
              ((pos * pulses * 2) - (pos * pulses * 2).floor()) :
          1 - ((pos * pulses * 2) - (pos * pulses * 2).floor())
        );
    },
    spring: function(pos) {
      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
    },
    none: function(pos) {
      return 0;
    },
    full: function(pos) {
      return 1;
    }
  },
  DefaultOptions: {
    duration:   1.0,   // seconds
    fps:        100,   // 100= assume 66fps max.
    sync:       false, // true for combining
    from:       0.0,
    to:         1.0,
    delay:      0.0,
    queue:      'parallel'
  },
  tagifyText: function(element) {
    var tagifyStyle = 'position:relative';
    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';

    element = $(element);
    $A(element.childNodes).each( function(child) {
      if (child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            new Element('span', {style: tagifyStyle}).update(
              character == ' ' ? String.fromCharCode(160) : character),
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if (((typeof element == 'object') ||
        Object.isFunction(element)) &&
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;

    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || { });
    var masterDelay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
    });
  },
  PAIRS: {
    'slide':  ['SlideDown','SlideUp'],
    'blind':  ['BlindDown','BlindUp'],
    'appear': ['Appear','Fade']
  },
  toggle: function(element, effect) {
    element = $(element);
    effect = (effect || 'appear').toLowerCase();
    var options = Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, arguments[2] || { });
    Effect[element.visible() ?
      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
  }
};

Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;

/* ------------- core effects ------------- */

Effect.ScopedQueue = Class.create(Enumerable, {
  initialize: function() {
    this.effects  = [];
    this.interval = null;
  },
  _each: function(iterator) {
    this.effects._each(iterator);
  },
  add: function(effect) {
    var timestamp = new Date().getTime();

    var position = Object.isString(effect.options.queue) ?
      effect.options.queue : effect.options.queue.position;

    switch(position) {
      case 'front':
        // move unstarted effects after this effect
        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
            e.startOn  += effect.finishOn;
            e.finishOn += effect.finishOn;
          });
        break;
      case 'with-last':
        timestamp = this.effects.pluck('startOn').max() || timestamp;
        break;
      case 'end':
        // start effect after last queued effect has finished
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }

    effect.startOn  += timestamp;
    effect.finishOn += timestamp;

    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
      this.effects.push(effect);

    if (!this.interval)
      this.interval = setInterval(this.loop.bind(this), 15);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if (this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    for(var i=0, len=this.effects.length;i<len;i++)
      this.effects[i] && this.effects[i].loop(timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if (!Object.isString(queueName)) return queueName;

    if (!this.instances[queueName])
      this.instances[queueName] = new Effect.ScopedQueue();

    return this.instances[queueName];
  }
};
Effect.Queue = Effect.Queues.get('global');

Effect.Base = Class.create();
Effect.Base.prototype = {
  position: null,
  start: function(options) {
    function codeForEvent(options,eventName){
      return (
        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
      );
    }
    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn+(this.options.duration*1000);
    this.fromToDelta  = this.options.to-this.options.from;
    this.totalTime    = this.finishOn-this.startOn;
    this.totalFrames  = this.options.fps*this.options.duration;

    eval('this.render = function(pos){ '+
      'if (this.state=="idle"){this.state="running";'+
      codeForEvent(this.options,'beforeSetup')+
      (this.setup ? 'this.setup();':'')+
      codeForEvent(this.options,'afterSetup')+
      '};if (this.state=="running"){'+
      'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
      'this.position=pos;'+
      codeForEvent(this.options,'beforeUpdate')+
      (this.update ? 'this.update(pos);':'')+
      codeForEvent(this.options,'afterUpdate')+
      '}}');

    this.event('beforeStart');
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).add(this);
  },
  loop: function(timePos) {
    if (timePos >= this.startOn) {
      if (timePos >= this.finishOn) {
        this.render(1.0);
        this.cancel();
        this.event('beforeFinish');
        if (this.finish) this.finish();
        this.event('afterFinish');
        return;
      }
      var pos   = (timePos - this.startOn) / this.totalTime,
          frame = (pos * this.totalFrames).round();
      if (frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  cancel: function() {
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).remove(this);
    this.state = 'finished';
  },
  event: function(eventName) {
    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
    if (this.options[eventName]) this.options[eventName](this);
  },
  inspect: function() {
    var data = $H();
    for(property in this)
      if (!Object.isFunction(this[property])) data[property] = this[property];
    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
  }
};

Effect.Parallel = Class.create(Effect.Base, {
  initialize: function(effects) {
    this.effects = effects || [];
    this.start(arguments[1]);
  },
  update: function(position) {
    this.effects.invoke('render', position);
  },
  finish: function(position) {
    this.effects.each( function(effect) {
      effect.render(1.0);
      effect.cancel();
      effect.event('beforeFinish');
      if (effect.finish) effect.finish(position);
      effect.event('afterFinish');
    });
  }
});

Effect.Tween = Class.create(Effect.Base, {
  initialize: function(object, from, to) {
    object = Object.isString(object) ? $(object) : object;
    var args = $A(arguments), method = args.last(),
      options = args.length == 5 ? args[3] : null;
    this.method = Object.isFunction(method) ? method.bind(object) :
      Object.isFunction(object[method]) ? object[method].bind(object) :
      function(value) { object[method] = value };
    this.start(Object.extend({ from: from, to: to }, options || { }));
  },
  update: function(position) {
    this.method(position);
  }
});

Effect.Event = Class.create(Effect.Base, {
  initialize: function() {
    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
  },
  update: Prototype.emptyFunction
});

Effect.Opacity = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    // make this work on IE on elements without 'layout'
    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
      this.element.setStyle({zoom: 1});
    var options = Object.extend({
      from: this.element.getOpacity() || 0.0,
      to:   1.0
    }, arguments[1] || { });
    this.start(options);
  },
  update: function(position) {
    this.element.setOpacity(position);
  }
});

Effect.Move = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      x:    0,
      y:    0,
      mode: 'relative'
    }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    this.element.makePositioned();
    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
    if (this.options.mode == 'absolute') {
      this.options.x = this.options.x - this.originalLeft;
      this.options.y = this.options.y - this.originalTop;
    }
  },
  update: function(position) {
    this.element.setStyle({
      left: (this.options.x  * position + this.originalLeft).round() + 'px',
      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
    });
  }
});

// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
  return new Effect.Move(element,
    Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
};

Effect.Scale = Class.create(Effect.Base, {
  initialize: function(element, percent) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      scaleX: true,
      scaleY: true,
      scaleContent: true,
      scaleFromCenter: false,
      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
      scaleFrom: 100.0,
      scaleTo:   percent
    }, arguments[2] || { });
    this.start(options);
  },
  setup: function() {
    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
    this.elementPositioning = this.element.getStyle('position');

    this.originalStyle = { };
    ['top','left','width','height','fontSize'].each( function(k) {
      this.originalStyle[k] = this.element.style[k];
    }.bind(this));

    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;

    var fontSize = this.element.getStyle('font-size') || '100%';
    ['em','px','%','pt'].each( function(fontSizeType) {
      if (fontSize.indexOf(fontSizeType)>0) {
        this.fontSize     = parseFloat(fontSize);
        this.fontSizeType = fontSizeType;
      }
    }.bind(this));

    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;

    this.dims = null;
    if (this.options.scaleMode=='box')
      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
    if (/^content/.test(this.options.scaleMode))
      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
    if (!this.dims)
      this.dims = [this.options.scaleMode.originalHeight,
                   this.options.scaleMode.originalWidth];
  },
  update: function(position) {
    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
    if (this.options.scaleContent && this.fontSize)
      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
  },
  finish: function(position) {
    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
  },
  setDimensions: function(height, width) {
    var d = { };
    if (this.options.scaleX) d.width = width.round() + 'px';
    if (this.options.scaleY) d.height = height.round() + 'px';
    if (this.options.scaleFromCenter) {
      var topd  = (height - this.dims[0])/2;
      var leftd = (width  - this.dims[1])/2;
      if (this.elementPositioning == 'absolute') {
        if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
        if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
      } else {
        if (this.options.scaleY) d.top = -topd + 'px';
        if (this.options.scaleX) d.left = -leftd + 'px';
      }
    }
    this.element.setStyle(d);
  }
});

Effect.Highlight = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    // Prevent executing on elements not in the layout flow
    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
    // Disable background image during the effect
    this.oldStyle = { };
    if (!this.options.keepBackgroundImage) {
      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
      this.element.setStyle({backgroundImage: 'none'});
    }
    if (!this.options.endcolor)
      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
    if (!this.options.restorecolor)
      this.options.restorecolor = this.element.getStyle('background-color');
    // init color calculations
    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
  },
  update: function(position) {
    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
  },
  finish: function() {
    this.element.setStyle(Object.extend(this.oldStyle, {
      backgroundColor: this.options.restorecolor
    }));
  }
});

Effect.ScrollTo = function(element) {
  var options = arguments[1] || { },
    scrollOffsets = document.viewport.getScrollOffsets(),
    elementOffsets = $(element).cumulativeOffset(),
    max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();

  if (options.offset) elementOffsets[1] += options.offset;

  return new Effect.Tween(null,
    scrollOffsets.top,
    elementOffsets[1] > max ? max : elementOffsets[1],
    options,
    function(p){ scrollTo(scrollOffsets.left, p.round()) }
  );
};

/* ------------- combination effects ------------- */

Effect.Fade = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  var options = Object.extend({
    from: element.getOpacity() || 1.0,
    to:   0.0,
    afterFinishInternal: function(effect) {
      if (effect.options.to!=0) return;
      effect.element.hide().setStyle({opacity: oldOpacity});
    }
  }, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Appear = function(element) {
  element = $(element);
  var options = Object.extend({
  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
  to:   1.0,
  // force Safari to render floated elements properly
  afterFinishInternal: function(effect) {
    effect.element.forceRerendering();
  },
  beforeSetup: function(effect) {
    effect.element.setOpacity(effect.options.from).show();
  }}, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Puff = function(element) {
  element = $(element);
  var oldStyle = {
    opacity: element.getInlineOpacity(),
    position: element.getStyle('position'),
    top:  element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height
  };
  return new Effect.Parallel(
   [ new Effect.Scale(element, 200,
      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
     Object.extend({ duration: 1.0,
      beforeSetupInternal: function(effect) {
        Position.absolutize(effect.effects[0].element)
      },
      afterFinishInternal: function(effect) {
         effect.effects[0].element.hide().setStyle(oldStyle); }
     }, arguments[1] || { })
   );
};

Effect.BlindUp = function(element) {
  element = $(element);
  element.makeClipping();
  return new Effect.Scale(element, 0,
    Object.extend({ scaleContent: false,
      scaleX: false,
      restoreAfterFinish: true,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping();
      }
    }, arguments[1] || { })
  );
};

Effect.BlindDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping();
    }
  }, arguments[1] || { }));
};

Effect.SwitchOff = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  return new Effect.Appear(element, Object.extend({
    duration: 0.4,
    from: 0,
    transition: Effect.Transitions.flicker,
    afterFinishInternal: function(effect) {
      new Effect.Scale(effect.element, 1, {
        duration: 0.3, scaleFromCenter: true,
        scaleX: false, scaleContent: false, restoreAfterFinish: true,
        beforeSetup: function(effect) {
          effect.element.makePositioned().makeClipping();
        },
        afterFinishInternal: function(effect) {
          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
        }
      })
    }
  }, arguments[1] || { }));
};

Effect.DropOut = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left'),
    opacity: element.getInlineOpacity() };
  return new Effect.Parallel(
    [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
    Object.extend(
      { duration: 0.5,
        beforeSetup: function(effect) {
          effect.effects[0].element.makePositioned();
        },
        afterFinishInternal: function(effect) {
          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
        }
      }, arguments[1] || { }));
};

Effect.Shake = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left') };
    return new Effect.Move(element,
      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
        effect.element.undoPositioned().setStyle(oldStyle);
  }}) }}) }}) }}) }}) }});
};

Effect.SlideDown = function(element) {
  element = $(element).cleanWhitespace();
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: window.opera ? 0 : 1,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || { })
  );
};

Effect.SlideUp = function(element) {
  element = $(element).cleanWhitespace();
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, window.opera ? 0 : 1,
   Object.extend({ scaleContent: false,
    scaleX: false,
    scaleMode: 'box',
    scaleFrom: 100,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
    }
   }, arguments[1] || { })
  );
};

// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
  return new Effect.Scale(element, window.opera ? 1 : 0, {
    restoreAfterFinish: true,
    beforeSetup: function(effect) {
      effect.element.makeClipping();
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping();
    }
  });
};

Effect.Grow = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.full
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var initialMoveX, initialMoveY;
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      initialMoveX = initialMoveY = moveX = moveY = 0;
      break;
    case 'top-right':
      initialMoveX = dims.width;
      initialMoveY = moveY = 0;
      moveX = -dims.width;
      break;
    case 'bottom-left':
      initialMoveX = moveX = 0;
      initialMoveY = dims.height;
      moveY = -dims.height;
      break;
    case 'bottom-right':
      initialMoveX = dims.width;
      initialMoveY = dims.height;
      moveX = -dims.width;
      moveY = -dims.height;
      break;
    case 'center':
      initialMoveX = dims.width / 2;
      initialMoveY = dims.height / 2;
      moveX = -dims.width / 2;
      moveY = -dims.height / 2;
      break;
  }

  return new Effect.Move(element, {
    x: initialMoveX,
    y: initialMoveY,
    duration: 0.01,
    beforeSetup: function(effect) {
      effect.element.hide().makeClipping().makePositioned();
    },
    afterFinishInternal: function(effect) {
      new Effect.Parallel(
        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
          new Effect.Scale(effect.element, 100, {
            scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
        ], Object.extend({
             beforeSetup: function(effect) {
               effect.effects[0].element.setStyle({height: '0px'}).show();
             },
             afterFinishInternal: function(effect) {
               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
             }
           }, options)
      )
    }
  });
};

Effect.Shrink = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.none
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      moveX = moveY = 0;
      break;
    case 'top-right':
      moveX = dims.width;
      moveY = 0;
      break;
    case 'bottom-left':
      moveX = 0;
      moveY = dims.height;
      break;
    case 'bottom-right':
      moveX = dims.width;
      moveY = dims.height;
      break;
    case 'center':
      moveX = dims.width / 2;
      moveY = dims.height / 2;
      break;
  }

  return new Effect.Parallel(
    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
    ], Object.extend({
         beforeStartInternal: function(effect) {
           effect.effects[0].element.makePositioned().makeClipping();
         },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
       }, options)
  );
};

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || { };
  var oldOpacity = element.getInlineOpacity();
  var transition = options.transition || Effect.Transitions.sinoidal;
  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
  reverser.bind(transition);
  return new Effect.Opacity(element,
    Object.extend(Object.extend({  duration: 2.0, from: 0,
      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
    }, options), {transition: reverser}));
};

Effect.Fold = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height };
  element.makeClipping();
  return new Effect.Scale(element, 5, Object.extend({
    scaleContent: false,
    scaleX: false,
    afterFinishInternal: function(effect) {
    new Effect.Scale(element, 1, {
      scaleContent: false,
      scaleY: false,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping().setStyle(oldStyle);
      } });
  }}, arguments[1] || { }));
};

Effect.Morph = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      style: { }
    }, arguments[1] || { });

    if (!Object.isString(options.style)) this.style = $H(options.style);
    else {
      if (options.style.include(':'))
        this.style = options.style.parseStyle();
      else {
        this.element.addClassName(options.style);
        this.style = $H(this.element.getStyles());
        this.element.removeClassName(options.style);
        var css = this.element.getStyles();
        this.style = this.style.reject(function(style) {
          return style.value == css[style.key];
        });
        options.afterFinishInternal = function(effect) {
          effect.element.addClassName(effect.options.style);
          effect.transforms.each(function(transform) {
            effect.element.style[transform.style] = '';
          });
        }
      }
    }
    this.start(options);
  },

  setup: function(){
    function parseColor(color){
      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
      color = color.parseColor();
      return $R(0,2).map(function(i){
        return parseInt( color.slice(i*2+1,i*2+3), 16 )
      });
    }
    this.transforms = this.style.map(function(pair){
      var property = pair[0], value = pair[1], unit = null;

      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
        value = value.parseColor();
        unit  = 'color';
      } else if (property == 'opacity') {
        value = parseFloat(value);
        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
          this.element.setStyle({zoom: 1});
      } else if (Element.CSS_LENGTH.test(value)) {
          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
          value = parseFloat(components[1]);
          unit = (components.length == 3) ? components[2] : null;
      }

      var originalValue = this.element.getStyle(property);
      return {
        style: property.camelize(),
        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
        targetValue: unit=='color' ? parseColor(value) : value,
        unit: unit
      };
    }.bind(this)).reject(function(transform){
      return (
        (transform.originalValue == transform.targetValue) ||
        (
          transform.unit != 'color' &&
          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
        )
      )
    });
  },
  update: function(position) {
    var style = { }, transform, i = this.transforms.length;
    while(i--)
      style[(transform = this.transforms[i]).style] =
        transform.unit=='color' ? '#'+
          (Math.round(transform.originalValue[0]+
            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
          (Math.round(transform.originalValue[1]+
            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
          (Math.round(transform.originalValue[2]+
            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
        (transform.originalValue +
          (transform.targetValue - transform.originalValue) * position).toFixed(3) +
            (transform.unit === null ? '' : transform.unit);
    this.element.setStyle(style, true);
  }
});

Effect.Transform = Class.create({
  initialize: function(tracks){
    this.tracks  = [];
    this.options = arguments[1] || { };
    this.addTracks(tracks);
  },
  addTracks: function(tracks){
    tracks.each(function(track){
      var data = $H(track).values().first();
      this.tracks.push($H({
        ids:     $H(track).keys().first(),
        effect:  Effect.Morph,
        options: { style: data }
      }));
    }.bind(this));
    return this;
  },
  play: function(){
    return new Effect.Parallel(
      this.tracks.map(function(track){
        var elements = [$(track.ids) || $$(track.ids)].flatten();
        return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
      }).flatten(),
      this.options
    );
  }
});

Element.CSS_PROPERTIES = $w(
  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
  'fontSize fontWeight height left letterSpacing lineHeight ' +
  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
  'right textIndent top width wordSpacing zIndex');

Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;

String.__parseStyleElement = document.createElement('div');
String.prototype.parseStyle = function(){
  var style, styleRules = $H();
  if (Prototype.Browser.WebKit)
    style = new Element('div',{style:this}).style;
  else {
    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
    style = String.__parseStyleElement.childNodes[0].style;
  }

  Element.CSS_PROPERTIES.each(function(property){
    if (style[property]) styleRules[property] = style[property];
  });

  if (Prototype.Browser.IE && this.include('opacity'))
    styleRules.opacity = this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1];

  return styleRules;
};

if (document.defaultView && document.defaultView.getComputedStyle) {
  Element.getStyles = function(element) {
    var css = document.defaultView.getComputedStyle($(element), null);
    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
      styles[property] = css[property];
      return styles;
    });
  };
} else {
  Element.getStyles = function(element) {
    element = $(element);
    var css = element.currentStyle, styles;
    styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) {
      hash[property] = css[property];
      return hash;
    });
    if (!styles.opacity) styles.opacity = element.getOpacity();
    return styles;
  };
};

Effect.Methods = {
  morph: function(element, style) {
    element = $(element);
    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
    return element;
  },
  visualEffect: function(element, effect, options) {
    element = $(element)
    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
    new Effect[klass](element, options);
    return element;
  },
  highlight: function(element, options) {
    element = $(element);
    new Effect.Highlight(element, options);
    return element;
  }
};

$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
  'pulsate shake puff squish switchOff dropOut').each(
  function(effect) {
    Effect.Methods[effect] = function(element, options){
      element = $(element);
      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
      return element;
    }
  }
);

$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
  function(f) { Effect.Methods[f] = Element[f]; }
);

Element.addMethods(Effect.Methods);// script.aculo.us dragdrop.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if(Object.isUndefined(Effect))
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],

  remove: function(element) {
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  },

  add: function(element) {
    element = $(element);
    var options = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false
    }, arguments[1] || { });

    // cache containers
    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if(Object.isArray(containment)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }

    if(options.accept) options.accept = [options.accept].flatten();

    Element.makePositioned(element); // fix IE
    options.element = element;

    this.drops.push(options);
  },

  findDeepestChild: function(drops) {
    deepest = drops[0];

    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];

    return deepest;
  },

  isContained: function(element, drop) {
    var containmentNode;
    if(drop.tree) {
      containmentNode = element.treeNode;
    } else {
      containmentNode = element.parentNode;
    }
    return drop._containers.detect(function(c) { return containmentNode == c });
  },

  isAffected: function(point, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.classNames(element).detect(
          function(v) { return drop.accept.include(v) } ) )) &&
      Position.within(drop.element, point[0], point[1]) );
  },

  deactivate: function(drop) {
    if(drop.hoverclass)
      Element.removeClassName(drop.element, drop.hoverclass);
    this.last_active = null;
  },

  activate: function(drop) {
    if(drop.hoverclass)
      Element.addClassName(drop.element, drop.hoverclass);
    this.last_active = drop;
  },

  show: function(point, element) {
    if(!this.drops.length) return;
    var drop, affected = [];

    this.drops.each( function(drop) {
      if(Droppables.isAffected(point, element, drop))
        affected.push(drop);
    });

    if(affected.length>0)
      drop = Droppables.findDeepestChild(affected);

    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
    if (drop) {
      Position.within(drop.element, point[0], point[1]);
      if(drop.onHover)
        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));

      if (drop != this.last_active) Droppables.activate(drop);
    }
  },

  fire: function(event, element) {
    if(!this.last_active) return;
    Position.prepare();

    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
      if (this.last_active.onDrop) {
        this.last_active.onDrop(element, this.last_active.element, event);
        return true;
      }
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  }
}

var Draggables = {
  drags: [],
  observers: [],

  register: function(draggable) {
    if(this.drags.length == 0) {
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);

      Event.observe(document, "mouseup", this.eventMouseUp);
      Event.observe(document, "mousemove", this.eventMouseMove);
      Event.observe(document, "keypress", this.eventKeypress);
    }
    this.drags.push(draggable);
  },

  unregister: function(draggable) {
    this.drags = this.drags.reject(function(d) { return d==draggable });
    if(this.drags.length == 0) {
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
      Event.stopObserving(document, "keypress", this.eventKeypress);
    }
  },

  activate: function(draggable) {
    if(draggable.options.delay) {
      this._timeout = setTimeout(function() {
        Draggables._timeout = null;
        window.focus();
        Draggables.activeDraggable = draggable;
      }.bind(this), draggable.options.delay);
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },

  deactivate: function() {
    this.activeDraggable = null;
  },

  updateDrag: function(event) {
    if(!this.activeDraggable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    // Mozilla-based browsers fire successive mousemove events with
    // the same coordinates, prevent needless redrawing (moz bug?)
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;

    this.activeDraggable.updateDrag(event, pointer);
  },

  endDrag: function(event) {
    if(this._timeout) {
      clearTimeout(this._timeout);
      this._timeout = null;
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },

  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },

  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },

  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },

  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },

  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onDrag'].each( function(eventName) {
      Draggables[eventName+'Count'] = Draggables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
}

/*--------------------------------------------------------------------------*/

var Draggable = Class.create({
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){
            Draggable._dragging[element] = false
          }
        });
      },
      zindex: 1000,
      revert: false,
      quiet: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };

    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
        }
      });

    var options = Object.extend(defaults, arguments[1] || { });

    this.element = $(element);

    if(options.handle && Object.isString(options.handle))
      this.handle = this.element.down('.'+options.handle, 0);

    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;

    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
      options.scroll = $(options.scroll);
      this._isScrollChild = Element.childOf(this.element, options.scroll);
    }

    Element.makePositioned(this.element); // fix IE

    this.options  = options;
    this.dragging = false;

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);

    Draggables.register(this);
  },

  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Draggables.unregister(this);
  },

  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },

  initDrag: function(event) {
    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if((tag_name = src.tagName.toUpperCase()) && (
        tag_name=='INPUT' ||
        tag_name=='SELECT' ||
        tag_name=='OPTION' ||
        tag_name=='BUTTON' ||
        tag_name=='TEXTAREA')) return;

      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var pos     = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });

      Draggables.activate(this);
      Event.stop(event);
    }
  },

  startDrag: function(event) {
    this.dragging = true;
    if(!this.delta)
      this.delta = this.currentDelta();

    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }

    if(this.options.ghosting) {
      this._clone = this.element.cloneNode(true);
      this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
      if (!this.element._originallyAbsolute)
        Position.absolutize(this.element);
      this.element.parentNode.insertBefore(this._clone, this.element);
    }

    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }

    Draggables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);

    if(!this.options.quiet){
      Position.prepare();
      Droppables.show(pointer, this.element);
    }

    Draggables.notify('onDrag', this, event);

    this.draw(pointer);
    if(this.options.change) this.options.change(this);

    if(this.options.scroll) {
      this.stopScrolling();

      var p;
      if (this.options.scroll == window) {
        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
      } else {
        p = Position.page(this.options.scroll);
        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
        p[1] += this.options.scroll.scrollTop + Position.deltaY;
        p.push(p[0]+this.options.scroll.offsetWidth);
        p.push(p[1]+this.options.scroll.offsetHeight);
      }
      var speed = [0,0];
      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
      this.startScrolling(speed);
    }

    // fix AppleWebKit rendering
    if(Prototype.Browser.WebKit) window.scrollBy(0,0);

    Event.stop(event);
  },

  finishDrag: function(event, success) {
    this.dragging = false;

    if(this.options.quiet){
      Position.prepare();
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      Droppables.show(pointer, this.element);
    }

    if(this.options.ghosting) {
      if (!this.element._originallyAbsolute)
        Position.relativize(this.element);
      delete this.element._originallyAbsolute;
      Element.remove(this._clone);
      this._clone = null;
    }

    var dropped = false;
    if(success) {
      dropped = Droppables.fire(event, this.element);
      if (!dropped) dropped = false;
    }
    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && Object.isFunction(revert)) revert = revert(this.element);

    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      if (dropped == 0 || revert != 'failure')
        this.options.reverteffect(this.element,
          d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect)
      this.options.endeffect(this.element);

    Draggables.deactivate(this);
    Droppables.reset();
  },

  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },

  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },

  draw: function(point) {
    var pos = Position.cumulativeOffset(this.element);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element);
      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
    }

    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];

    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    }

    var p = [0,1].map(function(i){
      return (point[i]-pos[i]-this.offset[i])
    }.bind(this));

    if(this.options.snap) {
      if(Object.isFunction(this.options.snap)) {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(Object.isArray(this.options.snap)) {
        p = p.map( function(v, i) {
          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
      } else {
        p = p.map( function(v) {
          return (v/this.options.snap).round()*this.options.snap }.bind(this))
      }
    }}

    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";

    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },

  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },

  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },

  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    if(this.options.scroll == window) {
      with (this._getWindowScroll(this.options.scroll)) {
        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
          var d = delta / 1000;
          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
        }
      }
    } else {
      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    }

    Position.prepare();
    Droppables.show(Draggables._lastPointer, this.element);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }

    if(this.options.change) this.options.change(this);
  },

  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
});

Draggable._dragging = { };

/*--------------------------------------------------------------------------*/

var SortableObserver = Class.create({
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },

  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },

  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
});

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,

  sortables: { },

  _findRootElement: function(element) {
    while (element.tagName.toUpperCase() != "BODY") {
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },

  destroy: function(element){
    var s = Sortable.options(element);

    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');

      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      quiet:       false,
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,

      // these take arrays of elements or ids and can be
      // used for better initialization performance
      elements:    false,
      handles:     false,

      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || { });

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      quiet:       options.quiet,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    // build options for the droppables
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
    }

    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    }

    // fix for gecko engine
    Element.cleanWhitespace(element);

    options.draggables = [];
    options.droppables = [];

    // drop on empty handling
    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
      var handle = options.handles ? $(options.handles[i]) :
        (options.handle ? $(e).select('.' + options.handle)[0] : e);
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);
    });

    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    // keep reference
    this.sortables[element.id] = options;

    // for onupdate
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },

  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },

  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);

    if(!Element.isParent(dropon, element)) {
      var index;

      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;

      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }

      dropon.insertBefore(element, child);

      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Sortable._marker.hide();
  },

  mark: function(dropon, position) {
    // mark on ghosting only
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return;

    if(!Sortable._marker) {
      Sortable._marker =
        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
          hide().addClassName('dropmarker').setStyle({position:'absolute'});
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});

    if(position=='after')
      if(sortable.overlap == 'horizontal')
        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
      else
        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});

    Sortable._marker.show();
  },

  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];

    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;

      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: [],
        position: parent.children.length,
        container: $(children[i]).down(options.treeTag)
      }

      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child)

      parent.children.push (child);
    }

    return parent;
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || { });

    var root = {
      id: null,
      parent: null,
      children: [],
      container: element,
      position: 0
    }

    return Sortable._tree(element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || { });

    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || { });

    var nodeMap = { };
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });

    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },

  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || { });
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);

    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" +
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
}

// Returns true if child is contained within element
Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;
  if (child.parentNode == element) return true;
  return Element.isParent(child.parentNode, element);
}

Element.findChildren = function(element, only, recursive, tagName) {
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
}

Element.offsetSize = function (element, type) {
  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
}
// script.aculo.us builder.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Builder = {
  NODEMAP: {
    AREA: 'map',
    CAPTION: 'table',
    COL: 'table',
    COLGROUP: 'table',
    LEGEND: 'fieldset',
    OPTGROUP: 'select',
    OPTION: 'select',
    PARAM: 'object',
    TBODY: 'table',
    TD: 'table',
    TFOOT: 'table',
    TH: 'table',
    THEAD: 'table',
    TR: 'table'
  },
  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
  //       due to a Firefox bug
  node: function(elementName) {
    elementName = elementName.toUpperCase();
    
    // try innerHTML approach
    var parentTag = this.NODEMAP[elementName] || 'div';
    var parentElement = document.createElement(parentTag);
    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
    } catch(e) {}
    var element = parentElement.firstChild || null;
      
    // see if browser added wrapping tags
    if(element && (element.tagName.toUpperCase() != elementName))
      element = element.getElementsByTagName(elementName)[0];
    
    // fallback to createElement approach
    if(!element) element = document.createElement(elementName);
    
    // abort if nothing could be created
    if(!element) return;

    // attributes (or text)
    if(arguments[1])
      if(this._isStringOrNumber(arguments[1]) ||
        (arguments[1] instanceof Array) ||
        arguments[1].tagName) {
          this._children(element, arguments[1]);
        } else {
          var attrs = this._attributes(arguments[1]);
          if(attrs.length) {
            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
              parentElement.innerHTML = "<" +elementName + " " +
                attrs + "></" + elementName + ">";
            } catch(e) {}
            element = parentElement.firstChild || null;
            // workaround firefox 1.0.X bug
            if(!element) {
              element = document.createElement(elementName);
              for(attr in arguments[1]) 
                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
            }
            if(element.tagName.toUpperCase() != elementName)
              element = parentElement.getElementsByTagName(elementName)[0];
          }
        } 

    // text, or array of children
    if(arguments[2])
      this._children(element, arguments[2]);

     return element;
  },
  _text: function(text) {
     return document.createTextNode(text);
  },

  ATTR_MAP: {
    'className': 'class',
    'htmlFor': 'for'
  },

  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
          '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'&quot;') + '"');
    return attrs.join(" ");
  },
  _children: function(element, children) {
    if(children.tagName) {
      element.appendChild(children);
      return;
    }
    if(typeof children=='object') { // array can hold nodes and text
      children.flatten().each( function(e) {
        if(typeof e=='object')
          element.appendChild(e)
        else
          if(Builder._isStringOrNumber(e))
            element.appendChild(Builder._text(e));
      });
    } else
      if(Builder._isStringOrNumber(children))
        element.appendChild(Builder._text(children));
  },
  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  },
  build: function(html) {
    var element = this.node('div');
    $(element).update(html.strip());
    return element.down();
  },
  dump: function(scope) { 
    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope 
  
    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);
  
    tags.each( function(tag){ 
      scope[tag] = function() { 
        return Builder.node.apply(Builder, [tag].concat($A(arguments)));  
      } 
    });
  }
}
// script.aculo.us controls.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
// 
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// Autocompleter.Base handles all the autocompletion functionality 
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least, 
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method 
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most 
// useful when one of the tokens is \n (a newline), as it 
// allows smart autocompletion after linebreaks.

if(typeof Effect == 'undefined')
  throw("controls.js requires including script.aculo.us' effects.js library");

var Autocompleter = { }
Autocompleter.Base = Class.create({
  baseInitialize: function(element, update, options) {
    element          = $(element)
    this.element     = element; 
    this.update      = $(update);  
    this.hasFocus    = false; 
    this.changed     = false; 
    this.active      = false; 
    this.index       = 0;     
    this.entryCount  = 0;
    this.oldElementValue = this.element.value;

    if(this.setOptions)
      this.setOptions(options);
    else
      this.options = options || { };

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;
    this.options.onShow       = this.options.onShow || 
      function(element, update){ 
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          Position.clone(element, update, {
            setHeight: false, 
            offsetTop: element.offsetHeight
          });
        }
        Effect.Appear(update,{duration:0.15});
      };
    this.options.onHide = this.options.onHide || 
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if(typeof(this.options.tokens) == 'string') 
      this.options.tokens = new Array(this.options.tokens);
    // Force carriage returns as token delimiters anyway
    if (!this.options.tokens.include('\n'))
      this.options.tokens.push('\n');

    this.observer = null;
    
    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, 'keypress', this.onKeyPress.bindAsEventListener(this));
  },

  show: function() {
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
    if(!this.iefix && 
      (Prototype.Browser.IE) &&
      (Element.getStyle(this.update, 'position')=='absolute')) {
      new Insertion.After(this.update, 
       '<iframe id="' + this.update.id + '_iefix" '+
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
      this.iefix = $(this.update.id+'_iefix');
    }
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  },
  
  fixIEOverlapping: function() {
    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    Element.show(this.iefix);
  },

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
    if(this.iefix) Element.hide(this.iefix);
  },

  startIndicator: function() {
    if(this.options.indicator) Element.show(this.options.indicator);
  },

  stopIndicator: function() {
    if(this.options.indicator) Element.hide(this.options.indicator);
  },

  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         Event.stop(event);
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         if(Prototype.Browser.WebKit) Event.stop(event);
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         if(Prototype.Browser.WebKit) Event.stop(event);
         return;
      }
     else 
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer = 
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },

  activate: function() {
    this.changed = false;
    this.hasFocus = true;
    this.getUpdatedChoices();
  },

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex) 
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },
  
  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },
  
  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;     
  }, 
  
  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ? 
          Element.addClassName(this.getEntry(i),"selected") : 
          Element.removeClassName(this.getEntry(i),"selected");
      if(this.hasFocus) { 
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },
  
  markPrevious: function() {
    if(this.index > 0) this.index--
      else this.index = this.entryCount-1;
    this.getEntry(this.index).scrollIntoView(true);
  },
  
  markNext: function() {
    if(this.index < this.entryCount-1) this.index++
      else this.index = 0;
    this.getEntry(this.index).scrollIntoView(false);
  },
  
  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },
  
  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },
  
  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    var value = '';
    if (this.options.select) {
      var nodes = $(selectedElement).select('.' + this.options.select) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
    
    var bounds = this.getTokenBounds();
    if (bounds[0] != -1) {
      var newValue = this.element.value.substr(0, bounds[0]);
      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
    } else {
      this.element.value = value;
    }
    this.oldElementValue = this.element.value;
    this.element.focus();
    
    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

  updateChoices: function(choices) {
    if(!this.changed && this.hasFocus) {
      this.update.innerHTML = choices;
      Element.cleanWhitespace(this.update);
      Element.cleanWhitespace(this.update.down());

      if(this.update.firstChild && this.update.down().childNodes) {
        this.entryCount = 
          this.update.down().childNodes.length;
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else { 
        this.entryCount = 0;
      }

      this.stopIndicator();
      this.index = 0;
      
      if(this.entryCount==1 && this.options.autoSelect) {
        this.selectEntry();
        this.hide();
      } else {
        this.render();
      }
    }
  },

  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onObserverEvent: function() {
    this.changed = false;   
    this.tokenBounds = null;
    if(this.getToken().length>=this.options.minChars) {
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
    this.oldElementValue = this.element.value;
  },

  getToken: function() {
    var bounds = this.getTokenBounds();
    return this.element.value.substring(bounds[0], bounds[1]).strip();
  },

  getTokenBounds: function() {
    if (null != this.tokenBounds) return this.tokenBounds;
    var value = this.element.value;
    if (value.strip().empty()) return [-1, 0];
    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
    var offset = (diff == this.oldElementValue.length ? 1 : 0);
    var prevTokenPos = -1, nextTokenPos = value.length;
    var tp;
    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
      if (tp > prevTokenPos) prevTokenPos = tp;
      tp = value.indexOf(this.options.tokens[index], diff + offset);
      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
    }
    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
  }
});

Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
  var boundary = Math.min(newS.length, oldS.length);
  for (var index = 0; index < boundary; ++index)
    if (newS[index] != oldS[index])
      return index;
  return boundary;
};

Ajax.Autocompleter = Class.create(Autocompleter.Base, {
  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  getUpdatedChoices: function() {
    this.startIndicator();
    
    var entry = encodeURIComponent(this.options.paramName) + '=' + 
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams) 
      this.options.parameters += '&' + this.options.defaultParams;
    
    new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }
});

// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
//                    text only at the beginning of strings in the 
//                    autocomplete array. Defaults to true, which will
//                    match text at the beginning of any *word* in the
//                    strings in the autocomplete array. If you want to
//                    search anywhere in the string, additionally set
//                    the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
//                   a partial match (unlike minChars, which defines
//                   how many characters are required to do any match
//                   at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
//                 Defaults to true.
//
// It's possible to pass in a custom function as the 'selector' 
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.

Autocompleter.Local = Class.create(Autocompleter.Base, {
  initialize: function(element, update, array, options) {
    this.baseInitialize(element, update, options);
    this.options.array = array;
  },

  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },

  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
      selector: function(instance) {
        var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
        var count     = 0;

        for (var i = 0; i < instance.options.array.length &&  
          ret.length < instance.options.choices ; i++) { 

          var elem = instance.options.array[i];
          var foundPos = instance.options.ignoreCase ? 
            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
            elem.indexOf(entry);

          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) { 
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
                elem.substr(entry.length) + "</li>");
              break;
            } else if (entry.length >= instance.options.partialChars && 
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
                break;
              }
            }

            foundPos = instance.options.ignoreCase ? 
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
              elem.indexOf(entry, foundPos + 1);

          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
        return "<ul>" + ret.join('') + "</ul>";
      }
    }, options || { });
  }
});

// AJAX in-place editor and collection editor
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).

// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
  setTimeout(function() {
    Field.activate(field);
  }, 1);
}

Ajax.InPlaceEditor = Class.create({
  initialize: function(element, url, options) {
    this.url = url;
    this.element = element = $(element);
    this.prepareOptions();
    this._controls = { };
    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
    Object.extend(this.options, options || { });
    if (!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + '-inplaceeditor';
      if ($(this.options.formId))
        this.options.formId = '';
    }
    if (this.options.externalControl)
      this.options.externalControl = $(this.options.externalControl);
    if (!this.options.externalControl)
      this.options.externalControlOnly = false;
    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
    this.element.title = this.options.clickToEditText;
    this._boundCancelHandler = this.handleFormCancellation.bind(this);
    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
    this._boundWrapperHandler = this.wrapUp.bind(this);
    this.registerListeners();
  },
  checkForEscapeOrReturn: function(e) {
    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
    if (Event.KEY_ESC == e.keyCode)
      this.handleFormCancellation(e);
    else if (Event.KEY_RETURN == e.keyCode)
      this.handleFormSubmission(e);
  },
  createControl: function(mode, handler, extraClasses) {
    var control = this.options[mode + 'Control'];
    var text = this.options[mode + 'Text'];
    if ('button' == control) {
      var btn = document.createElement('input');
      btn.type = 'submit';
      btn.value = text;
      btn.className = 'editor_' + mode + '_button';
      if ('cancel' == mode)
        btn.onclick = this._boundCancelHandler;
      this._form.appendChild(btn);
      this._controls[mode] = btn;
    } else if ('link' == control) {
      var link = document.createElement('a');
      link.href = '#';
      link.appendChild(document.createTextNode(text));
      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
      link.className = 'editor_' + mode + '_link';
      if (extraClasses)
        link.className += ' ' + extraClasses;
      this._form.appendChild(link);
      this._controls[mode] = link;
    }
  },
  createEditField: function() {
    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
    var fld;
    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
      fld = document.createElement('input');
      fld.type = 'text';
      var size = this.options.size || this.options.cols || 0;
      if (0 < size) fld.size = size;
    } else {
      fld = document.createElement('textarea');
      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
      fld.cols = this.options.cols || 40;
    }
    fld.name = this.options.paramName;
    fld.value = text; // No HTML breaks conversion anymore
    fld.className = 'editor_field';
    if (this.options.submitOnBlur)
      fld.onblur = this._boundSubmitHandler;
    this._controls.editor = fld;
    if (this.options.loadTextURL)
      this.loadExternalText();
    this._form.appendChild(this._controls.editor);
  },
  createForm: function() {
    var ipe = this;
    function addText(mode, condition) {
      var text = ipe.options['text' + mode + 'Controls'];
      if (!text || condition === false) return;
      ipe._form.appendChild(document.createTextNode(text));
    };
    this._form = $(document.createElement('form'));
    this._form.id = this.options.formId;
    this._form.addClassName(this.options.formClassName);
    this._form.onsubmit = this._boundSubmitHandler;
    this.createEditField();
    if ('textarea' == this._controls.editor.tagName.toLowerCase())
      this._form.appendChild(document.createElement('br'));
    if (this.options.onFormCustomization)
      this.options.onFormCustomization(this, this._form);
    addText('Before', this.options.okControl || this.options.cancelControl);
    this.createControl('ok', this._boundSubmitHandler);
    addText('Between', this.options.okControl && this.options.cancelControl);
    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
    addText('After', this.options.okControl || this.options.cancelControl);
  },
  destroy: function() {
    if (this._oldInnerHTML)
      this.element.innerHTML = this._oldInnerHTML;
    this.leaveEditMode();
    this.unregisterListeners();
  },
  enterEditMode: function(e) {
    if (this._saving || this._editing) return;
    this._editing = true;
    this.triggerCallback('onEnterEditMode');
    if (this.options.externalControl)
      this.options.externalControl.hide();
    this.element.hide();
    this.createForm();
    this.element.parentNode.insertBefore(this._form, this.element);
    if (!this.options.loadTextURL)
      this.postProcessEditField();
    if (e) Event.stop(e);
  },
  enterHover: function(e) {
    if (this.options.hoverClassName)
      this.element.addClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onEnterHover');
  },
  getText: function() {
    return this.element.innerHTML;
  },
  handleAJAXFailure: function(transport) {
    this.triggerCallback('onFailure', transport);
    if (this._oldInnerHTML) {
      this.element.innerHTML = this._oldInnerHTML;
      this._oldInnerHTML = null;
    }
  },
  handleFormCancellation: function(e) {
    this.wrapUp();
    if (e) Event.stop(e);
  },
  handleFormSubmission: function(e) {
    var form = this._form;
    var value = $F(this._controls.editor);
    this.prepareSubmission();
    var params = this.options.callback(form, value) || '';
    if (Object.isString(params))
      params = params.toQueryParams();
    params.editorId = this.element.id;
    if (this.options.htmlResponse) {
      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Updater({ success: this.element }, this.url, options);
    } else {
      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Request(this.url, options);
    }
    if (e) Event.stop(e);
  },
  leaveEditMode: function() {
    this.element.removeClassName(this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
    if (this.options.externalControl)
      this.options.externalControl.show();
    this._saving = false;
    this._editing = false;
    this._oldInnerHTML = null;
    this.triggerCallback('onLeaveEditMode');
  },
  leaveHover: function(e) {
    if (this.options.hoverClassName)
      this.element.removeClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onLeaveHover');
  },
  loadExternalText: function() {
    this._form.addClassName(this.options.loadingClassName);
    this._controls.editor.disabled = true;
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._form.removeClassName(this.options.loadingClassName);
        var text = transport.responseText;
        if (this.options.stripLoadedTextTags)
          text = text.stripTags();
        this._controls.editor.value = text;
        this._controls.editor.disabled = false;
        this.postProcessEditField();
      }.bind(this),
      onFailure: this._boundFailureHandler
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },
  postProcessEditField: function() {
    var fpc = this.options.fieldPostCreation;
    if (fpc)
      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
  },
  prepareOptions: function() {
    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
      Object.extend(this.options, defs);
    }.bind(this));
  },
  prepareSubmission: function() {
    this._saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  registerListeners: function() {
    this._listeners = { };
    var listener;
    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
      listener = this[pair.value].bind(this);
      this._listeners[pair.key] = listener;
      if (!this.options.externalControlOnly)
        this.element.observe(pair.key, listener);
      if (this.options.externalControl)
        this.options.externalControl.observe(pair.key, listener);
    }.bind(this));
  },
  removeForm: function() {
    if (!this._form) return;
    this._form.remove();
    this._form = null;
    this._controls = { };
  },
  showSaving: function() {
    this._oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    this.element.addClassName(this.options.savingClassName);
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
  },
  triggerCallback: function(cbName, arg) {
    if ('function' == typeof this.options[cbName]) {
      this.options[cbName](this, arg);
    }
  },
  unregisterListeners: function() {
    $H(this._listeners).each(function(pair) {
      if (!this.options.externalControlOnly)
        this.element.stopObserving(pair.key, pair.value);
      if (this.options.externalControl)
        this.options.externalControl.stopObserving(pair.key, pair.value);
    }.bind(this));
  },
  wrapUp: function(transport) {
    this.leaveEditMode();
    // Can't use triggerCallback due to backward compatibility: requires
    // binding + direct element
    this._boundComplete(transport, this.element);
  }
});

Object.extend(Ajax.InPlaceEditor.prototype, {
  dispose: Ajax.InPlaceEditor.prototype.destroy
});

Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
  initialize: function($super, element, url, options) {
    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
    $super(element, url, options);
  },

  createEditField: function() {
    var list = document.createElement('select');
    list.name = this.options.paramName;
    list.size = 1;
    this._controls.editor = list;
    this._collection = this.options.collection || [];
    if (this.options.loadCollectionURL)
      this.loadCollection();
    else
      this.checkForExternalText();
    this._form.appendChild(this._controls.editor);
  },

  loadCollection: function() {
    this._form.addClassName(this.options.loadingClassName);
    this.showLoadingText(this.options.loadingCollectionText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        var js = transport.responseText.strip();
        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
          throw 'Server returned an invalid collection representation.';
        this._collection = eval(js);
        this.checkForExternalText();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadCollectionURL, options);
  },

  showLoadingText: function(text) {
    this._controls.editor.disabled = true;
    var tempOption = this._controls.editor.firstChild;
    if (!tempOption) {
      tempOption = document.createElement('option');
      tempOption.value = '';
      this._controls.editor.appendChild(tempOption);
      tempOption.selected = true;
    }
    tempOption.update((text || '').stripScripts().stripTags());
  },

  checkForExternalText: function() {
    this._text = this.getText();
    if (this.options.loadTextURL)
      this.loadExternalText();
    else
      this.buildOptionList();
  },

  loadExternalText: function() {
    this.showLoadingText(this.options.loadingText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._text = transport.responseText.strip();
        this.buildOptionList();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },

  buildOptionList: function() {
    this._form.removeClassName(this.options.loadingClassName);
    this._collection = this._collection.map(function(entry) {
      return 2 === entry.length ? entry : [entry, entry].flatten();
    });
    var marker = ('value' in this.options) ? this.options.value : this._text;
    var textFound = this._collection.any(function(entry) {
      return entry[0] == marker;
    }.bind(this));
    this._controls.editor.update('');
    var option;
    this._collection.each(function(entry, index) {
      option = document.createElement('option');
      option.value = entry[0];
      option.selected = textFound ? entry[0] == marker : 0 == index;
      option.appendChild(document.createTextNode(entry[1]));
      this._controls.editor.appendChild(option);
    }.bind(this));
    this._controls.editor.disabled = false;
    Field.scrollFreeActivate(this._controls.editor);
  }
});

//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
//**** This only  exists for a while,  in order to  let ****
//**** users adapt to  the new API.  Read up on the new ****
//**** API and convert your code to it ASAP!            ****

Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
  if (!options) return;
  function fallback(name, expr) {
    if (name in options || expr === undefined) return;
    options[name] = expr;
  };
  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
    options.cancelLink == options.cancelButton == false ? false : undefined)));
  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
    options.okLink == options.okButton == false ? false : undefined)));
  fallback('highlightColor', options.highlightcolor);
  fallback('highlightEndColor', options.highlightendcolor);
};

Object.extend(Ajax.InPlaceEditor, {
  DefaultOptions: {
    ajaxOptions: { },
    autoRows: 3,                                // Use when multi-line w/ rows == 1
    cancelControl: 'link',                      // 'link'|'button'|false
    cancelText: 'cancel',
    clickToEditText: 'Click to edit',
    externalControl: null,                      // id|elt
    externalControlOnly: false,
    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
    formClassName: 'inplaceeditor-form',
    formId: null,                               // id|elt
    highlightColor: '#ffff99',
    highlightEndColor: '#ffffff',
    hoverClassName: '',
    htmlResponse: true,
    loadingClassName: 'inplaceeditor-loading',
    loadingText: 'Loading...',
    okControl: 'button',                        // 'link'|'button'|false
    okText: 'ok',
    paramName: 'value',
    rows: 1,                                    // If 1 and multi-line, uses autoRows
    savingClassName: 'inplaceeditor-saving',
    savingText: 'Saving...',
    size: 0,
    stripLoadedTextTags: false,
    submitOnBlur: false,
    textAfterControls: '',
    textBeforeControls: '',
    textBetweenControls: ''
  },
  DefaultCallbacks: {
    callback: function(form) {
      return Form.serialize(form);
    },
    onComplete: function(transport, element) {
      // For backward compatibility, this one is bound to the IPE, and passes
      // the element directly.  It was too often customized, so we don't break it.
      new Effect.Highlight(element, {
        startcolor: this.options.highlightColor, keepBackgroundImage: true });
    },
    onEnterEditMode: null,
    onEnterHover: function(ipe) {
      ipe.element.style.backgroundColor = ipe.options.highlightColor;
      if (ipe._effect)
        ipe._effect.cancel();
    },
    onFailure: function(transport, ipe) {
      alert('Error communication with the server: ' + transport.responseText.stripTags());
    },
    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
    onLeaveEditMode: null,
    onLeaveHover: function(ipe) {
      ipe._effect = new Effect.Highlight(ipe.element, {
        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
        restorecolor: ipe._originalBackground, keepBackgroundImage: true
      });
    }
  },
  Listeners: {
    click: 'enterEditMode',
    keydown: 'checkForEscapeOrReturn',
    mouseover: 'enterHover',
    mouseout: 'leaveHover'
  }
});

Ajax.InPlaceCollectionEditor.DefaultOptions = {
  loadingCollectionText: 'Loading options...'
};

// Delayed observer, like Form.Element.Observer, 
// but waits for delay after last key input
// Ideal for live-search fields

Form.Element.DelayedObserver = Class.create({
  initialize: function(element, delay, callback) {
    this.delay     = delay || 0.5;
    this.element   = $(element);
    this.callback  = callback;
    this.timer     = null;
    this.lastValue = $F(this.element); 
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  },
  delayedListener: function(event) {
    if(this.lastValue == $F(this.element)) return;
    if(this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
    this.lastValue = $F(this.element);
  },
  onTimerEvent: function() {
    this.timer = null;
    this.callback(this.element, $F(this.element));
  }
});
//Destroy method for Autocompleter
//HACK: Autocompleter does not save copies of the actual event
//  listner functions, this destructor stops all listeners on the events.
//  Also updated is the updateChoices to first remove event listeners on items.
//  Finally, patched updateElement so that it doesn't bomb in IE when the element is hidden
Autocompleter.Base.prototype.origUpdateChoices = Autocompleter.Base.prototype.updateChoices;
Object.extend(Autocompleter.Base.prototype, {
	updateElement: function(selectedElement) {
		if (this.options.updateElement) {
			this.options.updateElement(selectedElement);
			return;
		}
		var value = '';
		if (this.options.select) {
			var nodes = $(selectedElement).select('.' + this.options.select) || [];
			if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
		} else
			value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');

		var bounds = this.getTokenBounds();
		if (bounds[0] != -1) {
			var newValue = this.element.value.substr(0, bounds[0]);
			var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
			if (whitespace)
			newValue += whitespace[0];
			this.element.value = newValue + value + this.element.value.substr(bounds[1]);
		} else {
			this.element.value = value;
		}
		this.oldElementValue = this.element.value;
		try {
			this.element.focus();
		} catch(ex) {}

		if (this.options.afterUpdateElement)
			this.options.afterUpdateElement(this.element, selectedElement);
	},
	updateChoices: function(choices) {
		var ul = this.update.down();
        this.entryCount = ul ? ul.childNodes.length : 0;
        for(var i = 0; i < this.entryCount; i++) {
			var entry = this.getEntry(i);
			entry.autocompleteIndex = i;
			this.removeObservers(entry);
        }
        this.origUpdateChoices(choices);
	},
	removeObservers: function(element) {
	    Event.stopObserving(element, "mouseover");
	    Event.stopObserving(element, "click");
	},
	destroy: function() {
	    Event.stopObserving(this.element, 'blur');
	    Event.stopObserving(this.element, 'keypress');
		if(this.iefix) this.iefix.remove();
		this.updateChoices("<ul></ul>");
	}
});Number.validRegEx = /^-{0,1}\d*\.{0,1}\d+$/;
Number.thousandsSeparator = ",";
Number.prototype.formatThousands = function(decimals) {
	var n = Math.abs(this);
	return (this < 0 ? "-" : "") + (n.toFixed(1) + "").split('.')[0].split('').reverse().join('').match(/\d{3}|\d+$/g).join(Number.thousandsSeparator).split('').reverse().join('') +
		(decimals > 0 ? '.' + n.toFixed(decimals).toString().split('.')[1] : "");
};
Number.getSeparatorExp = function() {
	try { return new RegExp(Number.thousandsSeparator, "g") } catch(ex) {}
	try { return new RegExp("\\" + Number.thousandsSeparator, "g") } catch(ex) {}
	return new RegExp("$^", "g");
};

String.prototype.isNumber = function() {
	return Number.validRegEx.test(this.replace(Number.getSeparatorExp(), ""));
};
String.prototype.parseNumber = function() {
	return this.isNumber() ? parseFloat(this.replace(Number.getSeparatorExp(), "")) : NaN;
};

Number.parse = function(string) {
	return ("" + string).parseNumber();
};

Number.prototype.leadingZeros = function(n) {
	var res = "" + this;
	if(n === null) n = 1;
	while(res.length < n) {
		res = "0" + res;
	}
	return res;
};

Number.prototype.inRange = function(l, u) {
	return this >= l && this <= u;
};if(typeof Widget == "undefined") Widget = {};
Widget.Cookies = {
	/* Code originally from http://www.quirksmode.org/js/cookies.html */
	createCookie: function(name, value, days) {
		if(days) {
			var date = new Date();
			date.setTime(date.getTime()+(days*24*60*60*1000));
			var expires = "; expires="+date.toGMTString();
		}
		else var expires = "";
		document.cookie = name + "=" + value + expires + "; url=/";
	},

	readCookie: function(name) {
		var nameEQ = name + "=";
		var ca = document.cookie.split(';');
		for(var i=0;i < ca.length;i++) {
			var c = ca[i];
			while (c.charAt(0)==' ') c = c.substring(1,c.length);
			if(c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
		}
		return null;
	},

	eraseCookie: function(name) {
		createCookie(name,"",-1);
	}
};var Widget = window.Widget || {};
Widget.Styles = {
	/* Stylesheet manipulation - addRule code based on http://www.thescripts.com/forum/thread98428.html, */
	/*   others from yui-ext 0.33 (C) Copyright(c) 2006, Jack Slocum. */
	/*   updateRules added by Marc in an attempt to crank some speed out of this */
	/*   addStyleSheet added by Marc and modified getRules, getRule, addRule, updateRule */
	/*   removeStyleSheet added by Marc */
	globalCache: {},
	sheetCache: {},
	styleSheets: {},
	mustRefresh: true,
	count: 0,
	addStyleSheet: function(doc) {
		if(!doc) doc = document;
		if(doc.styleSheets) {
			var styleElement;
			if(doc.createElement && (styleElement = document.createElement("style"))) {
				styleElement.type = 'text/css';
				doc.getElementsByTagName('head')[0].appendChild(styleElement);
				var sheet = styleElement.sheet || doc.styleSheets[doc.styleSheets.length - 1];
				sheet.index = this.count++;
				this.styleSheets[sheet.index] = styleElement;
				styleElement.disabled = false;
				sheet.disabled = false;
				return sheet;
			}
		}
		return null;
	},
	removeStyleSheet: function(styleSheet) {
		var styleElement = this.styleSheets[styleSheet];
		if(styleElement) {
			styleSheet.disabled = true;
			styleElement.disabled = true;
			styleElement.parentNode.removeChild(styleElement);
		}
	},
	addRule: function(selector, rule, styleSheet, doc) {
		if(!doc) doc = document;
		if(!styleSheet && doc.styleSheets.length > 0) {
			styleSheet = doc.styleSheets[0/*doc.styleSheets.length - 1*/];
		}
		if(!styleSheet) styleSheet = addStyleSheet(doc);
		if(!styleSheet) return;
		if(styleSheet.insertRule) {
			styleSheet.insertRule(selector + ' { ' + rule + ' }', styleSheet.cssRules.length);
		} else if(styleSheet.addRule) {
			styleSheet.addRule(selector, rule);
		}
		this.mustRefresh = true;
	},
	addRules: function(rules, styleSheet) {
		for(var i = 0, l = rules.length; i < l; ++i) {
			this.addRule(rules[i].selector, rules[i].rule, styleSheet);
		}
	},
	getRules: function(styleSheet, refresh) {
		if(this.mustRefresh || refresh){
			this.globalCache = {};
			this.sheetCache = {};
			var ds = document.styleSheets;
			for(var i = 0, len = ds.length; i < len; i++) {
				try{
					var ss = ds[i], sc = null;
					if(ss.index) {
						sc = this.sheetCache[ss.index] = {};
					}
					var ssRules = ss.cssRules || ss.rules;
					for(var j = ssRules.length - 1; j >= 0; --j) {
						this.globalCache[ssRules[j].selectorText] = ssRules[j];
						if(sc) sc[ssRules[j].selectorText] = ssRules[j];
					}
				} catch(e) {} // try catch for cross domain access issue
			}
			this.mustRefresh = false;
		}
		if(styleSheet) {
			return this.sheetCache[styleSheet.index];
		}
		return this.globalCache;
	},
	getRule: function(selector, styleSheet, refresh) {
		var rs;
		if(styleSheet) {
			rs = this.sheetCache[styleSheet];
			if(!rs) rs = this.getRules(styleSheet, refresh);
		} else {
			rs = this.getRules(null, refresh);
		}
		if(!(selector instanceof Array)) {
			return rs[selector];
		}
		for(var i = 0; i < selector.length; i++) {
			if(rs[selector[i]]){
				return rs[selector[i]];
			}
		}
		return null;
	},
	updateRule: function(selector, name, value, styleSheet) {
   		if(!(selector instanceof Array)) {
   			var rule = this.getRule(selector, styleSheet);
   			if(rule){
   				rule.style[name.camelize()] = value;
   				return true;
   			}
   		} else {
   			for(var i = 0; i < selector.length; i++) {
   				if(this.updateRule(selector[i], property, value)) {
   					return true;
   				}
   			}
   		}
   		return false;
	},
	updateRules: function(rules, styleSheet) {
		var i, rs = this.getRules(styleSheet), r;
		for(i = 0; i < rules.length; ++i) {
			r = rules[i];
			rs[r.selector].style[r.name] = r.value;
		}
	}
};
Widget.Styles.refresh = Widget.Styles.getRules;
//Original code - now very modified - from epoch calendar control
Date.dayLength = 1000 * 60 * 60 * 24;
Date.weekLength = Date.dayLength * 7;

Date.prototype.getDayOfYear = function() {
	return parseInt((this.getTime() - new Date(this.getFullYear(),0,1).getTime())/86400000 + 1);
};
Date.prototype.getWeek = function() {
	var w = Math.ceil((this.getTime() - new Date(this.getFullYear(),0,1).getTime())/604800000 + 1);
	return (w == 53 ? 1 : w);
};
Date.prototype.getUeDay = function() {
	return parseInt(Math.floor((this.getTime() - this.getTimezoneOffset() * 60000)/86400000)); //must take into account the local timezone
};
//Modification by Marc Heiligers - make month and day names available globally.
Date.MonthNames = ['January','February','March','April','May','June','July','August','September','October','November','December','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
Date.DayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sun','Mon','Tue','Wed','Thu','Fri','Sat','S','M','T','W','T','F','S'];
Date.SUNDAY = 0;
Date.MONDAY = 1;
Date.TUESDAY = 2;
Date.WEDNESDAY = 3;
Date.THURSDAY = 4;
Date.FRIDAY = 5;
Date.SATURDAY = 6;
Date.weekStart = Date.SUNDAY;

//TODO: Pattern
Date.FORMATPATTERN = null;
Date.DEFAULTFORMAT = '#{D} #{MMM} #{YYYY}';
Date.prototype.format = function(formatString, pattern) {
	if(!pattern) pattern = Date.FORMATPATTERN;
	if(!formatString) formatString = Date.DEFAULTFORMAT;
	return formatString.interpolate({
		YY: parseInt(("" + this.getFullYear()).slice(-1)).leadingZeros(2),
		YYYY: this.getFullYear(),
		M: this.getMonth() + 1,
		MM: (this.getMonth() + 1).leadingZeros(2),
		MMM: Date.MonthNames[this.getMonth() + 12],
		MMMM: Date.MonthNames[this.getMonth()],
		D: this.getDate(),
		DD: this.getDate().leadingZeros(2),
		DDD: Date.DayNames[this.getDay() + 7],
		DDDD: Date.DayNames[this.getDay()]
		//TODO: Time formats
	}, pattern);
};

/*
 * JavaScript Pretty Date
 * Copyright (c) 2008 John Resig (jquery.com)
 * Licensed under the MIT license.
 */
/*
// Takes an ISO time and returns a string representing how
// long ago the date represents.
function prettyDate(time){
	var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ")),
		diff = (((new Date()).getTime() - date.getTime()) / 1000),
		day_diff = Math.floor(diff / 86400);

	if ( isNaN(day_diff) || day_diff < 0 || day_diff >= 31 )
		return;

	return day_diff == 0 && (
			diff < 60 && "just now" ||
			diff < 120 && "1 minute ago" ||
			diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
			diff < 7200 && "1 hour ago" ||
			diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
		day_diff == 1 && "Yesterday" ||
		day_diff < 7 && day_diff + " days ago" ||
		day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago";
}
*/

//Additional methods by Marc Heiligers:
//TODO: make Prototype extensions
Date.prototype.addMilliseconds = function(ms) {
	return new Date(new Date().setTime(this.getTime() + (ms)));
};

Date.prototype.addHours = function(hours) {
	return this.addMilliseconds(hours * 3600000);
};

Date.prototype.addDays = function(days) {
	return this.addMilliseconds(days * 86400000);
};

Date.prototype.addYears = function(years) {
	return new Date(this.getFullYear() + years, this.getMonth(), this.getDate(), this.getHours(), this.getMinutes(), this.getSeconds(), this.getMilliseconds());
};

Date.prototype.subtract = function(date) {
	return new DateSpan(this, date);
};

Date.prototype.isLeapYear = function() {
	var year = this.getYear();
	if (year % 4 == 0 && year % 400 != 0) {
		return true;
	}
	return false;
};

Date.prototype.daysInMonth = function() {
	var month = this.getMonth();
	if (month == 1) {
		if (this.isLeapYear()) {
			return 29;
		}
		return 28;
	}
	switch(month) {
		case 0:	case 2:	case 4:	case 6:	case 7:	case 9:	case 11: return 31;
		default: return 30;
	}
};

Date.prototype.getDateOnly = function() {
	return new Date(this.getFullYear(), this.getMonth(), this.getDate());
};

Date.prototype.getDayOf = function() {
	return new DateSpan(this.getDateOnly(), this.getDateOnly());
};

Date.prototype.getWeekOf = function() {
	var dayDiff = this.getDay();
	if (dayDiff < 0) dayDiff += 7;
	var start = this.addDays(-dayDiff).getDateOnly();
	return new DateSpan(start.addDays(Date.weekStart), start.addDays(6 + Date.weekStart));
};

Date.prototype.getMonthOf = function() {
	return new DateSpan(
		new Date(this.getFullYear(), this.getMonth(), 1),
		new Date(this.getFullYear(), this.getMonth(), this.daysInMonth())
	);
};

Date.prototype.getQuarterOf = function() {
	var month = parseInt(this.getMonth() / 3) * 3;
	var endYear = this.getFullYear();
	var endMonth = month + 2;
	if (endMonth > 11) {
		year++;
		endMonth -= 12;
	}
	var endDate = new Date(endYear, endMonth, 1);
	return new DateSpan(
		new Date(this.getFullYear(), month, 1),
		new Date(endYear, endMonth, endDate.daysInMonth())
	);
};

Date.prototype.getYearOf = function() {
	return new DateSpan(
		new Date(this.getFullYear(), 0, 1),
		new Date(this.getFullYear(), 11, 31)
	);
};

Date.prototype.getPrevDayOf = function() {
	return this.addDays(-1).getDayOf();
};

Date.prototype.getNextDayOf = function() {
	return this.addDays(1).getDayOf();
};

Date.prototype.getPrevWeekOf = function() {
	return this.addDays(-7).getWeekOf();
};

Date.prototype.getNextWeekOf = function() {
	return this.addDays(7).getWeekOf();
};

Date.prototype.getPrevMonthOf = function() {
	return new Date(this.getFullYear(), this.getMonth(), 1).addDays(-1).getMonthOf();
};

Date.prototype.getNextMonthOf = function() {
	return new Date(this.getFullYear(), this.getMonth(), this.daysInMonth()).addDays(1).getMonthOf();
};

Date.prototype.getPrevQuarterOf = function() {
	var month = this.getMonth() - 3;
	var year = this.getFullYear();
	if (month < 0) {
		year--;
		month += 12;
	}
	return new Date(year, month, 1).getQuarterOf();
};

Date.prototype.getNextQuarterOf = function() {
	var month = this.getMonth() + 3;
	var year = this.getFullYear();
	if (month > 11) {
		year++;
		month -= 12;
	}
	return new Date(year, month, 1).getQuarterOf();
};

Date.prototype.getPrevYearOf = function() {
	return new Date(this.getFullYear() - 1, 0, 1).getYearOf();
};

Date.prototype.getNextYearOf = function() {
	return new Date(this.getFullYear() + 1, 0, 1).getYearOf();
};

var DateSpan = Class.create({
	initialize: function(start, end) {
		this.start = start;
		this.end = end;
	},
	intersects: function(period) {
		return (period.start <= this.end && period.end >= this.start);
	},
	contains: function(period) {
		return (period.start >= this.start && period.end <= this.end);
	},
	containsDate: function(date) {
		return (date >= this.start && date <= this.end);
	},
	totalDays: function() {
		return (this.end.getTime() - this.start.getTime()) / 86400000;
	},
	format: function(formatString, pattern, dateFormatString, datePattern) {
		if(!formatString) formatString = DateSpan.DEFAULTFORMAT;
		if(!pattern) pattern = DateSpan.FORMATPATTERN;
		if(!dateFormatString) dateFormatString = Date.DEFAULTFORMAT;
		if(!datePattern) datePattern = Date.FORMATPATTERN;
		var o = {
			S: this.start.format(dateFormatString, datePattern),
			E: this.end.format(dateFormatString, datePattern),
			D: Math.ceil(this.totalDays()),
			d: Math.floor(this.totalDays())
		};
		o.Ds = o.D == 1 ? DateSpan.DAY : DateSpan.DAYS;
		return formatString.interpolate(o);
	},
	toString: function() {
		return format.apply(arguments);
	}
});
DateSpan.FORMATPATTERN = 'Ds|D|S|E';
DateSpan.DEFAULTFORMAT = 'D DD (S - E)';
DateSpan.DAY = "day";
DateSpan.DAYS = "days";/**
 * @fileoverview activator.js
 * Widget.Activator activates controls with highlighters, selection, and en/disabling functionality.<br />
 * Requires Prototype (http://www.prototypejs.org) 1.6 or later<br /><br />
 *
 * Copyright (c) 2007 - 2008 Marc Heiligers (marc@eternal.co.za) http://www.eternal.co.za<br /><br />
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.<br /><br />
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.<br /><br />
 *
 * Change History:<br />
 * Version 0.2.00: 11 Oct 2008<br />
 * - Prepared for release, updated docs and demo pages<br />
 * - Added MIT license<br />
 * Version 0.1.15: 8 Oct 2008<br />
 * - BREAKING CHANGE: the event memo is no longer the element but now contains the following properties:<br />
 * 		element - the element<br />
 * 		activator - the activator<br />
 * 		event - the event that triggered this event<br />
 * Version 0.1.14: 5 Oct 2008<br />
 * - Fixed bug where elements were still firing mouseout when disabled<br />
 * Version 0.1.13: 8 Jun 2008<br />
 * - Updated setEnabled so that it removes the active and hover styles if you disable an element<br />
 * Version 0.1.12: 18 Apr 2008<br />
 * - Updated apply so that it ensures that selected and enabled elements remain so<br />
 * Version 0.1.11: 17 Mar 2008<br />
 * - Fixed a bug in setSelected and setEnabled where the default was not true<br />
 * Version 0.1.10: 12 Mar 2008<br />
 * - Added click event<br />
 * Version 0.1.9: 7 Mar 2008<br />
 * - Added this.selector to save the original selector<br />
 * - Added apply method which removes all and reapplies the original selector<br />
 * Version 0.1.8: 3 Mar 2008<br />
 * - Moved event name options into their own events options<br />
 * Version 0.1.7: 25 Feb 2008<br />
 * - Fixed a bug where the elements array was not correctly compacted after remove<br />
 * Version 0.1.6: 23 Feb 2008<br />
 * - getElements will return all elements if passed null or *<br />
 * - The above affects methods like remove: remove() or remove("*") will remove all elements<br />
 * - Selector in constructor is now optional<br />
 * - Minor improvement in constructor - just call add instead of calling add for each element returned from $$(selector) which add does anyway<br />
 * - Fixed a bug in add where it would incorrectly try to add a selector string passed in<br />
 * - Moved className options into a classNames object<br />
 * Version 0.1.5: 18 Feb 2008<br />
 * - Fixed a bugs in selectAll and enableAll where the default was not true<br />
 * Version 0.1.4: 19 Jan 2008<br />
 * - Fixed a minor bug in add<br />
 * - Added possibility for selector to be null - TODO: paramater mashing<br />
 * Version 0.1.3: 10 Dec 2007<br />
 * - Added mousedownEvent and mouseupEvent names to options<br />
 * Version 0.1.2: 19 Oct 2007<br />
 * - Added handlers array and updated destroy method to stopObserving all event handlers<br />
 * Version 0.1.1: 18 Oct 2007<br />
 * - Improved #isElement method to allow for sliding doors<br />
 * - Turned event names into properties of Widget.Activator (Widget.Activator.mousedownEvent and Widget.Activator.mouseupEvent)<br />
 * Version 0.1.0: 14 Oct 2007<br />
 * - Initial version
 */
if(typeof Widget == "undefined") Widget = {};
 /**
 * @class Widget.Activator
 * @version 0.1.13
 * @author Marc Heiligers marc@eternal.co.za http://www.eternal.co.za
 */
Widget.Activator = Class.create(/** @scope Widget.Activator **/{
	/**
	 * The Activator class constructor.
	 * @constructor Activator
	 * @param {string} [selector] A CSS selector string which is defines one or more elements to be activated
	 * @param {object} [options] An object hash of options.
	 */
	initialize: function(selector, options) {
		if(selector && !Object.isString(selector)) {
			options = selector;
			selector = null;
		}

		/**
		 * The default options.classNames.normals object.
		 * @class
		 * @param {string} [normal] The className to be applied to all activated elements. (default: null)
		 * @param {string} [hover] The className to be applied when the element is hovered. (default: "hover")
		 * @param {string} [active] The className to be applied when the element is active (mouse down). (default: "active")
		 * @param {string} [selected] The className to be applied when the element is selected. (default: "selected")
		 * @param {string} [disbaled] The className to be applied when the element is disabled. (default: "disabled")
		 */
		var classNames = Object.extend({
			normal: null,
			hover: "hover",
			active: "active",
			selected: "selected",
			disabled: "disabled"
		}, options ? options.classNames || {} : {});

		/**
		 * The default options.classNames.normals object.
		 * @class
		 * @param {string} [mouseover] The name of the mouse over event. (default: Widget.Activator.mouseoverEvent = "activator:mouseover")
		 * @param {string} [mouseout] The name of the mouse out event. (default: Widget.Activator.mouseoutEvent = "activator:mouseout")
		 * @param {string} [mousedown] The name of the mouse down event. (default: Widget.Activator.mousedownEvent = "activator:mousedown")
		 * @param {string} [mouseup] The name of the mouse up event. (default: Widget.Activator.mouseupEvent = "activator:mouseup")
		 * @param {string} [click] The name of the click event. (default: Widget.Activator.clickEvent = "activator:click")
		 */
		var events = Object.extend({
			mouseover: Widget.Activator.mouseoverEvent,
			mouseout: Widget.Activator.mouseoutEvent,
			mousedown: Widget.Activator.mousedownEvent,
			mouseup: Widget.Activator.mouseupEvent,
			click: Widget.Activator.clickEvent
		}, options ? options.events || {} : {});

		/**
		 * The default options object.
		 * @class
		 * @param {object} [classNames] The classNames object to be applied to all activated elements. (default: null)
		 * @param {object} [events] The event names to be used. (default: null)
		 * @param {string} [container] An element that contains all activated elements, and that is the source of all activator events. If null, document.body is used. (default: null)
		 * @param {bool} [singleSelect] If true, only one element can be selected at a time. (default: false)
		 * @param {bool} [extendElements] If true, all activated elements are extended with #getActivator(), #isSelected(), #setSelected(bool), #isEnabled(), and #setEnabled(bool) methods. (default: false)
		 */
		this.options = Object.extend({
			container: null,
			singleSelect: false,
			extendElements: false
		}, options || {});
		this.options.classNames = classNames;
		this.options.events = events;

		this.mouseoverListener = this.mouseover.bindAsEventListener(this);
		this.mouseoutListener = this.mouseout.bindAsEventListener(this);
		this.mousedownListener = this.mousedown.bindAsEventListener(this);
		this.mouseupListener = this.mouseup.bindAsEventListener(this);
		this.clickListener = this.click.bindAsEventListener(this);
		if(this.options.container) {
			this.options.container = $(this.options.container);
			this.options.container.observe("mouseover", this.mouseoverListener);
			this.options.container.observe("mouseout", this.mouseoutListener);
			this.options.container.observe("mousedown", this.mousedownListener);
			this.options.container.observe("mouseup", this.mouseupListener);
			this.options.container.observe("click", this.clickListener);
		}

		this.elements = [];
		if(selector) {
			this.add(selector);
			this.selector = selector;
		}

		this.handlers = [];
	},
	/**
	 * Removes all elements and then adds elements based on the selector, or the original selector if none is passed.
	 * @param {string|Array|element} elements A CSS selector string, array of elements, or single element to add.
	 */
	apply: function(elements) {
		var s = this.getSelected();
		var d = this.getDisabled();
		this.remove("*");
		this.add(elements ? elements : this.selector);
		this.setSelected(s);
		this.setEnabled(d, false);
	},
	/**
	 * Add elements to the activator.
	 * @param {string|Array|element} elements A CSS selector string, array of elements, or single element to add.
	 */
	add: function(elements) {
		if(Object.isString(elements)) return this.add(this.options.container ? this.options.container.select(elements) : $$(elements));
		if(Object.isArray(elements)) {
			elements.each(function(e) {
				this.add(e);
			}, this);
			return;
		}
		var element = $(elements);
		if(this.options.container) {
			var t = element.ancestors().find(function(a) {
				return a == this.options.container;
			}.bind(this));
			if(!t) {
				throw Widget.Activator.notContainerChild.interpolate({ element: element.identify(), container: this.options.container.identify() });
			}
		} else {
			element.observe("mouseover", this.mouseoverListener);
			element.observe("mouseout", this.mouseoutListener);
			element.observe("mousedown", this.mousedownListener);
			element.observe("mouseup", this.mouseupListener);
			element.observe("click", this.clickListener);
		}
		if(this.options.classNames.normal) element.addClassName(this.options.classNames.normal);
		this.elements.push(element);
		element[Widget.Activator.activatorAttribute] = this;
		element[Widget.Activator.selectedAttribute] = false;
		element[Widget.Activator.enabledAttribute] = true;
		if(this.options.extendElements) {
			element[Widget.Activator.getActivatorFunction] = function() { return this[Widget.Activator.activatorAttribute]; };
			element[Widget.Activator.setSelectedFunction] = function(selected) { this.getActivator().setSelected(this, selected); };
			element[Widget.Activator.isSelectedFunction] = function() { return this.getActivator().isSelected(this); };
			element[Widget.Activator.setEnabledFunction] = function(enabled) { this.getActivator().setEnabled(this, enabled); };
			element[Widget.Activator.isEnabledFunction] = function() { return this.getActivator().isEnabled(this); };
		}
	},
	/**
	 * Remove elements from the activator.
	 * @param {string|Array|number|element} elements A CSS selector string, array of elements, index of an element, or single element to remove.
	 */
	remove: function(elements) {
		this.getElements(elements).each(function(e) {
			if(!this.options.container) {
				e.stopObserving("mouseover", this.mouseoverListener);
				e.stopObserving("mouseout", this.mouseoutListener);
				e.stopObserving("mousedown", this.mousedownListener);
				e.stopObserving("mouseup", this.mouseupListener);
				e.stopObserving("click", this.clickListener);
			}
			$w("activatorAttribute selectedAttribute enabledAttribute getActivatorFunction setSelectedFunction isSelectedFunction setEnabledFunction isEnabledFunction").each(function(a) {
				//if(!Object.isUndefined(e[Widget.Activator[a]])) delete e[Widget.Activator[a]];
				e[Widget.Activator[a]] = null;
			});
			/* TODO: Figure out why these die in IE
			delete e[Widget.Activator.activatorAttribute];
			delete e[Widget.Activator.selectedAttribute];
			delete e[Widget.Activator.enabledAttribute];
			if(this.options.extendElements) {
				delete e[Widget.Activator.getActivatorFunction];
				delete e[Widget.Activator.setSelectedFunction];
				delete e[Widget.Activator.isSelectedFunction];
				delete e[Widget.Activator.setEnabledFunction];
				delete e[Widget.Activator.isEnabledFunction];
			}*/
			e.removeClassName(this.options.classNames.hover);
			e.removeClassName(this.options.classNames.active);
			e.removeClassName(this.options.classNames.selected);
			e.removeClassName(this.options.classNames.disabled);
			if(this.options.classNames.normal) e.removeClassName(this.options.classNames.normal);
			this.elements[this.elements.indexOf(e)] = null;
		}.bind(this));
		this.elements = this.elements.compact();
	},
	/**
	 * The mouseover event handler
	 * @private
	 */
	mouseover: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			e.addClassName(this.options.classNames.hover);
			this.fire(this.options.events.mouseover, e, event);
		}
	},
	/**
	 * The mouseout event handler
	 * @private
	 */
	mouseout: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			e.removeClassName(this.options.classNames.hover);
			e.removeClassName(this.options.classNames.active);
			this.fire(this.options.events.mouseout, e, event);
		}
	},
	/**
	 * The mousedown event handler
	 * @private
	 */
	mousedown: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			e.addClassName(this.options.classNames.active);
			this.fire(this.options.events.mousedown, e, event);
		}
	},
	/**
	 * The mouseup event handler
	 * @private
	 */
	mouseup: function(event) {
		var e = this.isElement(event.element());
		if(e) e.removeClassName(this.options.classNames.active);
		if(e && !e[Widget.Activator.disabledAttribute]) {
			this.fire(this.options.events.mouseup, e, event);
		}
	},
	/**
	 * The click event handler
	 * @private
	 */
	click: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			this.fire(this.options.events.click, e, event);
		}
	},
	/**
	 * Set an event handler for an activator event. The Activator class fires mousedown and mouseup events.
	 * Event handlers are attached to the options.container if there is one, and document.body if not.
	 * If there are multiple Activators without containers, all event handlers will receive messages for
	 * all the activators. You can test to see if the element is part of this activator using the isElement method.
	 * The element for which the event was fired is in event.memo.
	 * @param {string} eventName The name of the event. Use Widget.Activator.mousedownHandler and Widget.Activator.mouseupHandler constants.
	 * @param {function} elements Your event handler function.
	 */
	observe: function(eventName, handler) {
		if(this.options.container) {
			this.options.container.observe(eventName, handler);
		} else {
			$(document).observe(eventName, handler);
		}
		this.handlers.push({ eventName: eventName, handler: handler });
		return this;
	},
	/**
	 * Remove an event handler for an activator event.
	 * @param {string} eventName The name of the event. Use Widget.Activator.mousedownHandler and Widget.Activator.mouseupHandler constants.
	 * @param {function} elements Your event handler function.
	 */
	stopObserving: function(eventName, handler) {
		if(this.options.container) {
			this.options.container.stopObserving(eventName, handler);
		} else {
			$(document).stopObserving(eventName, handler);
		}
		return this;
	},
	/**
	 * Fires an activator event.
	 * @private
	 */
	fire: function(eventName, element, event) {
		var memo = {
			activator: this,
			element: element,
			event: event
		}
		if(this.options.container) {
			this.options.container.fire(eventName, memo);
		} else {
			$(document).fire(eventName, memo);
		}
		return this;
	},
	/**
	 * Gets an element from the activator by index or id.
	 * @param {number|string} element The index or id of the element to get.
	 * @returns {element} The element if found, null otherwise.
	 */
	getElement: function(element) {
		if(Object.isNumber(element)) {
			return this.elements[element];
		}
		return this.isElement(element);
	},
	/**
	 * Gets activator elements by index or CSS selector.
	 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to get.
	 * @returns {Array} The array elements found.
	 */
	getElements: function(elements) {
		if(elements === null || elements === "*") return this.getElements(this.elements);
		if(Object.isNumber(elements)) return [ this.elements[elements] ];
		if(Object.isString(elements)) return this.getElements($$(elements));
		if(Object.isArray(elements)) {
			var es = [];
			elements.each(function(a) {
				var e = this.getElement(a);
				if(e) es.push(e);
			}.bind(this));
			return es;
		}
		if(this.isElement(elements)) return [ $(elements) ];
		return [];
	},
	/**
	 * Determines if an element or one of it's parent elements is member of the activator's list of elements.
	 * @param {element} element The element to test.
	 * @returns {element} The element or parent element that is a member of the activator's list of elements if found, null otherwise.
	 */
	isElement: function(element) {
		if(!element || element == this.options.container || element == document.body) {
			return null;
		}
		element = $(element);
		var e = this.elements.find(function(e) {
			return e == element;
		});
		if(!e) {
			if(!element.up) return null;
			return this.isElement(element.up());
		}
		return e;
	},
	/**
	 * (De)Selects one or more elements (and deselects all other elements if the singleSelect option is true).
	 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to select.
	 * @param {bool} [selected] If true, the elements are selected; they are deselected otherwise. (default: true)
	 */
	setSelected: function(elements, selected) {
		var se = (Object.isUndefined(selected) || selected);
		var fn = se ? Element.addClassName : Element.removeClassName;
		if(se && this.options.singleSelect) {
			this.selectAll(false);
		}
		this.getElements(elements).each(function(e) {
			fn(e, this.options.classNames.selected);
			e[Widget.Activator.selectedAttribute] = se;
		}, this);
	},
	/**
	 * Determines if an element is selected.
	 * @param {number|string} element The index or id of the element to get.
	 * @returns {bool} True if the element is selected, false otherwise.
	 */
	isSelected: function(element) {
		return this.getElement(element)[Widget.Activator.selectedAttribute];
	},
	/**
	 * Gets all selected elements.
	 * @returns {Array|element} An array of selected elements if the singleSelect option is false. If the singleSelect option is true, returns the selected element if any, null otherwise.
	 */
	getSelected: function() {
		var es = [];
		this.elements.each(function(e) {
			if(e[Widget.Activator.selectedAttribute]) es.push(e);
		});
		if(this.options.singleSelect) {
			return es.length > 0 ? es[0] : null;
		}
		return es;
	},
	/**
	 * (Dis)Enables one or more elements.
	 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to enable.
	 * @param {bool} [enabled] If true, the elements are enabled; they are disabled otherwise. (default: true)
	 */
	setEnabled: function(elements, enabled) {
		var en = (Object.isUndefined(enabled) || enabled);
		var fn = en ? Element.removeClassName : Element.addClassName;
		this.getElements(elements).each(function(e) {
			fn(e, this.options.classNames.disabled);
			e[Widget.Activator.disabledAttribute] = !en;
			if(!en) {
				e.removeClassName(this.options.classNames.hover);
				e.removeClassName(this.options.classNames.active);
			}
		}.bind(this));
	},
	/**
	 * Determines if an element is enabled.
	 * @param {number|string} element The index or id of the element to get.
	 * @returns {bool} True if the element is enabled, false otherwise.
	 */
	isEnabled: function(element) {
		return !this.getElement(element)[Widget.Activator.disabledAttribute];
	},
	/**
	 * Gets all disabled elements.
	 * @returns {Array} An array of disabled elements.
	 */
	getDisabled: function() {
		var es = [];
		this.elements.each(function(e) {
			if(e[Widget.Activator.disabledAttribute]) es.push(e);
		});
		return es;
	},
	/**
	 * (De)Selects all elements.
	 * @param {bool} [selected] If true, the elements are selected; they are deselected otherwise. (default: true)
	 */
	selectAll: function(selected) {
		var se = (Object.isUndefined(selected) || selected);
		this.elements.each(function(e) {
			this.setSelected(e, se);
		}.bind(this));
	},
	/**
	 * (Dis)Enables all elements.
	 * @param {bool} [enabled] If true, the elements are enabled; they are disabled otherwise. (default: true)
	 */
	enableAll: function(enabled) {
		var en = (Object.isUndefined(enabled) || enabled);
		this.elements.each(function(e) {
			this.setEnabled(e, en);
		}.bind(this));
	},
	/**
	 * Destroys the activator. Removes all event handlers and extended methods.
	 */
	destroy: function() {
		this.remove(this.elements);
		this.handlers.each(function(h) {
			this.stopObserving(h.eventName, h.handler);
		}.bind(this));
		if(this.options.container) {
			this.options.container.stopObserving("mouseover", this.mouseoverListener);
			this.options.container.stopObserving("mouseout", this.mouseoutListener);
			this.options.container.stopObserving("mousedown", this.mousedownListener);
			this.options.container.stopObserving("mouseup", this.mouseupListener);
			this.options.container.stopObserving("click", this.clickListener);
		}
		this.handlers.length = 0;
		this.elements.length = 0;
	}
});
/**
 * Constants
 */
Object.extend(Widget.Activator, {
	/**
	 * Template for error message displayed when an element is not a child of the container.
	 */
	notContainerChild: "Element #{element} is not a child of the container #{container}.",
	/**
	 * Name of element attribute for the activator.
	 */
	activatorAttribute: "activator",
	/**
	 * Name of element attribute for selected.
	 */
	selectedAttribute: "activatorSelected",
	/**
	 * Name of element attribute for disabled.
	 */
	disabledAttribute: "activatorDisabled",
	/**
	 * Name of element attribute for getActivator function if elements are extended.
	 */
	getActivatorFunction: "getActivator",
	/**
	 * Name of element attribute for setSelected function if elements are extended.
	 */
	setSelectedFunction: "setSelected",
	/**
	 * Name of element attribute for isSelected function if elements are extended.
	 */
	isSelectedFunction: "isSelected",
	/**
	 * Name of element attribute for setEnabled function if elements are extended.
	 */
	setEnabledFunction: "setEnabled",
	/**
	 * Name of element attribute for isEnabled function if elements are extended.
	 */
	isEnabledFunction: "isEnabled",
	/**
	 * Name of mouseover event fired by the activator.
	 */
	mouseoverEvent: "activator:mouseover",
	/**
	 * Name of mouseout event fired by the activator.
	 */
	mouseoutEvent: "activator:mouseout",
	/**
	 * Name of mousedown event fired by the activator.
	 */
	mousedownEvent: "activator:mousedown",
	/**
	 * Name of mouseup event fired by the activator.
	 */
	mouseupEvent: "activator:mouseup",
	/**
	 * Name of click event fired by the activator.
	 */
	clickEvent: "activator:click"
});/**
 * @fileoverview docker.js
 * Widget.Docker controls element docking.<br />
 * Requires Prototype (http://www.prototypejs.org) 1.6 or later<br /><br />
 * by Marc Heiligers (marc@eternal.co.za) http://www.eternal.co.za<br /><br />
 * You may use this class in any way you like, but don't blame me if things go <br />
 * pear shaped. If you use this library I'd like a mention on your website, but <br />
 * it's not required. If you like it, send me an email. If you find bugs, send <br />
 * me an email. If you don't like it, don't tell me: you'll hurt my feelings. <br /><br />
 * Change History:<br />
 * Version 0.0.2: 21 Jul 2008<br />
 * - Added destroy method. NOTE: destroy does not put elements back where it found them, nor does it restore position style
 * Version 0.0.2: 15 Jul 2008<br />
 * - Event "layout:performed" renamed to "docker:resized" and available through the options object: docker.options.events.resized
 * - Added "docker:resizing" event (docker.options.events.resizing) which fires before resizing
 * - Added observe, stopObserving and fire curried methods
 * Version 0.0.1: Sometime beginning 2008<br />
 * - Initial version
 */

var Widget = window.Widget || {};
//TODO: optimize performLayout
//DONE: ensure no negative sizes are set (breaks ie)
//DONE: add check to only dock visible elements
//TODO: add destroy()
//TODO: reset removed elements on apply()
//HACK: there seems to be a bug in prototype 1.6.1.rc3
document.viewport.getDimensions = function() {
	var d = document.documentElement ? document.documentElement : document.body;
	return { width: d.clientWidth, height: d.clientHeight };
}
Widget.Docker = Class.create({
	initialize: function(element, options) {
		if(!element) {
			this.element = $(document.body);
			this.getDimensions = document.viewport.getDimensions;
		} else {
			this.element = $(element);
			this.getDimensions = this.element.getDimensions.bind(this.element);
		}

		var events = Object.extend({
			resizing: Widget.Docker.resizingEvent,
			resized: Widget.Docker.resizedEvent
		}, options ? options.events : {} || {});
		this.options = Object.extend({
			delay: 0.3
		}, options || {});
		this.options.events = events;

		this.resizeListener = this.resize.bind(this);
		Event.observe(window, "resize", this.resizeListener);

		///@deprecated
		this.performLayout = this.layout;

		this.observe = Element.observe.curry(this.element);
		this.stopObserving = Element.stopObserving.curry(this.element);
		this.fire = Element.fire.curry(this.element);

		this.layoutHandler = this.layout.bind(this);

		this.apply();
	},
	resize: function(e, delayed) {
		if(this.resizeTimeout) clearTimeout(this.resizeTimeout);
		this.resizeTimeout = this.layoutHandler.delay(this.options.delay, null, delayed);
	},
	apply: function() {
		/*$$("[class~=dock-top]", "[class~=dock-left]", "[class~=dock-right]", "[class~=dock-bottom]", "[class~=dock-fill]").each(function(c) {
			if(!c._oldPosition) c._oldPosition = c.getStyle("position");
			c.setStyle({ position: "absolute" });
		});*/
		this.layout();
	},
	layout: function(element, delayed) {
		if(!element && !delayed) {
			this.fire(this.options.events.resizing, this);
		}

		var e, d, x, y, w, h, f, con = [], me = this;
		if(!element) {
			e = this.element;
			d = this.getDimensions();
		} else {
			e = element;
			d = e.getDimensions();
		}
		x = this.padding(e, "left");// + this.border(e, "left");
		y = this.padding(e, "top");// + this.border(e, "top");
		w = d.width - this.padding(e, "left") - this.padding(e, "right") - this.border(e, "left") - this.border(e, "right");
		h = d.height - this.padding(e, "top") - this.padding(e, "bottom") - this.border(e, "top") - this.border(e, "bottom");

		e.childElements().each(function(c) {
			if(!c.visible() || c.hasClassName("dock-parent") || c.getStyle("display") == "none") {
				return;
			}
			if(!c._oldPosition) {
				c._oldPosition = c.getStyle("position");
			}
			if(c.hasClassName("dock-container")) {
				con.push(c);
			}
			if(c.hasClassName("dock-fill")) {
				f = c;
			} else if(c.hasClassName("dock-top")) {
				c.setStyle({
					position: "absolute",
					top: me.setSize(y),
					left: me.setSize(x),
					width: me.setSize(w - me.margin(c, "left") - me.margin(c, "right") - me.border(c, "left") - me.border(c, "right") - me.padding(c, "left") - me.padding(c, "right"))
				});
				y += me.margin(c, "top") + me.margin(c, "bottom") + c.getHeight();
				h -= me.margin(c, "top") + me.margin(c, "bottom") + c.getHeight();
			} else if(c.hasClassName("dock-left")) {
				c.setStyle({
					position: "absolute",
					top: me.setSize(y),
					left: me.setSize(x),
					height: me.setSize(h - me.margin(c, "top") - me.margin(c, "bottom") - me.border(c, "top") - me.border(c, "bottom") - me.padding(c, "top") - me.padding(c, "bottom"))
				});
				x += me.margin(c, "left") + me.margin(c, "right") + c.getWidth();
				w -= me.margin(c, "left") + me.margin(c, "right") + c.getWidth();
			} else if(c.hasClassName("dock-right")) {
				c.setStyle({
					position: "absolute",
					top: me.setSize(y),
					left: me.setSize(x + w - c.getWidth() - me.margin(c, "left") - me.margin(c, "right")),
					height: me.setSize(h - me.margin(c, "top") - me.margin(c, "bottom") - me.border(c, "top") - me.border(c, "bottom") - me.padding(c, "top") - me.padding(c, "bottom"))
				});
				w -= me.margin(c, "left") + me.margin(c, "right") + c.getWidth();
			} else if(c.hasClassName("dock-bottom")) {
				c.setStyle({
					position: "absolute",
					top: me.setSize(y + h - c.getHeight() - me.margin(c, "top") - me.margin(c, "bottom")),
					left: me.setSize(x),
					width: me.setSize(w - me.margin(c, "left") - me.margin(c, "right") - me.border(c, "left") - me.border(c, "right") - me.padding(c, "left") - me.padding(c, "right"))
				});
				h -= me.margin(c, "top") + me.margin(c, "bottom") + c.getHeight();
			}
		});
		if(f) {
			f.setStyle({
				position: "absolute",
				top: me.setSize(y),
				left: me.setSize(x),
				height: me.setSize(h - me.margin(f, "top") - me.margin(f, "bottom") - me.border(f, "top") - me.border(f, "bottom") - me.padding(f, "top") - me.padding(f, "bottom")),
				width: me.setSize(w - me.margin(f, "left") - me.margin(f, "right") - me.border(f, "left") - me.border(f, "right") - me.padding(f, "left") - me.padding(f, "right"))
			});
		}
		con.each(function(c) {
			me.performLayout(c);
		});
		if(!element && !delayed) {
			this.resizeTimeout = null;
			this.resize(null, true);
		} else if(!element && delayed) {
			this.fire(this.options.events.resized, this);
		}
	},
	//-- Private functions -----------------------------------------------------
	//borrowed from DockManager
	fullPadding: function(e, s) {
		return this.padding(e, s) + this.border(e, s) + this.margin(e, s);
	},
	border: function(e, s) {
		var border = parseInt(e.getStyle("border-" + s + "-width") || 0);
		if(isNaN(border)) border = 0;
		return border;
	},
	padding: function(e, s) {
		var padding = parseInt(e.getStyle("padding-" + s) || 0);
		if(isNaN(padding)) padding = 0;
		return padding;
	},
	margin: function(e, s) {
		var margin = parseInt(e.getStyle("margin-" + s) || 0);
		if(isNaN(margin)) margin = 0;
		return margin;
	},
	setSize: function(s) {
		if(s < 0 || isNaN(s)) s = 0;
		return s + "px"
	},
	destroy: function() {
		this.stopObserving();
		Event.stopObserving(window, "resize", this.resizeListener);
	}
});
Object.extend(Widget.Docker, {
	resizingEvent: "docker:resizing",
	resizedEvent: "docker:resized"
});
///@deprecated
Widget.DockManager = Widget.Docker;/**
 * @fileoverview Widget.DatePicker <br />
 * � 2008 E-Technik<br />
 * <br />
 * Requires Prototype  1.6 (http://www.prototypejs.org) or later <br />
 * and Scriptaculous 1.8 (http://script.aculo.us) or later. <br />
 * <br />
 * Version 0.0.1: 5 Aug 2008<br />
 * - First version after old widgets<br />
 * @class Widget.DatePicker
 * @version 0.0.1
 * @author Marc Heiligers marc@eternal.co.za http://www.eternal.co.za
 */
Widget = window.Widget || {};
/**
 * The Widget.DatePicker class constructor. <br />
 * @constructor Widget.DatePicker
 * @param {string|element} element The id of or actual element to be converted to a date picker
 * @param {object} [options] An object of options.
 */
Widget.DatePicker = Class.create(/** @scope Widget.DatePicker **/{
	initialize: function(element, options) {
		this.element = $(element);
		var id = this.id = this.element.identify();

		//Is the element a text field? Get the date
		this.date = new Date(this.element.value);

		var classNames = Object.extend({
			container: "datepicker"
		}, options ? options.classNames || {} : {});

		var symbols = Object.extend({
			prev: "&laquo;",
			next: "&raquo;"
		}, options ? options.symbols || {} : {});

		/**
		 * The default options object.
		 * @class
		 * @param {string} [id] The id used as queue scope. (default: img.id)
		 */
		this.options = Object.extend({
			date: null,
			min: new Date("1 Jan 1970"),
			max: new Date("1 Jan 2050")
		}, options || {});
		this.options.classNames = classNames;
		this.options.symbols = symbols;
		this.date = this.options.date || this.date;
		if(!this.date || !this.date.addDays || this.date.format().indexOf("NaN") > -1) {
			this.date = new Date();
		}

		//Create a div for the popup and then the popup
		var div = this.div = new Element("div", {
			id: id + "_datepicker",
			className: this.options.classNames.container
		});
		div.update(Widget.DatePicker.calendarTemplate.evaluate({
			prevsym: this.options.symbols.prev,
			nextsym: this.options.symbols.next,
			mthid: this.id + "_month",
			yrid: this.id + "_year",
			containerid: this.id + "_calendar"
		}));
		var popup = this.popup = new Widget.Popup(div, {
			anchor: {
				element: this.element,
				show: "focus",
				horizontal: "right"
			},
			hide: {
				on: false
			}
		});
		for(var i = this.options.min.getFullYear(), l = this.options.max.getFullYear() + 1; i < l; ++i) {
			$(this.id + "_year").options.add(new Option(i, i));
		}
		for(var i = 0; i < 12; ++i) {
			$(this.id + "_month").options.add(new Option(Date.MonthNames[i + 12], i));
		}
		$(this.id + "_year").value = (this.options.date || new Date()).getFullYear();
		$(this.id + "_month").value = (this.options.date || new Date()).getMonth();
		$(this.id + "_year").observe("click", this.periodChanged.bind(this));
		$(this.id + "_month").observe("click", this.periodChanged.bind(this));
		//Create the activator for highlighting and selection
		var activator = this.activator = new Widget.Activator(".clickable", {
			container: div
		});
		activator.observe(activator.options.events.click, this.click.bind(this));

		//Setup the events
		this.showHandler = this.show.bind(this);
		popup.observe(popup.options.events.showing, this.showHandler);
		this.blurHandler = this.blur.bind(this);
		this.element.observe("blur", this.blurHandler);
	},
	render: function(d) {
		this.activator.remove("*");

		this.month = d;
		this.updating = true;
		$(this.id + "_year").value = d.getFullYear();
		$(this.id + "_month").value = d.getMonth();
		this.updating = false;

		var html = [];
		var t = Widget.DatePicker.dayNameTemplate;
		html.push("<table width='100%'><tr>");
		for(var i = 0; i < 7; ++i) {
			html.push(t.evaluate({ date: Date.DayNames[i + 14], cls: "dayName" }));
		}
		html.push("</tr>");

		t = Widget.DatePicker.dayTemplate;
		var today = new Date().getDateOnly().getTime();
		var sel = this.date ? this.date.getTime() : 0;
		var day = d.getMonthOf().start.getWeekOf().start;
		this.start = day;
		var cls;
		for(var i = 0; i < 42; ++i) {
			if(!(i % 7)) {
				html.push("<tr>");
			}
			cls = "day";
			if(day.getMonth() != d.getMonth()) {
				cls += " outside";
			}
			if(day.getTime() == today) {
				cls += " today";
			}
			if(day.getTime() == sel) {
				cls += " selected";
			}
			html.push(t.evaluate({ date: day.getDate(), i: i, cls: cls }));
			if(i % 7 == 6) {
				html.push("</tr>");
			}
			day = day.addDays(1);
		}
		html.push("</table>");

		$(this.id + "_calendar").update(html.join(""));

		this.activator.apply();
	},
	set: function(date) {
		this.date = date;
		if(date) {
			this.element.value = date.format();
		}
	},
	show: function() {
		var d = this.date;
		if(!d) {
			d = new Date();
		}
		this.render(d);
	},
	hide: function() {
		if(this.hideTimeout) {
			clearTimeout(this.hideTimeout);
			this.hideTimeout = null;
		}
		this.popup.hide();
	},
	periodChanged: function(e) {
		if(this.hideTimeout) {
			clearTimeout(this.hideTimeout);
			this.hideTimeout = null;
		}
		if(this.updating) return;
		var d = this.render(new Date($F(this.id + "_year"), $F(this.id + "_month"), 1));
	},
	click: function(e) {
		if(this.hideTimeout) {
			clearTimeout(this.hideTimeout);
			this.hideTimeout = null;
		}

		e.stop();
		var cls = $w(e.memo.element.className).find(function(cls) {
			return cls.startsWith("date-");
		});
		if(!cls) return;
		var act = cls.split("-")[1];
		switch(act) {
			case "prev":
				this.render(this.month.getPrevMonthOf().start);
				break;
			case "next":
				this.render(this.month.getNextMonthOf().start);
				break;
			case "today":
				this.popup.hide();
				this.date = new Date().getDateOnly();
				this.element.value = this.date.format();
				break;
			case "none":
				this.popup.hide();
				this.date = null;
				this.element.value = "";
				break;
			case "close":
				this.popup.hide();
				break;
			default:
				this.popup.hide();
				this.date = this.start.addDays(parseInt(act));
				this.element.value = this.date.format();
				break;
		}
	},
	blur: function() {
		this.hideTimeout = this.hide.bind(this).delay(0.2);
	},
	destroy: function() {
		this.activator.destroy();
		this.popup.destroy();
		this.div.remove();
		this.element.stopObserving("blur", this.blurHandler);
	}

});

Object.extend(Widget.DatePicker, {
	dayTemplate: new Template("<td class='clickable date-#{i} #{cls}'>#{date}</td>"),
	dayNameTemplate: new Template("<td class='#{cls}'>#{date}</td>"),
	calendarTemplate: new Template(
		"<table width='100%'><tr>" +
			"<td class='clickable date-prev'>#{prevsym}</td>" +
			"<td class='' colspan='3'><select id='#{mthid}'></select></td>" +
			"<td class='' colspan='2'><select id='#{yrid}'></sleect></td>" +
			"<td class='clickable date-next'>#{nextsym}</td>" +
		"</tr></table>" +
		"<div id='#{containerid}'></div>" +
		"<table width='100%'><tr>" +
			"<td class='clickable date-today'>Today</td>" +
			"<td class='clickable date-none'>None</td>" +
			"<td class='clickable date-close'>Close</td>" +
		"</tr></table>"
	)
});if(typeof Widget == "undefined") Widget = {};
//TODO: Resize
//TODO: Fire events
//TODO: Docs
//TODO: Add get method
//TODO: Allow add/remove of panels?
//TODO: Add destroy method
//TODO: Allow specification of panels and buttons within element
//TODO: Redirect methods (setSelected, etc)
Widget.Accordion = Class.create({
	initialize: function(element, options) {
		this.element = $(element);

		var classNames =  Object.extend({
			accordion: "accordion",
			panel: "panel",
			button: "button",
			hover: "hover",
			active: "active",
			selected: "selected",
			disabled: "disabled"
		}, options ? options.classNames || {} : {});

		var events = Object.extend({
			changing: Widget.Accordion.changingEvent,
			changed: Widget.Accordion.changedEvent
		}, options ? options.events || {} : {});

		this.options = Object.extend({
			animationDuration: 0.2,
			vertical: true,
			changeEvent: "click",
			panelSelector: "div[title]",
			buttonSelector: "div.button",
			noCreate: false
		}, options || {});
		this.options.classNames = classNames;
		this.options.events = events;

		var activator = this.activator = new Widget.Activator(null, {
			classNames: classNames,
			events: events,
			container: this.element,
			singleSelect: true,
			extendElements: true
		});
		activator.observe(activator.options.events[this.options.changeEvent], this.buttonClick.bindAsEventListener(this));

		var panels = this.panels = [];
		var buttons = this.element.select(this.options.buttonSelector);
		this.element.select(this.options.panelSelector).each(function(panel, i) {
			panel.hide();
			panels.push(panel);
			panel.addClassName(this.options.classNames.panel);

			var button;
			if(!this.options.noCreate) {
				var title = panel.getAttribute("title");
				var button = new Element("div", {
					className: this.options.classNames.button,
					title: title
				}).update(title);
				panel.setAttribute("title", "");
				panel.insert({ before: button });
			} else {
				button = buttons[i];
				button.addClassName(this.options.classNames.button);
			}
			if(!this.options.vertical) {
				button.setStyle({ "float": "left" });
				panel.setStyle({ "float": "left" });
			}
			button.panel = panel;
			activator.add(button);
		}, this);

		//Activator methods
		this.setSelected = activator.setSelected.bind(activator);
		this.setEnabled = activator.setEnabled.bind(activator);
		this.enableAll = activator.enableAll.bind(activator);
		this.getSelected = activator.getSelected.bind(activator);
		this.isSelected = activator.isSelected.bind(activator);

		//Event methods
		this.observe = Element.observe.curry(this.element);
		this.stopObserving = Element.stopObserving.curry(this.element);
		this.fire = Element.fire.curry(this.element);

		this.animating = false;

		this.show(this.panels[0]);
	},
	buttonClick: function(event) {
		if(!event.memo.element.panel || !this.activator.isElement(event.memo.element)) return;
		this.change(this.currentPanel, event.memo.element.panel);
	},
	show: function(panel) {
		if(this.animating || this.currentPanel == panel) return;
		this.currentPanel = panel;
		panel.show();
		if(this.options.vertical) {
			var h = Math.max(this.calcHeight() - this.border(panel, "top") - this.border(panel, "bottom") - this.padding(panel, "top") - this.padding(panel, "bottom"), 0);
			if(window.Effect) {
				new Effect.Scale(panel, 0, {
					duration: this.options.animationDuration,
					scaleX: false,
					scaleY: true,
					scaleContent: false,
					scaleFrom: 0,
					scaleTo: 100,
					scaleMode: {
						originalHeight: h
					}
				});
			} else {
				panel.setStyle({ height: h + "px" });
			}
		} else {
			var w = Math.max(this.calcWidth() - this.border(panel, "left") - this.border(panel, "right") - this.padding(panel, "left") - this.padding(panel, "right"), 0);
			if(window.Effect) {
				new Effect.Scale(panel, 0, {
					duration: this.options.animationDuration,
					scaleX: true,
					scaleY: false,
					scaleContent: false,
					scaleFrom: 0,
					scaleTo: 100,
					scaleMode: {
						originalWidth: w
					}
				});
			} else {
				panel.setStyle({ width: w + "px" });
			}
		}

		panel.previous().setSelected(true);
	},
	hide: function() {
		if(this.animating) return;
		var panel = this.currentPanel;
		this.currentPanel = null;
		if(this.options.vertical) {
			var h = Math.max(this.calcHeight() - this.border(panel, "top") - this.border(panel, "bottom") - this.padding(panel, "top") - this.padding(panel, "bottom"), 0);
			if(window.Effect) {
				new Effect.Scale(panel, 0, {
					duration: this.options.animationDuration,
					scaleX: false,
					scaleY: true,
					scaleContent: false,
					scaleFrom: 100,
					scaleTo: 0,
					scaleMode: {
						originalHeight: h
					},
					afterFinish: function() {
						panel.hide();
					}
				});
			} else {
				panel.hide();
			}
		} else {
			var w = Math.max(this.calcWidth() - this.border(panel, "left") - this.border(panel, "right") - this.padding(panel, "left") - this.padding(panel, "right"), 0);
			if(window.Effect) {
				new Effect.Scale(panel, 0, {
					duration: this.options.animationDuration,
					scaleX: true,
					scaleY: false,
					scaleContent: false,
					scaleFrom: 100,
					scaleTo: 0,
					scaleMode: {
						originalWidth: w
					},
					afterFinish: function() {
						panel.hide();
					}
				});
			} else {
				panel.hide();
			}
		}
		panel.previous().setSelected(false);
	},
	change: function(oldPanel, newPanel) {
		if(oldPanel == newPanel) return;
		if(this.effect) {
			this.effect.finish();
		}
		var e = this.fire(this.options.events.changing, this);
		if(e.cancelled) return;
		if(window.Effect) {
			if(this.options.vertical) {
				var hNew = Math.max(this.calcHeight() - this.border(newPanel, "top") - this.border(newPanel, "bottom") - this.padding(newPanel, "top") - this.padding(newPanel, "bottom"), 0);
				var hOld = Math.max(hNew - this.border(oldPanel, "top") - this.border(oldPanel, "bottom") - this.padding(oldPanel, "top") - this.padding(oldPanel, "bottom"), 0);
				newPanel.setStyle({ height: "0px" });
				oldPanel.setStyle({ height: hOld + "px" });
				newPanel.show();
				newPanel.previous().setSelected(true);
				new Effect.Parallel([
					new Effect.Scale(oldPanel, 0, {
						sync: true,
						scaleX: false,
						scaleContent: false,
						scaleFrom: 100,
						scaleTo: 0,
						scaleMode: {
							originalHeight: hOld
						}
					}),
					new Effect.Scale(newPanel, 100, {
						sync: true,
						scaleX: false,
						scaleContent: false,
						scaleFrom: 0,
						scaleTo: 100,
						scaleMode: {
							originalHeight: hNew
						}
					})
				], {
					duration: this.options.animationDuration,
					afterFinish: function() {
						oldPanel.hide();
						this.fire(this.options.events.changed, this);
					}.bind(this)
				});
			} else {
				var wNew = Math.max(this.calcWidth() - this.border(newPanel, "left") - this.border(newPanel, "right") - this.padding(newPanel, "left") - this.padding(newPanel, "right"), 0);
				var wOld = Math.max(wNew - this.border(oldPanel, "left") - this.border(oldPanel, "right") - this.padding(oldPanel, "left") - this.padding(oldPanel, "right"), 0);
				newPanel.setStyle({ width: "0px" });
				oldPanel.setStyle({ width: wOld + "px" });
				newPanel.show();
				newPanel.previous().setSelected(true);
				this.effect = new Effect.Parallel([
					new Effect.Scale(oldPanel, 0, {
						sync: true,
						scaleY: false,
						scaleContent: false,
						scaleFrom: 100,
						scaleTo: 0,
						scaleMode: {
							originalWidth: wOld
						}
					}),
					new Effect.Scale(newPanel, 100, {
						sync: true,
						scaleY: false,
						scaleContent: false,
						scaleFrom: 0,
						scaleTo: 100,
						scaleMode: {
							originalWidth: wNew
						}
					})
				], {
					duration: this.options.animationDuration,
					afterFinish: function() {
						oldPanel.hide();
						this.fire(this.options.events.changed, this);
						this.effect = null;
					}.bind(this)
				});
			}
		} else {
			this.hide(oldPanel);
			this.show(newPanel);
			this.element.fire(this.events.changed, this);
		}
		this.currentPanel = newPanel;
	},
	calcHeight: function() {
		var h = 0;
		this.panels.each(function(p) {
			var b = p.previous();
			if(b.visible()) {
				h += p.previous().getHeight();
			}
		});
		return this.element.getClientDimensions().height - h;
	},
	calcWidth: function() {
		var w = 0;
		this.panels.each(function(p) {
			var b = p.previous();
			if(b.visible()) {
				w += p.previous().getWidth();
			}
		});
		return this.element.getClientDimensions().width - w;
	},
	performLayout: function() {
		if(!this.currentPanel) return;
		if(this.options.vertical) {
			this.currentPanel.setStyle({
				height: Math.max(
					this.calcHeight() -
					this.border(this.currentPanel, "top") -
					this.border(this.currentPanel, "bottom") -
					this.padding(this.currentPanel, "top") -
					this.padding(this.currentPanel, "bottom"),
				0) + "px"
			});
		} else {
			this.currentPanel.setStyle({
				width: Math.max(
					this.calcWidth() -
					this.border(this.currentPanel, "left") -
					this.border(this.currentPanel, "right") -
					this.padding(this.currentPanel, "left") -
					this.padding(this.currentPanel, "right"),
				0) + "px"
			});
		}
	},
	//-- Private functions -----------------------------------------------------
	border: function(e, s) {
		var border = parseInt(e.getStyle("border-" + s + "-width") || 0);
		if(isNaN(border)) border = 0;
		return border;
	},
	padding: function(e, s) {
		var padding = parseInt(e.getStyle("padding-" + s) || 0);
		if(isNaN(padding)) padding = 0;
		return padding;
	}
});

Object.extend(Widget.Accordion, {
	clickEvent: "accordion:click",
	changingEvent: "accordian:changing",
	changedEvent: "accordian:changed"
});
/**
 * @fileoverview grid.js
 * Widget.Grid creates a grid control.<br />
 * Requires Prototype (http://www.prototypejs.org) 1.6 or later<br /><br />
 * by Marc Heiligers (marc@eternal.co.za) http://www.eternal.co.za<br /><br />
 * You may use this class in any way you like, but don't blame me if things go <br />
 * pear shaped. If you use this library I'd like a mention on your website, but <br />
 * it's not required. If you like it, send me an email. If you find bugs, send <br />
 * me an email. If you don't like it, don't tell me: you'll hurt my feelings. <br /><br />
 * Change History:<br />
 * Version 0.0.5: 17 Jun 2008<br />
 * - Fixed issues with reuse of styles created. Grid now checks if a rule exists before creating a new one.<br />
 * - Started adding resizing and resized events for columns and rows. Allowed plugins to veto sizing.<br />
 * Version 0.0.4: 14 Jun 2008<br />
 * - Fixed various issues with the RowAdjuster and ColumnSorter plugins<br />
 * - Fixed bug in setColumnStyle which calls ensureColumnExists and caused issues with renderColumns<br />
 *   (a similar issue is like to be found with setRowStyle, but has not been looked at yet)
 * Version 0.0.3: 11 Jun 2008<br />
 * - Separated events and classNames from options
 * - Created default event names object
 * Version 0.0.2: 9 Jun 2008<br />
 * - Added plugins<br />
 * - Added some of this documentation<br />
 * Version 0.0.1: Sometime beginning 2008<br />
 * - Initial version
 */


var Widget = window.Widget || {};
//CHECK: Fix bug in freeze Y
//TODO: Fix funny scrolling bug when clicking on bottom right corner of body panel
//DONE: Scrolling on headers
//TODO: Change all $R to for loops
Widget.Grid = Class.create({
	initialize: function(element, model, options, plugins) {
		this.element = $(element);
		this.model = model;
		this.plugins = plugins || [];

		var classNames = Object.extend({
			freezeTop: "freezeTop",
			freezeLeft: "freezeLeft",
			freezeStatic: "freezeStatic",
			freezeCell: "freeze",
			body: "body",
			outerCell: "cell",
			innerCell: "contents"
		}, options ? options.classNames : {} || {});

		var events = Object.extend({
			cellCreated: Widget.Grid.cellCreatedEvent,
			resizing: Widget.Grid.resizingEvent,
			resized: Widget.Grid.resizedEvent,
			columnResizing: Widget.Grid.columnResizing,
			columnResized: Widget.Grid.columnResized,
			rowResizing: Widget.Grid.rowResizing,
			rowResized: Widget.Grid.rowResized,
			refreshing: Widget.Grid.refreshingEvent,
			refreshed: Widget.Grid.refreshedEvent
		}, options ? options.events : {} || {});

		this.options = Object.extend({
			freeze: [0, 1],
			margins: [0, 0],
			wheel: 18,
			autoResize: false,
			renderLimit: Prototype.Browser.IE ? 10 : 50 //All other browsers are just that much faster than IE
		}, options || {});
		this.options.classNames = classNames;
		this.options.events = events;

		this.freezeTop = new Element("div", {
			className: this.options.classNames.freezeTop,
			style: "position: absolute; overflow: hidden;"
		});
		this.freezeLeft = new Element("div", {
			className: this.options.classNames.freezeLeft,
			style: "position: absolute; overflow: hidden;"
		});
		this.freezeStatic = new Element("div", {
			className: this.options.classNames.freezeStatic,
			style: "position: absolute; overflow: hidden;"
		});
		this.body = new Element("div", {
			className: this.options.classNames.body,
			style: "position: absolute; overflow: auto;"
		});
		this.element.insert(this.body);
		this.element.insert(this.freezeTop);
		this.element.insert(this.freezeLeft);
		this.element.insert(this.freezeStatic);

		this.styleSheet = Widget.Styles.addStyleSheet();

		this.templates = {
			colSel: new Template("##{id} .c#{c}"),
			colRule: new Template("left: #{x}px; width: #{w}px;"),
			rowSel: new Template("##{id} .r#{r}"),
			rowRule: new Template("top: #{y}px; height: #{h}px; position: absolute; overflow: hidden"),
			outerCellClass: new Template("r#{y} c#{x} " + this.options.classNames.outerCell),
			freezeCellClass: new Template("r#{y} c#{x} " + this.options.classNames.freezeCell),
			innerCell: new Template("<div class='" + this.options.classNames.innerCell + "'>#{c}</div>")
		}

		this.observe = Element.observe.curry(this.element);
		this.stopObserving = Element.stopObserving.curry(this.element);
		this.fire = Element.fire.curry(this.element);

		this.refresh("all", true);
		this.model.setGrid(this);
		this.render(this.element.getViewportDimensions());
		this.performLayout();
		this.resize();

		this.body.observe("scroll", this.scroll.bind(this));
		if(this.options.autoResize) {
			this.resizeListener = this.resize.bindAsEventListener(this);
			Event.observe(window, "resize", this.resizeListener);
		}
		this.freezeLeft.observe("mouse:wheel", this.wheelLeft.bindAsEventListener(this));
		this.freezeTop.observe("mouse:wheel", this.wheelTop.bindAsEventListener(this));

		plugins.invoke("setGrid", this, model);
	},
	//-- Render methods --------------------------------------------------------
	refresh: function(clear, noRender) {
		if(!clear) clear = "all";
		this.fire(this.options.events.refreshing, { grid: this, clear: clear, noRender: noRender });
		var top = this.body.scrollTop, left = this.body.scrollLeft;

		//TODO: destroy cells
		switch(clear) {
			case "all":
			case "cols":
			//TODO: separate rows and ensure that freeze rows are not destroyed
			case "rows":
				Widget.Styles.removeStyleSheet(this.styleSheet);
				this.styleSheet = Widget.Styles.addStyleSheet();
				this.freezeStatic.update("");
				this.freezeTop.update("");
				this.cols = { m: -1, "-1": { x: 0, w: 0, r: false } };
				this.rows = { m: -1, "-1": { y: 0, h: 0, r: false } };

				this.freezeTop.insert(new Element("div").setStyle({
					position: "absolute",
					overflow: "hidden",
					width: "1px",
					height: "1px",
					top: "65535px",
					left: "65535px"
				}));

				this.freezeLeft.update("");
				this.freezeLeft.insert(new Element("div").setStyle({
					position: "absolute",
					overflow: "hidden",
					width: "1px",
					height: "1px",
					top: "65535px",
					left: "65535px"
				}));
			case "body":
			default:
				this.body.update("");
				this.scrollMarker = new Element("div").setStyle({
					position: "absolute",
					overflow: "hidden",
					width: "1px",
					height: "1px"
				});
				this.body.insert(this.scrollMarker);
				if(this.rows.m) {
					var freeze = this.options.freeze;
					$H(this.rows).each(function(row) {
						if(row.key > freeze[1] - 1) {
							var c = [];
							$R(0, freeze[0] - 1).each(function(x) {
								c[x] = row.value.c[x];
							});
							row.value.c = c;
						}
					});
				}
		}

		if(!noRender) {
			this.prevViewport = null;
			this.render(this.element.getViewportDimensions());
			this.performLayout();
			this.body.scrollTop = top;
			this.body.scrollLeft = left;
		}

		this.fire(this.options.events.refreshed, { grid: this, clear: clear, noRender: noRender });
	},
	render: function(viewport) {
		clearTimeout(this.renderTimeout);
		this.renderTimeout = null;
		if(!viewport) viewport = this.body.getViewportDimensions();
		//if(Object.toJSON(this.prevViewport) == Object.toJSON(viewport)) return; //Removed for ColumnResizer
		this.prevViewport = viewport;
		var result = this.renderColumns(viewport);
		if(result.more) {
			this.scroll();
		} else {
			result = this.renderRows(viewport, result.cells);
			if(result.more) {
				this.scroll();
			} else {
				this.performLayout();
			}
		}
		this.fire(this.options.events.cellCreated, result.cells);
	},
	renderColumns: function(viewport) {
		var cell, d, pane, col, row, mx = this.model.getCols(), my = this.options.freeze[1], x, y, ar = [], uw = [];
		var cols = this.cols, rows = this.rows, dm = this.model;
		var fz0 = this.options.freeze[0], m0 = this.options.margins[0], m1 = this.options.margins[1];
		var id = this.element.identify();
		var colSel = this.templates.colSel, colRule = this.templates.colRule;
		var rowSel = this.templates.rowSel, rowRule = this.templates.rowRule;
		var ss = this.styleSheet;
		var count = 0, max = this.options.renderLimit, cells = [];
		for(x = 0; x < mx && count < max; ++x) {
			col = cols[x];
			if(!col || !col.r || cols.m < x) {
				if(x < fz0) {
					pane = this.freezeStatic;
				} else {
					pane = this.freezeTop;
				}
				if(!col) {
					col = cols[x] = {
						x: x == fz0 ? 0 : cols[x - 1].x + cols[x - 1].w + m0,
						w: dm.getWidth(x)
					};
					col.r = false; //Widget.Styles.getRule(colSel.evaluate({ id: id, c: x }), ss) != null;
				}
				//TODO: Check this. Done for column sorter. See next TODO.
				cols.m = x;
				if(col.x + col.w >= viewport.x) {
					if(col.x > viewport.right && x > fz0) {
						//TODO: See prev TODO
						//if(cols.m < x) cols.m = x;
						break;
					}
					if(!col.r) {
						ar.push({ selector: colSel.evaluate({ id: id, c: x }), rule: colRule.evaluate(col) });
						//Widget.Styles.addRule(colSel.evaluate({ id: id, c: x }), colRule.evaluate(col));
						col.r = true;
					} /*else {
						uw.push({ col: x, width: col.w });
					}*/
					for(y = 0; y < my && count < max; ++y) {
						row = rows[y];
						if(!row) {
							row = rows[y] = {
								y: rows[y - 1].y + rows[y - 1].h + m1,
								h: dm.getHeight(y),
								c: []
							};
							row.r = false; //Widget.Styles.getRule(rowSel.evaluate({ id: id, r: y }), ss) != null;
							if(!row.r) {
								ar.push({ selector: rowSel.evaluate({ id: id, r: y }), rule: rowRule.evaluate(row) });
								//Widget.Styles.addRule(rowSel.evaluate({ id: id, r: y }), rowRule.evaluate(row));
								row.r = true;
							}
						}
						cell = this.renderCell(x, y, pane);
						row.c[x] = cell;
						cells.push({ x: x, y: y, cell: cell });
						count++;
					}
				}
			}
		}
		Widget.Styles.addRules(ar, ss);
		//if(uw.length) this.setColumnWidth(uw);
		return { cells: cells, more: count == max };
	},
	renderRows: function(viewport, cells) {
		var cell, d, pane, row, col, mx = this.model.getCols(), my = this.model.getRows(), x, y;
		var cols = this.cols, rows = this.rows, dm = this.model;
		var fz0 = this.options.freeze[0], fz1 = this.options.freeze[1];
		var m0 = this.options.margins[0], m1 = this.options.margins[1];
		var rowSel = this.templates.rowSel, rowRule = this.templates.rowRule;
		var id = this.element.identify();
		var ss = this.styleSheet;
		var count = cells.length, max = this.options.renderLimit;
		for(y = fz1; y < my && count < max; ++y) {
			row = rows[y];
			if(!row) {
				row = rows[y] = {
					y: y == fz1 ? 0 : rows[y - 1].y + rows[y - 1].h + m1,
					h: dm.getHeight(y),
					r: false,
					c: []
				}
			}
			if(row.y + row.h >= viewport.y) {
				rows.m = y;
				if(row.y > viewport.bottom && y > fz1) {
					//if(rows.m < y) rows.m = y;
					break;
				}
				if(!row.r) {
					Widget.Styles.addRule(
						rowSel.evaluate({
							id: id,
							r: y
						}),
						rowRule.evaluate(row),
						ss
					);
				}
				row.r = true;
				for(x = 0; x < mx && count < max; ++x) {
					col = cols[x];
					if(x < fz0 || col.x + col.w >= viewport.x) {
						if(col.x > viewport.right) {
							break;
						}
						if(!row.c[x]) {
							if(x < fz0) {
								pane = this.freezeLeft;
							} else {
								pane = this.body;
							}
							cell = this.renderCell(x, y, pane);
							row.c[x] = cell;
							cells.push({ x: x, y: y, cell: cell });
							count++;
						}
					}
				}
			}
		}
		return { cells: cells, more: count == max };
	},
	renderCell: function(x, y, pane) {
		var cell = new Element("div", {
			className: x < this.options.freeze[0] || y < this.options.freeze[1] ?
				this.templates.freezeCellClass.evaluate({ y: y, x: x }) :
				this.templates.outerCellClass.evaluate({ y: y, x: x })
		}).update(this.templates.innerCell.evaluate({ c: this.model.getContent(x, y) }));
		pane.insert(cell);
		//this.fire(this.options.events.cellCreated, { x: x, y: y, cell: cell });
		return cell;
	},
	performLayout: function() {
		var left = this.cols[this.options.freeze[0] - 1].x + this.cols[this.options.freeze[0] - 1].w + this.options.margins[0];
		var top = this.rows[this.options.freeze[1] - 1].y + this.rows[this.options.freeze[1] - 1].h + this.options.margins[1];
		this.scrollMarker.setStyle({
			top: (this.model.getTotalHeight() + this.model.getRows() * this.options.margins[1] - top) + "px",
			left: (this.model.getTotalWidth() + this.model.getCols() * this.options.margins[0] - left) + "px"
		});
		var d = this.element.getClientDimensions();
		this.body.setStyle({
			top: top + "px",
			left: left + "px",
			width: Math.max(d.width - left, 0) + "px",
			height: Math.max(d.height - top, 0) + "px"
		});
		var i = this.body.getInnerMargins();
		this.freezeTop.setStyle({
			top: "0px",
			left: left + "px",
			width: Math.max(d.width - left - i.width, 0) + "px",
			height: top + "px"
		});
		this.freezeLeft.setStyle({
			top: top + "px",
			left: "0px",
			width: left + "px",
			height: Math.max(d.height - top - i.height, 0) + "px"
		});
		this.freezeStatic.setStyle({
			top: "0px",
			left: "0px",
			width: left + "px",
			height: top + "px"
		});
		try { //TODO: Figure out why IE fails on this sometimes
			this.freezeTop.scrollLeft = this.body.scrollLeft;
			this.freezeLeft.scrollTop = this.body.scrollTop;
		} catch(ex) {}
	},
	scroll: function() {
		if(this.renderTimeout) {
			clearTimeout(this.renderTimeout);
		}
		this.renderTimeout = this.render.bind(this).delay(0.1);
		this.freezeTop.scrollLeft = Math.max(this.body.scrollLeft, 0);
		this.freezeLeft.scrollTop = Math.max(this.body.scrollTop, 0);
	},
	resize: function(event, delayed) {
		this.performLayout();
		this.render();

		if(!delayed) {
			this.resize.bind(this).delay(0.5, null, true);
		} else {
			this.fire(this.options.events.resized, { grid: this });
		}
	},
	wheelLeft: function(event) {
		this.body.scrollTop -= event.memo.delta * this.options.wheel;
	},
	wheelTop: function(event) {
		this.body.scrollLeft -= event.memo.delta * this.options.wheel;
	},
	//-- Utility methods -------------------------------------------------------
	setColumnWidth: function(x, width) {
		var widths, w, cols = this.cols, m0 = this.options.margins[0], fz0 = this.options.freeze[0], i, col, l, e;
		var dm = this.model, id = this.element.identify();
		var colSel = this.templates.colSel, rules = [];

		if(!(x instanceof Array)) {
			widths = [ { col: x, width: width } ];
		} else {
			widths = x;
		}

		x = 65535;
		for(i = 0, l = widths.length; i < l; ++i) {
			w = widths[i];
			col = cols[w.col];
			if(!col) {
				col = this.ensureColumnExists(w.col);
			}

			//Allow listeners to veto the setting of this column width
			//e = this.fire(this.options.events.columnResizing, w);
			//if(e.stopped) continue;

			col.w = w.width;
			if(col.r) {
				rules.push({
					selector: colSel.evaluate({ id: id, c: w.col }),
					name: "width",
					value: w.width + "px"
				});
			}
			if(w.col < x) x = w.col;
		}

		var m = x < fz0 ? fz0 : dm.getCols();
		for(i = x + 1; i < m; ++i) {
			col = cols[i];
			if(!col) {
				cols[i] = {
					x: cols[i - 1].x + cols[i - 1].w + m0,
					w: dm.getWidth(i),
					r: false
				}
			} else {
				col.x = cols[i - 1].x + cols[i - 1].w + m0;
				if(col.r) {
					rules.push({
						selector: colSel.evaluate({ id: id, c: i }),
						name: "left",
						value: col.x + "px"
					});
				}
			}
		}
		if(rules.length) {
			Widget.Styles.updateRules(rules, this.styleSheet);
			/*this.render();
			this.performLayout();*/
			this.scroll();
		}
		return col;
	},
	setRowHeight: function(y, height) {
		var heights, h, rows = this.rows, m1 = this.options.margins[1], dm = this.model, id = this.element.identify(), i, row;
		var rowSel = this.templates.rowSel, rules = [];

		if(!(y instanceof Array)) {
			heights = [ { row: y, height: height }];
		} else {
			heights = y;
		}

		y = 65535;
		for(i = 0; i < heights.length; ++i) {
			h = heights[i];
			row = rows[h.row];
			if(!row) {
				row = this.ensureRowExists(h.row);
			}
			row.h = h.height;
			if(row.r) {
				rules.push({
					selector: rowSel.evaluate({ id: id, r: h.row }),
					name: "height",
					value: h.height + "px"
				});
			}
			if(h.row < y) y = h.row;
		}

		var m = dm.getRows();
		for(i = y + 1; i < m; ++i) {
			row = rows[i];
			if(!row) {
				rows[i] = {
					y: rows[i - 1].y + rows[i - 1].h + (rows[i - 1].h ? m1 : 0),
					h: dm.getHeight(i),
					r: false,
					c: []
				}
			} else {
				row.y = rows[i - 1].y + rows[i - 1].h + (rows[i - 1].h ? m1 : 0);
				if(row.r) {
					rules.push({
						selector: rowSel.evaluate({ id: id, r: i }),
						name: "top",
						value: row.y + "px"
					});
				}
			}
		}
		Widget.Styles.updateRules(rules, this.styleSheet);
		this.scroll();
		return row;
	},
	getCellPosition: function(cell) {
		cell = $(cell);
		var r, c, row, col;
		$w(cell.className).each(function(cls) {
			if(cls.startsWith("r") && !isNaN(r = parseInt(cls.substr(1)))) {
				row = r;
			}
			if(cls.startsWith("c") && !isNaN(c = parseInt(cls.substr(1)))) {
				col = c;
			}
		});
		return { row: row, col: col };
	},
	getCell: function(x, y) {
		return this.ensureCellExists(x, y);
	},
	setCell: function(x, y, value) {
		var cell = this.getCell(x, y);
		if(cell) {
			cell.down().update(value);
		}
	},
	ensureCellExists: function(x, y) {
		var row = this.rows[y], cell;
		if(row && (cell = row.c[x])) {
			return cell;
		}
		if(!row) {
			row = this.ensureRowExists(y);
		}
		this.ensureColumnExists(x);
		var pane = this.body;
		if(x < this.options.freeze[0] && y < this.options.freeze[1]) {
			pane = this.freezeStatic;
		} else if(x < this.options.freeze[0]) {
			pane = this.freezeLeft;
		} else if(y < this.options.freeze[1]) {
			pane = this.freezeTop;
		}
		var cell = row.c[x] = this.renderCell(x, y, pane);
		this.fire(this.options.events.cellCreated, [{ x: x, y: y, cell: cell }]);
		return cell;
	},
	ensureVisible: function(xOrCell, y) {
		if(!Object.isElement(xOrCell)) {
			var cell = this.ensureCellExists(xOrCell, y);
			this.scrollTo(cell);
			return cell;
		} else {
			this.scrollTo(xOrCell);
			return xOrCell;
		}
	},
	scrollTo: function(xOrCell, y, width, height) {
		var x = xOrCell, d, p;
		if(Object.isElement(x)) {
			d = x.getDimensions();
			p = x.positionedOffset();
			x = p.left;
			y = p.top;
			width = d.width;
			height = d.height;
		}
		p = this.body.getViewportDimensions();
		if(p.right < x + width) this.body.scrollLeft = x + width - p.width;
		if(p.x > x) this.body.scrollLeft = x;
		if(p.bottom < y + height) this.body.scrollTop = y + height - p.height;
		if(p.y > y) this.body.scrollTop = y;
		this.render();
	},
	ensureColumnExists: function(x) {
		var col, cols = this.cols, dm = this.model, i;
		if(cols.m > x) return cols[x];
		var fz0 = this.options.freeze[0], m0 = this.options.margins[0];
		for(i = cols.m; i <= x; ++i) {
			col = cols[i];
			if(!col) {
				col = cols[i] = {
					x: i == fz0 ? 0 : cols[i - 1].x + cols[i - 1].w + m0,
					w: dm.getWidth(i),
					r: false
				}
			}
		}
		if(!col.r) {
			Widget.Styles.addRule(
				this.templates.colSel.evaluate({
					id: this.element.identify(),
					c: x
				}),
				this.templates.colRule.evaluate(col),
				this.styleSheet
			);
			col.r = true;
		}
		return col;
	},
	ensureRowExists: function(y) {
		var row, rows = this.rows, rs = this.templates.rowSel, id = this.element.identify();
		var ss = this.styleSheet;
		if(rows.m > y) return rows[y];
		for(var i = rows.m; i <= y; ++i) {
			row = rows[i];
			if(!row) {
				row = this.rows[i] = {
					y: i == this.options.freeze[1] ? 0 : rows[i - 1].y + rows[i - 1].h + this.options.margins[1],
					h: this.model.getHeight(i),
					r: false, //Widget.Styles.getRule(rs.evaluate({ id: id, r: y }), ss) != null,
					c: []
				}
			}
		}
		if(!row.r) {
			Widget.Styles.addRule(rs.evaluate({ id: id, r: y }), this.templates.rowRule.evaluate(row), ss);
			row.r = true;
		}
		return row;
	},
	setColumnStyle: function(x, styles) {
		this.ensureColumnExists(x);
		var s = styles;
		if(!Object.isString(s)) {
			var s = "";
			$H(styles).each(function(style) {
				s += style.key.camelize() + ":" + style.value + ";";
			});
		}
		Widget.Styles.addRule(this.templates.colSel.evaluate({ id: this.element.identify(), c: x }), s, this.styleSheet);
		/*var sel = this.templates.colSel.evaluate({ id: this.element.identify(), c: x });
		var ss = this.styleSheet;
		$H(styles).each(function(style) {
			Widget.Styles.updateRule(sel, style.key, style.value, ss);
		});*/
	},
	setRowStyle: function(y, styles) {
		this.ensureRowExists(y);
		var s = styles;
		if(!Object.isString(s)) {
			var s = "";
			$H(styles).each(function(style) {
				s += style.key.camelize() + ":" + style.value + ";";
			});
		}
		Widget.Styles.addRule(this.templates.rowSel.evaluate({ id: this.element.identify(), r: y }), s, this.styleSheet);
		/*var sel = this.templates.rowSel.evaluate({ id: this.element.identify(), r: y });
		var ss = this.styleSheet;
		$H(styles).each(function(style) {
			Widget.Styles.updateRule(sel, style.key, style.value, ss);
		});*/
	},
	getCellFromEvent: function(e) {
		var cell = e.element();
		if(cell.hasClassName(this.options.classNames.outerCell) || cell.hasClassName(this.options.classNames.freezeCell)) {
			return cell;
		}
		cell = e.element().up("." + this.options.classNames.outerCell);
		if(!cell) {
			cell = e.element().up("." + this.options.classNames.freezeCell);
		}
		return cell;
	},
	destroy: function() {
		this.model.destroy();
		this.plugins.invoke("destroy");
		[ this.element, this.freezeTop, this.freezeLeft, this.freezeStatic, this.body ].invoke("stopObserving");
		if(this.options.autoResize) {
			Event.stopObserving(window, "resize", this.resizeListener);
		}
		this.element.update("");
		Widget.Styles.removeStyleSheet(this.styleSheet);
	}
});

Object.extend(Widget.Grid, {
	cellCreatedEvent: "grid:cellCreated",
	resizingEvent: "grid:resizing",
	resizedEvent: "grid:resized",
	columnResizingEvent: "grid:columnResizing",
	columnResizedEvent: "grid:columnResized",
	rowResizingEvent: "grid:rowResizing",
	rowResizedEvent: "grid:rowResized",
	refreshingEvent: "grid:refreshing",
	refreshedEvent: "grid:refreshed"
});

//-- Data Models ---------------------------------------------------------------

Widget.Grid.BaseModel = Class.create({
	initialize: function(data) {
		this.data = data;
		this.grid = null;

		this.rows = {};
		this.cols = {};
	},
	setGrid: function(grid) {
		this.grid = grid;
	},
	getRows: function() {
		return 0;
	},
	getCols: function() {
		return 0;
	},
	getWidth: function(x) {
		return this.cols[x] != null ? this.cols[x] : 100;
	},
	setWidth: function(x, w) {
		this.cols[x] = w;
	},
	getTotalWidth: function() {
		var w = 0;
		for(var i = 0, l = this.getCols(); i < l; ++i) {
			w += this.getWidth(i);
		}
		return w;
	},
	getHeight: function(y) {
		return this.rows[y] != null ? this.rows[y] : 24;
	},
	setHeight: function(y, h) {
		this.rows[y] = h;
	},
	getTotalHeight: function() {
		var h = 0;
		for(var i = 0, l = this.getRows(); i < l; ++i) {
			h += this.getHeight(i);
		}
		return h;
	},
	getContent: function(x, y) {
		return "";
	},
	getEditor: function(x, y) {
		return null;
	},
	setContent: function(x, y, value) {
		this.grid.setCell(x, y, value);
	},
	destroy: function() {
	}
});

Widget.Grid.ArrayModel = Class.create(Widget.Grid.BaseModel, {
	getRows: function() {
		return this.data.length;
	},
	getCols: function(x) {
		return this.data[0].length;
	},
	getContent: function(x, y) {
		return this.data[y][x];
	},
	setContent: function(x, y, value) {
		this.data[y][x] = value;
		this.grid.setCell(x, y, value);
	}
});

Widget.Grid.ListViewModel = Class.create(Widget.Grid.BaseModel, {
	initialize: function($super, data, headers, options) {
		$super(data);
		this.headers = headers;
		this.options = Object.extend({
			rowHeight: 24,
			colWidths: {},
			headerAlign: "center"
		}, options || {});
		this.cols = this.options.colWidths;
	},
	setGrid: function($super, grid) {
		$super(grid);
		grid.setRowStyle(0, { "text-align": this.options.headerAlign });
	},
	getRows: function() {
		return this.data.length + 1;
	},
	getCols: function() {
		return this.headers.length;
	},
	getHeight: function(y) {
		return this.rows[y] != null ? this.rows[y] : this.options.rowHeight;
	},
	getContent: function(x, y) {
		if(!y) {
			return this.headers[x];
		} else {
			return this.data[y - 1][x];
		}
	}
});

//TODO: Fix bug in column headers
Widget.Grid.SpreadsheetModel = Class.create(Widget.Grid.BaseModel, {
	initialize: function($super, rows, cols, data) {
		$super(data);
		this.rows = rows;
		this.cols = cols;
	},
	setGrid: function($super, grid) {
		$super(grid);
		grid.setRowStyle(0, { "text-align": "center" });
		grid.setColumnStyle(0, { "text-align": "center" });
	},
	getRows: function() {
		return this.rows + 1;
	},
	getCols: function(x) {
		return this.cols + 1;
	},
	getWidth: function(x) {
		return x == 0 ? 30 : 100;
	},
	getTotalWidth: function() {
		return this.cols * 100 + 30;
	},
	getTotalHeight: function() {
		return (this.rows + 1) * 20;
	},
	getContent: function(x, y) {
		if(x == 0 && y == 0) return "";
		if(y == 0) {
			var r = "", yy;
			while(x > 26) {
				xx = Math.floor(x / 26);
				r += String.fromCharCode(xx + 64);
				x -= xx * 26;
			}
			return r + String.fromCharCode(x + 64);
		}
		if(x == 0) return y - 1;
		var row = this.data[y - 1];
		if(!row) this.data[y - 1] = [];
		var col = this.data[y - 1][x - 1];
		return col ? col : "";
	},
	getEditor: function(x, y) {
		return new Widget.Grid.CellEditor.Text(this.getContent(x, y));
	},
	setContent: function(x, y, value) {
		this.data[y - 1][x - 1] = value;
		this.grid.setCell(x, y, value);
	}//,
	/*onCellCreated: function(event) {
		if(!event.memo.x || !event.memo.y) {
			event.memo.cell.setStyle({ textAlign: "center" });
		}
	}*/
});

//Requires date.js
//data is expected to be an array of contiguous days
Widget.Grid.CalendarModel = Class.create(Widget.Grid.BaseModel, {
	initialize: function($super, startDate, endDate, data, options) {
		$super(data);
		this.startDate = startDate;
		this.endDate = endDate;

		this.options = Object.extend({
			columnWidth: 200,
			rowHeight: 120,
			dayRowHeight: 20,
			weekColumnWidth: 40,
			dayFormat: "#{D}",
			monthFormat: "#{D} #{MMMM}",
			dateClassName: "date",
			dayClassName: "day",
			outsideClassName: "outside",
			pastClassName: "past"
		}, options || {});

		this.realStart = this.startDate.getWeekOf().start;
		this.realEnd = this.endDate.getWeekOf().end;
		this.days = this.realEnd.getUeDay() - this.realStart.getUeDay();

		this.diff = this.realStart.getUeDay() - this.startDate.getUeDay();

		this.rows = Math.ceil(this.days / 7);

		this.colWidths = [ this.options.weekColumnWidth ];
		for(var i = 1; i < 8; ++i) this.colWidths[i] = this.options.columnWidth;

		this.rowHeights = [ this.options.dayRowHeight ];
		for(var i = 1; i < this.rows + 1; ++i) this.rowHeights[i] = this.options.rowHeight;

		this.cellCreatedHandler = this.onCellCreated.bind(this);
	},
	setGrid: function($super, grid) {
		$super(grid);
		grid.observe(grid.options.events.cellCreated, this.cellCreatedHandler);
	},
	getRows: function() {
		return this.rows + 1;
	},
	getCols: function(x) {
		return 8;
	},
	getWidth: function(x) {
		return this.colWidths[x];
	},
	getHeight: function(y) {
		return this.rowHeights[y]
	},
	setWidth: function(x, w) {
		this.colWidths[x] = w;
	},
	setHeight: function(y, h) {
		this.rowHeights[y] = h
	},
	getTotalWidth: function() {
		var w = 0;
		for(var i = 0; i < 8; w += this.colWidths[i], ++i);
		return w;
	},
	getTotalHeight: function() {
		var h = 0;
		for(var i = 0, l = this.rows + 1; i < l; h += this.rowHeights[i], ++i);
		return h;
	},
	getContent: function(x, y) {
		if(x == 0 && y == 0) return "Week";
		if(y == 0) {
			return this.realStart.addDays(x - 1).format("#{DDDD}");
		}
		if(x == 0) {
			return this.realStart.addDays((y - 1) * 7).getWeek();
		}
		return this.data[(y - 1) * 7 + x - 1 + this.diff] || "";
	},
	setContent: function(x, y, value) {
		//TODO: setContent
		this.data[y - 1][x - 1] = value;
		this.grid.setCell(x, y, value);
	},
	onCellCreated: function(event) {
		event.memo.each(function(c) {
			if(!c.x || !c.y) {
				return;
			}
			c.cell.down().addClassName(this.options.dayClassName);
			var d = this.realStart.addDays((c.y - 1) * 7 + c.x - 1);
			var elm = new Element("div", {
				className: this.options.dateClassName
			}).update(d.getDate() == 1 ? d.format(this.options.monthFormat) : d.format(this.options.dayFormat));
			c.cell.insert(elm);
			if(d < this.startDate || d > this.endDate) {
				c.cell.addClassName(this.options.outsideClassName);
			}
			if(d < new Date().getDateOnly()) {
				c.cell.addClassName(this.options.pastClassName);
			}
		}, this);
	},
	getDateCellPosition: function(date) {
		var days = date.getUeDay() - this.realStart.getUeDay();
		var y = Math.floor(days / 7);
		var x = days - y * 7;
		return { row: y + 1, col: x + 1 };
	},
	destroy: function() {
		this.grid.stopObserving(this.grid.options.events.cellCreated, this.cellCreatedHandler);
	}
});

//-- Plugins -------------------------------------------------------------------
Widget.Grid.PluginBase = Class.create({
	setGrid: function(grid, model) {
		this.grid = grid;
		this.model = model;
	},
	destroy: function() {
	}
});

//TODO: Improve to only highlight cells in viewport
//TODO: Add only cell highlighting - highlight mode option to replace freezeOnly?
Widget.Grid.CellHighlighter = Class.create(Widget.Grid.PluginBase, {
	initialize: function(options) {
		var classNames = Object.extend({
			freeze: "highlight-freeze",
			body: "highlight-body",
			cell: "highlight-cell"
		}, options ? options.classNames : {} || {});

		var highlight = Object.extend({
			rows: true,
			columns: true,
			freezeOnly: true
		}, options ? options.classNames : {} || {});

		this.options = Object.extend({
		}, options || {});

		this.options.classNames = classNames;
		this.options.highlight = highlight;

		this.mouseoverListener = this.mouseover.bind(this);
		this.mousemoveListener = this.mousemove.bind(this);
		this.mouseoutListener = this.mouseout.bind(this);

		this.currentCell = null;
		this.currentPosition = null;
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		this.grid.observe("mouseover", this.mouseoverListener);
		this.grid.observe("mousemove", this.mousemoveListener);
		this.grid.observe("mouseout", this.mouseoutListener);
	},
	mouseover: function(event) {
		var cell;
		if(cell = this.grid.getCellFromEvent(event)) {
			this.highlight(cell);
		}
	},
	mousemove: function(event) {
		var cell;
		if(cell = this.grid.getCellFromEvent(event)) {
			this.highlight(cell);
		}
	},
	mouseout: function(event) {
		this.highlight(null);
	},
	highlight: function(cell) {
		if(this.currentCell == cell) return;
		var pos, c, m;

		//Remove highlighting
		if(this.currentCell) {
			pos = this.currentPosition;
			if(this.options.highlight.rows) {
				m = (this.options.highlight.freezeOnly ? this.grid.options.freeze[0] : this.grid.model.getCols()) - 1;
				$R(0, m).each(function(x) {
					if(c = this.grid.getCell(x, pos.row)) {
						c.removeClassName(
							x < this.grid.options.freeze[0] ? this.options.classNames.freeze : this.options.classNames.body
						);
					}
				}.bind(this));
			}
			if(this.options.highlight.columns) {
				m = (this.options.highlight.freezeOnly ? this.grid.options.freeze[1] : this.grid.model.getRows()) - 1;
				$R(0, m).each(function(y) {
					if(c = this.grid.getCell(pos.col, y)) {
						c.removeClassName(
							y < this.grid.options.freeze[1] ? this.options.classNames.freeze : this.options.classNames.body
						);
					}
				}.bind(this));
			}
			this.grid.getCell(pos.col, pos.row).removeClassName(this.options.classNames.cell);
			this.currentCell = null;
		}

		//Add highlighting
		if(cell) {
			//Element.extend(cell);
			pos = this.currentPosition = this.grid.getCellPosition(cell);
			if(this.options.highlight.rows) {
				m = (this.options.highlight.freezeOnly ? this.grid.options.freeze[0] : this.grid.model.getCols()) - 1;
				$R(0, m).each(function(x) {
					if(c = this.grid.getCell(x, pos.row)) {
						c.addClassName(
							x < this.grid.options.freeze[0] ? this.options.classNames.freeze : this.options.classNames.body
						);
					}
				}.bind(this));
			}
			if(this.options.highlight.columns) {
				m = (this.options.highlight.freezeOnly ? this.grid.options.freeze[1] : this.grid.model.getRows()) - 1;
				$R(0, m).each(function(y) {
					if(c = this.grid.getCell(pos.col, y)) {
						c.addClassName(
							y < this.grid.options.freeze[1] ? this.options.classNames.freeze : this.options.classNames.body
						);
					}
				}.bind(this));
			}
			this.grid.getCell(pos.col, pos.row).addClassName(this.options.classNames.cell);
			this.currentCell = cell;
		}
	},
	destroy: function() {
		if(this.grid) {
			this.grid.stopObserving("mouseover", this.mouseoverListener);
			this.grid.stopObserving("mousemove", this.mousemoveListener);
			this.grid.stopObserving("mouseout", this.mouseoutListener);
		}
		this.grid = null;
	}
});

//TODO: Multiple selections
//TODO: Pagedown/up, home, end
//TODO: Opera repeat key
//TODO: Safari keys and selection
//TODO: Get selection
//TODO: Figure out why sometimes on up or left the cell is not highlighted
//TODO: Freeze cell highlighting
//TODO: Key passing - to and from editor
//TODO: Allow all keys
//TODO: Add selection events: cell:selected, cell:deselected
//TODO: Add other events: cell:keypress, cell:click, cell:dblclick
//TODO: Starting Cell (starting selection)
Widget.Grid.CellSelector = Class.create(Widget.Grid.PluginBase, {
	initialize: function(options) {
		var classNames = Object.extend({
			current: "selected-current",
			other: "selected-other"
		}, options ? options.classNames : {} || {});

		this.options = Object.extend({
			multiple: false,
			startingCell: null,
			tabIndex: -1,
			hideGridFocus: true,
			limitFreeze: true,
			limits: [0, 0]
			/*editor: null,
			editKeys: [ Event.KEY_F2 ],
			editMouse: "double"*/
		}, options || {});

		this.options.classNames = classNames;

		this.keydownListener = this.keydown.bindAsEventListener(this);
		this.clickListener = this.click.bind(this);
		//this.dblclickListener = this.dblclick.bind(this);
		//this.grid.observe("dblclick", this.dblclickListener);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		this.grid.element.setAttribute("tabindex", this.options.tabIndex);
		if(this.options.hideGridFocus) {
			if(Prototype.Browser.IE) {
				this.grid.element.setAttribute("hidefocus", true);
			} else {
				Widget.Styles.addRule(
					"#" + this.grid.element.identify() + ":focus",
					"outline: none;",
					grid.styleSheet
				);
			}
		}

		if(this.options.limitFreeze) {
			if(this.options.limits[0] < this.grid.options.freeze[0]) {
				this.options.limits[0] = this.grid.options.freeze[0]
			}
			if(this.options.limits[1] < this.grid.options.freeze[1]) {
				this.options.limits[1] = this.grid.options.freeze[1]
			}
		}

		this.grid.observe("keydown", this.keydownListener);
		this.grid.observe("click", this.clickListener);


		this.selectedCells = [];
		this.selectedCell = this.options.startingCell;
		if(this.selectedCell) {
			this.selectedCells.push(this.selectedCell);
			this.highlight(this.selectedCell);
			this.grid.ensureVisible(this.selectedCell);
		}
	},
	select: function(cell) {
		if(cell != this.selectedCell) {
			this.unhighlight(this.selectedCell);
			this.selectedCell = cell;
			this.highlight(this.selectedCell);
		}
	},
	keydown: function(event) {
		if(this.editor) {
			return;
		}
		var handled = false;
		var key = event.which || event.keyCode;
		if(!this.selectedCell) {
			switch(key) {
				case Event.KEY_DOWN:
				case Event.KEY_RIGHT:
					this.selectedCell = this.grid.ensureVisible(this.options.limits[0], this.options.limits[1]);
					handled = true;
					break;
				case Event.KEY_UP:
					this.selectedCell = this.grid.ensureVisible(this.options.limits[0], this.grid.model.getRows() - 1);
					handled = true;
					break;
				case Event.KEY_LEFT:
					this.selectedCell = this.grid.ensureVisible(this.grid.model.getCols() - 1, this.options.limits[1]);
					handled = true;
					break;
			}
			if(this.selectedCell) {
				this.highlight(this.selectedCell);
			}
		} else {
			var pos = this.grid.getCellPosition(this.selectedCell);
			var oldCell = this.selectedCell;
			switch(key) {
				case Event.KEY_DOWN:
					if(pos.row < this.grid.model.getRows() - 1) {
						this.selectedCell = this.grid.ensureVisible(pos.col, pos.row + 1);
					}
					handled = true;
					break;
				case Event.KEY_UP:
					if(pos.row > this.options.limits[1]) {
						this.selectedCell = this.grid.ensureVisible(pos.col, pos.row - 1);
					}
					handled = true;
					break;
				case Event.KEY_RIGHT:
					if(pos.col < this.grid.model.getCols() - 1) {
						this.selectedCell = this.grid.ensureVisible(pos.col + 1, pos.row);
					}
					handled = true;
					break;
				case Event.KEY_LEFT:
					if(pos.col > this.options.limits[0]) {
						this.selectedCell = this.grid.ensureVisible(pos.col - 1, pos.row);
					}
					handled = true;
					break;
			}
			if(oldCell != this.selectedCell) {
				this.unhighlight(oldCell);
				this.highlight(this.selectedCell);
			}
			/*if(this.options.editor && this.options.editKeys.find(function(k) { return k == key; })) {
				this.editor = this.options.editor.editCell({
					grid: this.grid,
					cell: this.selectedCell,
					editComplete: this.editComplete.bind(this)
				});
				handled = true;
			}*/
		}
		if(handled) event.stop();
	},
	click: function(event) {
		var cell = this.grid.getCellFromEvent(event);
		if(!cell) return;
		var pos = this.grid.getCellPosition(cell);
		if(pos.col >= this.options.limits[0] && pos.row >= this.options.limits[1]) {
			this.unhighlight(this.selectedCell);
			this.selectedCell = this.grid.ensureVisible(cell);
			this.highlight(this.selectedCell);
			/*if(this.options.editor && this.options.editMouse == "single") {
				this.editor = this.options.editor.editCell({
					grid: this.grid,
					cell: this.selectedCell,
					editComplete: this.editComplete.bind(this)
				});
			}*/
		}
	},
	/*dblclick: function(event) {
		var cell = this.getCell(event);
		if(!cell) return;
		var pos = this.grid.getCellPosition(cell);
		if(pos.col >= this.options.limits[0] && pos.row >= this.options.limits[1]) {
			this.unhighlight(this.selectedCell);
			this.selectedCell = this.grid.ensureVisible(cell);
			this.highlight(this.selectedCell);
			if(this.options.editor && this.options.editMouse == "double") {
				this.editor = this.options.editor.editCell({
					grid: this.grid,
					cell: this.selectedCell,
					editComplete: this.editComplete.bind(this)
				});
			}
		}
	},*/
	highlight: function(cell) {
		if(cell) cell.addClassName(this.options.classNames.current);
	},
	unhighlight: function(cell) {
		if(cell) cell.removeClassName(this.options.classNames.current);
	},
	/*editComplete: function() {
		this.editor = null;
		//TODO: Don't steal the focus if it shouldn't be on the grid
		this.setFocus.bind(this).defer();
		this.setFocus.bind(this).delay(0.1);
	},
	setFocus: function() {
		this.grid.element.focus();
	},*/
	destroy: function() {
		if(this.grid) {
			this.grid.stopObserving("keydown", this.keydownListener);
			this.grid.stopObserving("click", this.clickListener);
		}
		this.grid = null;
	}
});

//TODO: Multiple selection
//TODO: deselection
Widget.Grid.RowSelector = Class.create(Widget.Grid.PluginBase, {
	initialize: function(options) {
		var classNames = Object.extend({
			current: "selected-current",
			other: "selected-other"
		}, options ? options.classNames : {} || {});

		var events = Object.extend({
			rowSelected: Widget.Grid.RowSelector.rowSelectedEvent,
			rowDblClick: Widget.Grid.RowSelector.rowDblClickEvent
		}, options ? options.events : {} || {});

		this.options = Object.extend({
			multiple: false,
			tabIndex: -1,
			hideGridFocus: true,
			limitFreeze: true,
			limit: 0
		}, options || {});
		this.options.classNames = classNames;
		this.options.events = events;

		this.keydownListener = this.keydown.bindAsEventListener(this);
		this.clickListener = this.click.bind(this);
		this.dblclickListener = this.dblclick.bind(this);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		this.grid.element.setAttribute("tabindex", this.options.tabIndex);
		if(this.options.hideGridFocus) {
			if(Prototype.Browser.IE) {
				this.grid.element.setAttribute("hidefocus", true);
			} else {
				Widget.Styles.addRule(
					"#" + this.grid.element.identify() + ":focus",
					"outline: none;",
					grid.styleSheet
				);
			}
		}

		if(this.options.limitFreeze) {
			if(this.options.limit < this.grid.options.freeze[1]) {
				this.options.limit = this.grid.options.freeze[1]
			}
		}

		this.selectedRow = null;
		this.selectedRows = [];

		this.grid.observe("keydown", this.keydownListener);
		this.grid.observe("click", this.clickListener);
		this.grid.observe("dblclick", this.dblclickListener);
	},
	keydown: function(event) {
		var handled = false;
		var key = event.which || event.keyCode;
		var row;
		if(!this.selectedRow) {
			switch(key) {
				case Event.KEY_DOWN:
					row = this.options.limit;
					handled = true;
					break;
				case Event.KEY_UP:
					row = this.grid.model.getRows() - 1;
					handled = true;
					break;
			}
			if(row) {
				this.highlight(row);
			}
		} else {
			var oldRow = this.selectedRow;
			switch(key) {
				case Event.KEY_DOWN:
					if(oldRow < this.grid.model.getRows() - 1) {
						row = oldRow + 1;
					}
					handled = true;
					break;
				case Event.KEY_UP:
					if(oldRow > this.options.limit) {
						row = oldRow - 1;
					}
					handled = true;
					break;
			}
			if(row && oldRow != row) {
				this.selectedRow = row;
				this.unhighlight(oldRow);
				this.highlight(row);
			}
		}
		if(handled) {
			this.grid.element.fire("grid:rowSelected", row);
			event.stop();
		}
	},
	click: function(event) {
		var cell = this.grid.getCellFromEvent(event);
		if(!cell) return;
		var pos = this.grid.getCellPosition(cell);
		if(pos.row >= this.options.limit) {
			if(this.selectedRow) {
				this.unhighlight(this.selectedRow);
			}
			this.selectedRow = pos.row;
			this.highlight(this.selectedRow);
			this.grid.fire(this.options.events.rowSelected, this.selectedRow);
		}
	},
	dblclick: function(event) {
		this.click(event);
		if(this.selectedRow) {
			this.grid.fire(this.options.events.rowDblClick, this.selectedRow);
		}
	},
	highlight: function(row) {
		this.grid.ensureVisible(0, row);
		for(var i = 0; i < this.grid.model.getCols(); ++i) {
			var cell = this.grid.getCell(i, row);
			if(cell) cell.addClassName(this.options.classNames.current);
		}
	},
	unhighlight: function(row) {
		for(var i = 0; i < this.grid.model.getCols(); ++i) {
			var cell = this.grid.getCell(i, row);
			if(cell) cell.removeClassName(this.options.classNames.current);
		}
	},
	destroy: function() {
		if(this.grid) {
			this.grid.stopObserving("keydown", this.keydownListener);
			this.grid.stopObserving("click", this.clickListener);
			this.grid.stopObserving("dblclick", this.dblclickListener);
		}
		this.grid = null;
		//TODO: Remove selection
	}
});
Object.extend(Widget.Grid.RowSelector, {
	rowSelectedEvent: "grid:rowSelected",
	rowDblClickEvent: "grid:rowDblClick"
});

//Data Model requires:
//  isSortable(col) > Boolean
//  sort(col, asc) > Boolean
Widget.Grid.ColumnSorter = Class.create(Widget.Grid.PluginBase, {
	initialize: function(options) {
		var classNames = Object.extend({
			sortable: "sorter-sortable",
			ascending: "sorter-ascending",
			descending: "sorter-descending"
		});
		this.options = Object.extend({
			refreshClear: "body",
			isSortable: function(x) { return true; },
			sort: function(x, asc) {
				var r = asc ? 1 : -1;
				this.model.data.sort(function(a, b) {
					if(!a[x] && !b[x]) return 0;
					if(!a[x] && b[x]) return 1;
					if(a[x] && !b[x]) return -1;
					return a[x] > b[x] ? r : a[x] < b[x] ? -r : 0;
				})
			}.bind(this)
		}, options || {});
		this.options.classNames = classNames;

		this.col = -1;
		this.asc = true;

		this.cellCreatedListener = this.cellCreated.bindAsEventListener(this);
		this.clickListener = this.click.bindAsEventListener(this);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		//HACK: Using private properties of Widget.Grid, which is okay I guess, but I could add getters to grid
		var row = grid.rows[0], cell;
		if(row) {
			$R(0, grid.cols.m).each(function(x) {
				if(this.options.isSortable(x) && (cell = row.c[x])) {
					cell.addClassName(this.options.classNames.sortable);
				}
			}.bind(this));
		}

		this.grid.observe(grid.options.events.cellCreated, this.cellCreatedListener);
		this.grid.observe("click", this.clickListener);
	},
	cellCreated: function(event) {
		if(event.memo.y == 0 && this.options.isSortable(event.memo.x)) {
			event.memo.cell.addClassName(this.options.classNames.sortable);
			if(event.memo.x == this.col) {
				event.memo.cell.addClassName(this.asc ? this.options.classNames.ascending : this.options.classNames.descending);
			}
		}
	},
	click: function(event) {
		var cell = this.getCell(event);
		if(!cell) return;
		var pos = this.grid.getCellPosition(cell);
		if(pos.row) return;
		if(pos.col == this.col) {
			this.asc = !this.asc;
		}
		if(this.options.isSortable(pos.col)) {
			this.options.sort(pos.col, this.asc);
			this.grid.refresh(this.options.refreshClear);
			if(this.col != -1) {
				this.grid.getCell(this.col, 0).removeClassName(this.options.classNames.ascending);
				this.grid.getCell(this.col, 0).removeClassName(this.options.classNames.descending);
			}
			this.col = pos.col;
			this.grid.getCell(this.col, 0).addClassName(this.asc ? this.options.classNames.ascending : this.options.classNames.descending);
		}
	},
	getCell: function(e) {
		var cell = this.grid.getCellFromEvent(e);
		if(!cell) return null;
		var pos = this.grid.getCellPosition(cell);
		if(pos.row == 0) return cell;
		return null;
	},
	destroy: function() {
		if(this.grid) {
			this.grid.stopObserving(this.grid.options.events.cellCreated, this.cellCreatedListener);
			this.grid.stopObserving("click", this.clickListener);
		}
	}
});

//Data model requires: setWidth(col, width)
//TODO: resize freeze columns
Widget.Grid.ColumnResizer = Class.create(Widget.Grid.PluginBase, {
	initialize: function(options) {
		var classNames = Object.extend({
			main: "resizer",
			handle: "resizer-handle",
			marker: "resizer-marker"
		}, options ? options.classNames : {} || {});
		this.options = Object.extend({
		}, options || {});
		this.options.classNames = classNames;

		this.mouseoverListener = this.mouseover.bindAsEventListener(this);
		this.dragstartListener = this.dragstart.bind(this);
		this.dragListener = this.drag.bind(this);
		this.dragendListener = this.dragend.bind(this);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		this.resizer = new Element("div", {
			className: this.options.classNames.main,
			style: "position: absolute; top: -1000px; left: -1000px; overflow: hidden;"
		});
		this.handle = new Element("div", {
			className: this.options.classNames.handle
		});
		this.resizer.insert(this.handle);
		this.marker = new Element("div", {
			className: this.options.classNames.marker,
			style: "height: 5000px;"
		});
		this.resizer.insert(this.marker);
		grid.element.insert(this.resizer);

		this.size = this.handle.getDimensions();
		this.left = Math.floor(this.size.width / 2) - this.grid.options.margins[0];

		grid.observe("mousemove", this.mouseoverListener);

		this.draggable = new Draggable(this.resizer, {
			constraint: "horizontal",
			starteffect: null,
			endeffect: null,
			onStart: this.dragstartListener,
			onDrag: this.dragListener,
			onEnd: this.dragendListener
		});
	},
	mouseover: function(event) {
		if(this.dragging) return;
		if(event.element() == this.resizer || event.element() == this.handle) return;
		var cell = this.getCell(event);
		if(cell) {
			var x = event.pointerX(), y = event.pointerY();
			var d = cell.getDimensions();
			var v = cell.viewportOffset();
			var p = cell.cumulativeOffset();
			var edge = "none";
			if(this.nearLeftEdge(x, y, v, d)) {
				edge = "left";
			} else if(this.nearRightEdge(x, y, v, d)) {
				edge = "right";
			} else {
			}
			if(cell != this.cell || edge != this.edge) {
				this.cell = cell;
				this.edge = edge;
				var e = this.grid.element.cumulativeOffset();
				var i = this.grid.element.getInnerMargins();
				var b = this.grid.body.getClientDimensions();
				var s = cell.cumulativeScrollOffset();

				if(edge != "none") {
					this.handle.setStyle({
						height: d.height + "px"
					});
					this.start = (edge == "left" ? (p.left - e.left - s.left - this.left) : (p.left + d.width - e.left - s.left - this.left));
					this.resizer.setStyle({
						top: "0px",
						left: this.start + "px",
						height: d.height + "px"
					});
				} else {
					this.resizer.setStyle({
						top: "-1000px",
						left: "-1000px"
					});
				}
			}
		} else {
			this.resizer.setStyle({
				top: "-1000px",
				left: "-1000px"
			});
			this.cell = null;
		}
	},
	dragstart: function(draggable, event) {
		this.dragging = true;
		var b = this.grid.element.getClientDimensions();
		this.resizer.setStyle({
			height: b.height + "px"
		});
	},
	drag: function(draggable, event) {
		//TODO: scrolling
		//TODO: limiting
	},
	dragend: function(draggable, event) {
		this.dragging = false;
		if(!this.cell) return;
		pos = this.grid.getCellPosition(this.cell);
		var col = pos.col;
		if(this.edge == "left") {
			col--;
		}

		var width = this.grid.model.getWidth(col) + this.resizer.positionedOffset().left - this.start + this.grid.options.margins[0];
		if(width < 0) width = 0;
		this.grid.model.setWidth(col, width);
		this.grid.setColumnWidth(col, width);

		var d = this.cell.getDimensions();
		var p = this.cell.cumulativeOffset();
		var e = this.grid.element.cumulativeOffset();
		var s = this.cell.cumulativeScrollOffset();
		this.start = (this.edge == "left" ? (p.left - e.left - s.left - this.left) : (p.left + d.width - e.left - s.left - this.left));
		this.resizer.setStyle({
			top: "0px",
			left: this.start + "px",
			height: d.height + "px"
		});
	},
	getCell: function(e) {
		var cell = this.grid.getCellFromEvent(e);
		if(!cell) return null;
		var pos = this.grid.getCellPosition(cell);
		if(pos.row != 0 || pos.col == 0) {
			cell = null;
		}
		return cell;
	},
	nearLeftEdge: function(x, y, p, d) {
		return x > p.left && x < p.left + this.size.width && y > p.top && y < p.top + d.height;
	},
	nearRightEdge: function(x, y, p, d) {
		return x > p.left + d.width - this.size.width && x < p.left + d.width && y > p.top && y < p.top + d.height;
	},
	destroy: function() {
		if(this.grid) {
			this.draggable.destroy();
			this.grid.stopObserving("mousemove", this.mouseoverListener);
			this.resizer.remove();
		}
		this.grid = null;
	}
});

//DataModel requires
//  isCollapsable(x, y) > boolean
//  isCollapsed(x, y) > boolean
//  getCollapsableRows(x, y) > int
//  setHeight(y, h)
Widget.Grid.RowCollapser = Class.create(Widget.Grid.PluginBase, {
	initialize: function(options) {
		var classNames = Object.extend({
			collapsed: "collapser-collapsed",
			expanded: "collapser-expanded"
		}, options ? options.classNames : {} || {});
		this.options = Object.extend({
			fullRow: false,
			alwaysShow: true,
			isCollapsable: function(y) { return false; },
			isCollapsed: function(y) { return this.model.getHeight(y + 1) <= 0; }.bind(this),
			getRows: function(y) { return 0; }
		}, options || {});
		this.options.classNames = classNames;

		this.cellcreatedListener = this.cellcreated.bindAsEventListener(this);
		this.mouseoverListener = this.mouseover.bindAsEventListener(this);
		this.mousemoveListener = this.mousemove.bindAsEventListener(this);
		this.mouseoutListener = this.mouseout.bindAsEventListener(this);
		this.clickListener = this.click.bindAsEventListener(this);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		if(this.options.alwaysShow) {
			this.grid.observe(this.grid.options.events.cellCreated, this.cellcreatedListener);
			//HACK: Using private properties of Widget.Grid
			var col = grid.cols[0], cell;
			if(col) {
				$R(0, grid.rows.m).each(function(y) {
					if(this.options.isCollapsable(y)) {
						if(grid.rows[y] && (cell = grid.rows[y].c[0])) {
							if(this.options.isCollapsed(y)) {
								cell.addClassName(this.options.classNames.collapsed);
							} else {
								cell.addClassName(this.options.classNames.expanded);
							}
						}
					}
				}.bind(this));
			}
		} else {
			this.grid.observe("mouseover", this.mouseoverListener);
			this.grid.observe("mousemove", this.mousemoveListener);
			this.grid.observe("mouseout", this.mouseoutListener);
		}
		this.grid.observe("click", this.clickListener);

		this.currentCell = null;
		this.currentPosition = null;

		this.rowHeights = {};
	},
	cellcreated: function(event) {
		event.memo.each(function(c) {
			if(!c.x && this.options.isCollapsable(c.y)) {
				if(this.options.isCollapsed(c.y)) {
					c.cell.addClassName(this.options.classNames.collapsed);
				} else {
					c.cell.addClassName(this.options.classNames.expanded);
				}
			}
		}, this);
	},
	mouseover: function(event) {
		var cell;
		if(cell = this.getCell(event)) {
			this.highlight(cell);
		}
	},
	mousemove: function(event) {
		var cell;
		if(cell = this.getCell(event)) {
			this.highlight(cell);
		}
	},
	mouseout: function(event) {
		this.highlight(null);
	},
	click: function(event) {
		if(this.options.alwaysShow) {
			this.currentCell = this.grid.getCellFromEvent(event);
			if(!this.currentCell) return;
			this.currentPosition = this.grid.getCellPosition(this.currentCell);
			this.currentCell = this.grid.getCell(0, this.currentPosition.row);
			this.currentPosition.col = 0;
			if(!this.options.isCollapsable(this.currentPosition.row)) {
				this.currentCell = null;
				this.currentPosition = null;
			}
		}
		if(!this.currentCell) return;
		var rows = this.options.getRows(this.currentPosition.row);
		var collapsed = this.options.isCollapsed(this.currentPosition.row);
		var heights = [];
		if(collapsed) {
			$R(this.currentPosition.row + 1, this.currentPosition.row + rows).each(function(y) {
				heights.push({ row: y, height: this.rowHeights[y] });
				this.model.setHeight(y, this.rowHeights[y]);
			}.bind(this));
		} else {
			$R(this.currentPosition.row + 1, this.currentPosition.row + rows).each(function(y) {
				heights.push({ row: y, height: 0 });
				this.rowHeights[y] = this.model.getHeight(y);
				this.model.setHeight(y, 0);
			}.bind(this));
		}
		this.grid.setRowHeight(heights);
		this.grid.performLayout();
		var cell = this.currentCell;
		this.highlight(null);
		this.highlight(cell);
	},
	highlight: function(cell) {
		if(this.currentCell == cell) return;
		var pos, c, m;
		if(this.currentCell) {
			pos = this.currentPosition;
			this.currentCell.removeClassName(this.options.classNames.collapsed);
			this.currentCell.removeClassName(this.options.classNames.expanded);
			this.currentCell = null;
		}
		if(cell) {
			pos = this.currentPosition = this.grid.getCellPosition(cell);
			if(pos.col == 0 && this.options.isCollapsable(pos.row)) {
				if(this.options.isCollapsed(pos.row)) {
					cell.addClassName(this.options.classNames.collapsed);
				} else {
					cell.addClassName(this.options.classNames.expanded);
				}
				this.currentCell = cell;
			}
		}
	},
	destroy: function() {
		if(this.grid) {
			if(this.options.alwaysShow) {
				this.grid.stopObserving(this.grid.options.events.cellCreated, this.cellcreatedListener);
			} else {
				this.grid.stopObserving("mouseover", this.mouseoverListener);
				this.grid.stopObserving("mousemove", this.mousemoveListener);
				this.grid.stopObserving("mouseout", this.mouseoutListener);
			}
			this.grid.stopObserving("click", this.clickListener);
		}
		this.grid = null;
	}
});

//Data model requires: setWidth(col, width)
//colWidths is an array of proportional sizes. Columns with width -1 remain as they are.
//eg. colWidths = [ -1, 1, 2, 1, 1, 1 ]
//  col[0] remains it's current size;
//  col[2] is twice the size of col[1, 3, 4, 5]
Widget.Grid.ColumnAutosizer = Class.create(Widget.Grid.PluginBase, {
	initialize: function(colWidths, options) {
		this.colWidths = colWidths;
		this.options = Object.extend({
			minimum: 0
		}, options || {});
		this.resizeHandler = this.resize.bind(this);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		grid.observe(grid.options.events.resized, this.resizeHandler);
		//this.resize();
	},
	resize: function() {
		var used = 0, prop = 0, count = 0, dm = this.grid.model, m0 = this.grid.options.margins[0], m = this.options.minimum, c = dm.getCols(), cw = this.colWidths;

		for(var i = 0; i < c; ++i) {
			if(!cw[i] || cw[i] == -1) {
				used += dm.getWidth(i) + m0;
			} else {
				prop += cw[i];
				++count;
			}
		}
		var d = this.grid.element.getClientDimensions().width - this.grid.body.getInnerMargins().width;
		var w1 = Math.max(Math.floor((d - count * m0 - used - m0) / prop), 0);
		var cols = [];

		var ww;
		for(var i = 0; i < c; ++i) {
			if(cw[i] && cw[i] != -1) {
				ww = Math.max(Math.floor(cw[i] * w1), m);
				cols.push({ col: i, width: ww });
				dm.setWidth(i, ww);
			}
		}
		this.grid.setColumnWidth(cols);
	},
	destroy: function() {
		if(this.grid) {
			this.grid.stopObserving(this.grid.options.events.resized, this.resizeHandler);
		}
		this.grid = null;
	}
});

Widget.Grid.RowAdjuster = Class.create(Widget.Grid.PluginBase, {
	initialize: function(options) {
		this.options = Object.extend({
			defaultHeight: 20,
			delay: 0.1,
			pause: 10,
			stop: false
		}, options || {});
		this.cellCreatedHandler = this.cellCreated.bind(this);
		this.refreshingHandler = this.refreshing.bind(this);
		this.adjustHandler = this.adjust.bind(this);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		grid.observe(grid.options.events.cellCreated, this.cellCreatedHandler);
		grid.observe(grid.options.events.refreshed, this.refreshingHandler);

		this.timeout = this.adjustHandler.delay(this.options.delay);
		this.y = this.grid.options.freeze[1] - 1;
	},
	adjust: function() {
		if(!this.grid) return;
		var m = this.model, r = null, cls = this.grid.options.classNames.innerCell, f1 = this.grid.options.freeze[1] - 1;
		var s = this.y == f1;
		while(!r) {
			this.y++;
			if(this.y >= m.getRows()) {
				this.y = f1;
				r = true;
			} else {
				var r = this.grid.rows[this.y];
				if(r && m.getHeight(this.y)) {
					var h = 0; //m.getHeight(this.y);
					for(var x = 0; x < m.getCols(); ++x) {
						var c = r.c[x];
						if(c) {
							var ch = c.down("." + cls).scrollHeight;
							if(ch > h) h = ch;
						}
					}
					if(h != m.getHeight(this.y) && h != 0) {
						this.grid.setRowHeight(this.y, h);
						m.setHeight(this.y, h);
					} else if(!this.options.stop) {
						r = null;
					}
				}
			}
		}
		this.timeout = this.adjustHandler.delay((s && this.y == f1) ? this.options.pause : this.options.delay);
	},
	cellCreated: function(e) {
		var y = 65535;
		e.memo.each(function(c) {
			if(c.y < y) y = c.y;
		});
		if(y < this.model.getRows()) {
			this.y = y;
		}
		clearTimeout(this.timeout);
		this.adjust();
	},
	refreshing: function(e) {
		this.y = -1;
		var m = this.model, h = this.options.defaultHeight;
		for(var y = 0, l = m.getRows(); y < l; ++y) {
			m.setHeight(y, h);
		}
	},
	destroy: function() {
		if(this.grid) {
			this.grid.stopObserving(this.grid.options.events.cellCreated, this.cellCreatedHandler);
			this.grid.stopObserving(this.grid.options.events.refreshed, this.refreshedHandler);
		}
		clearTimeout(this.timeout);
		this.grid = null;
	}
});

Widget.Grid.Docker = Class.create(Widget.Grid.PluginBase, {
	initialize: function(docker, options) {
		this.docker = docker;
		this.resizedHandler = this.resized.bind(this);
	},
	setGrid: function($super, grid, model) {
		this.destroy();

		$super(grid, model);

		this.docker.observe(this.docker.options.events.resized, this.resizedHandler);
	},
	resized: function() {
		this.grid.resize();
	},
	destroy: function() {
		if(this.grid) {
			this.docker.stopObserving(this.docker.options.events.resized, this.resizedHandler);
		}
	}
});

//-- Cell Editor Plugins -------------------------------------------------------

//The data model requires additional methods for editing:
//  getEditor(x, y) > Widget.Grid.CellEditor.EditorBase or subclass
//  setContent(x, y, value)
Widget.Grid.CellEditor = Class.create({
	initialize: function(options) {
		this.options = Object.extend({
		}, options || {});
	},
	editCell: function(data) {
		if(this.data) {
			this.cancelEdit();
		}
		this.data = data;
		this.pos = data.grid.getCellPosition(data.cell);
		this.editor = data.grid.model.getEditor(this.pos.col, this.pos.row);
		if(this.editor) this.editor.show(this, data.grid, data.cell);
		return this.editor;
	},
	cancelEdit: function() {
		this.okEdit(null);
	},
	okEdit: function(value) {
		if(value) {
			this.data.grid.model.setContent(this.pos.col, this.pos.row, value);
		}
		this.editor.destroy();
		this.pos = null;
		this.editor = null;
		this.data.editComplete();
		this.data = null;
	},
	destroy: function() {
		if(this.editor) {
			this.editor.detroy();
		}
		this.data = null;
	}
});

Widget.Grid.CellEditor.EditorBase = Class.create({
	initialize: function(options) {
		this.options = Object.extend({
			editorClassName: "editor"
		}, options || {});

		this.element = new Element("div", {
			style: "position: absolute; overflow: hidden; top: -5000px; left: -5000px;",
			className: this.options.editorClassName
		});
		$(document.body).insert(this.element);
	},
	show: function(editor, grid, cell) {
		this.editor = editor;
		this.grid = grid;
		this.cell = cell;
		this.position();
	},
	position: function() {
		this.cell.up().insert(this.element);
		var i = this.element.getInnerMargins();
		var d = this.cell.getClientDimensions();
		var p = this.cell.positionedOffset();
		this.element.setStyle({
			top: p.top + "px",
			left: p.left + "px",
			width: (d.width - i.width) + "px",
			height: (d.height - i.height) + "px"
		});
	},
	hide: function() {
		this.element.hide();
	},
	destroy: function() {
		this.element.remove();
		this.element = null;
	}
});

//TODO: Cancel edit option on blur
//TODO: Multiline edit
Widget.Grid.CellEditor.Text = Class.create(Widget.Grid.CellEditor.EditorBase, {
	initialize: function($super, value, options) {
		options = Object.extend({
			textEditorClassName: "textEditor"
		}, options || {});
		$super(options);

		this.textarea = new Element("textarea", {
			className: this.options.textEditorClassName
		}).update(value);
		this.element.insert(this.textarea);

		this.blurListener = this.blur.bind(this);
		this.keydownListener = this.keydown.bindAsEventListener(this);

		this.textarea.observe("blur", this.blurListener);
		this.textarea.observe("keydown", this.keydownListener);
	},
	show: function($super, editor, grid, cell) {
		$super(editor, grid, cell);
		this.textarea.select();
		this.textarea.focus();
	},
	blur: function() {
		this.editor.okEdit(this.textarea.value);
	},
	keydown: function(event) {
		switch(event.which || event.keyCode) {
			case Event.KEY_RETURN:
			case Event.KEY_TAB:
				this.editor.okEdit(this.textarea.value);
				event.stop();
				break;
			case Event.KEY_ESC:
				this.editor.okEdit(null);
				event.stop();
				break;
		}
	},
	destroy: function($super) {
		this.textarea.stopObserving("blur", this.blurListener);
		this.textarea.stopObserving("keydown", this.keydownListener);
		this.textarea.remove();
		this.textarea = null;
		$super();
	}
});

Widget.Grid.CellEditor.TextNumeric = Class.create(Widget.Grid.CellEditor.EditorBase, {
	initialize: function($super, value, options) {
		options = Object.extend({
			textEditorClassName: "textNumericEditor"
		}, options || {});
		$super(options);

		this.textarea = new Element("textarea", {
			className: this.options.textEditorClassName
		}).update(value);
		this.element.insert(this.textarea);

		this.blurListener = this.blur.bind(this);
		this.keydownListener = this.keydown.bindAsEventListener(this);

		this.textarea.observe("blur", this.blurListener);
		this.textarea.observe("keydown", this.keydownListener);
	},
	show: function($super, editor, grid, cell) {
		$super(editor, grid, cell);
		this.textarea.select();
		this.textarea.focus();
	},
	blur: function() {
		this.editor.okEdit(this.textarea.value);
	},
	keydown: function(event) {
		var key = event.which || event.keyCode
		switch(key) {
			case Event.KEY_RETURN:
			case Event.KEY_TAB:
				this.editor.okEdit(this.textarea.value);
				event.stop();
				break;
			case Event.KEY_ESC:
				this.editor.okEdit(null);
				event.stop();
				break;
			default:
				if(!key.inRange(48, 57) && !key.inRange(96, 105)) {
					event.stop();
				}
		}
	},
	destroy: function($super) {
		this.textarea.stopObserving("blur", this.blurListener);
		this.textarea.stopObserving("keydown", this.keydownListener);
		this.textarea.remove();
		this.textarea = null;
		$super();
	}
});
/**
 * @fileoverview table.js
 * Widget.Table creates a table based grid control.<br />
 * Requires Prototype (http://www.prototypejs.org) 1.6 or later<br /><br />
 * � 2008-2009 Marc Heiligers (marc@e-technik.com) http://www.e-technik.com<br /><br />
 * This class may not be reused without a license.<br />
 * Change History:<br />
 * Version 0.0.1: 20/12/2008<br />
 * - Initial version
 * TODO:
 * - addRow, insertRow, updateRow, removeRow
 * - mergeCells
 * - ColumnResizer plugin
 * - RowAdjuster plugin
 * - RowSelector plugin
 * - set table required styles automatically
 * - cell editing
 * - events
 * - options (events, classes)
 * - destroy
 */
var Widget = window.Widget || {};
Widget.Table = Class.create({
	initialize: function(element, model, options) {
		this.element = $(element);
		this.model = model;

		var events = Object.extend({
			rendering: Widget.Table.renderingEvent,
			rendered: Widget.Table.renderedEvent,
			refreshing: Widget.Table.refreshingEvent,
			refreshed: Widget.Table.refreshedEvent,
			resizing: Widget.Table.resizingEvent,
			resized: Widget.Table.resizedEvent,
			destroying: Widget.Table.destroyingEvent,
			destroyed: Widget.Table.destroyedEvent
		}, options ? options.events || {} : {});
		this.options = Object.extend({
			freeze: [ 0, 1 ],
			destroyObservers: true,
			rowHeights: true
		}, options || {});
		this.options.events = events;

		this.observe = Element.observe.curry(this.element);
		this.stopObserving = Element.stopObserving.curry(this.element);

		this.render();
	},
	getRowCount: function() {
		var t = this.body.firstChild; //.down("table");
		return t ? this.options.freeze[1] + t.rows.length - 1 : -1;
	},
	getColumnCount: function() {
		var t = this.body.firstChild; //.down("table");
		return t ? this.options.freeze[0] + (t.rows[0] ? t.rows[0].cells.length : 0) : -1;
	},
	getCell: function(x, y) {
		if(x < this.options.freeze[0]) {
			if(y < this.options.freeze[1]) {
				if(!this.freezeStaticRows) {
					this.freezeStaticRows = this.freezeStatic.firstChild.rows; //.select("tr");
				}
				return $(this.freezeStaticRows[y + 1].cells[x]);
			} else {
				if(!this.freezeLeftRows) {
					this.freezeLeftRows = this.freezeLeft.firstChild.rows; //.select("tr");
				}
				return $(this.freezeLeftRows[y - this.options.freeze[1] + 1].cells[x]);
			}
		} else {
			if(y < this.options.freeze[1]) {
				if(!this.freezeTopRows) {
					this.freezeTopRows = this.freezeTop.firstChild.rows; //.select("tr");
				}
				return $(this.freezeTopRows[y + 1].cells[x - this.options.freeze[0]]);
			} else {
				if(!this.bodyRows) {
					this.bodyRows = this.body.firstChild.rows; //.select("tr");
				}
				return $(this.bodyRows[y - this.options.freeze[1] + 1].cells[x - this.options.freeze[0]]);
			}
		}
	},
	ensureVisible: function(x, y) {
		var c = this.getCell(x, y);
		if(x >= this.options.freeze[0]) {
			this.body.scrollLeft = c.offsetLeft;
		}
		if(y >= this.options.freeze[1]) {
			this.body.scrollTop = c.offsetTop;
		}
	},
	getRow: function(y, freeze) {
		if(freeze && this.options.freeze[0]) {
			return $(this.getCell(0, y).parentNode);
		}
		return $(this.getCell(this.options.freeze[0], y).parentNode);
	},
	getLocation: function(cell) {
		if(cell.tagName.toLowerCase() != "td") {
			cell = cell.up("td");
		}
		if(cell.tagName.toLowerCase() != "td") {
			return { x: -1, y: -1 };
		}
		var x = cell.previousSiblings().length;
		var y = cell.up("tr").previousSiblings().length;
		var div = cell.up("div");
		if(div == this.freezeStatic) {
			return { x: x, y: y - 1 };
		}
		if(div == this.freezeTop) {
			return { x: x + this.options.freeze[0], y: y - 1 };
		}
		if(div == this.freezeLeft) {
			return { x: x, y: y + this.options.freeze[1] - 1 };
		}
		if(div == this.body) {
			return { x: x + this.options.freeze[0], y: y + this.options.freeze[1] - 1 };
		}
		return { x: -1, y: -1 };
	},
	setWidth: function(x, w) {
		if(this.options.freeze[1]) {
			if(x < this.options.freeze[0]) {
				$(this.freezeLeft.down("table").rows[0].cells[x]).setStyle({ width: w + "px" });
			} else {
				$(this.body.down("table").rows[0].cells[x - this.options.freeze[0]]).setStyle({ width: w + "px" });
			}
		}
		this.getCell(x, -1).setStyle({ width: w + "px" });
		this.resize.bind(this).defer();
	},
	setHeight: function(y, h) {
		this.setRowStyle(y, { height: h + "px" });
	},
	setRowStyle: function(y, styles, bodyStyles, cells) {
		if(bodyStyles === true) {
			var r = this.getRow(y, true);
			for(var x = 0; x < r.cells.length; ++x) {
				$(r.cells[x]).setStyle(styles);
			}
			r = this.getRow(y);
			for(var x = 0; x < r.cells.length; ++x) {
				$(r.cells[x]).setStyle(styles);
			}
		} else if(cells) {
			var r = this.getRow(y, true);
			for(var x = 0; x < r.cells.length; ++x) {
				$(r.cells[x]).setStyle(styles);
			}
			r = this.getRow(y);
			for(var x = 0; x < r.cells.length; ++x) {
				$(r.cells[x]).setStyle(bodyStyles);
			}
		} else {
			if(this.options.freeze[1]) {
				this.getRow(y, true).setStyle(styles);
			}
			this.getRow(y).setStyle(bodyStyles || styles);
		}
		this.resize.bind(this).defer();
	},
	setColumnStyle: function(x, styles, bodyStyles) {
		for(var y = 0; y < this.model.getRows(); ++y) {
			if(y >= this.options.freeze[1]) {
				this.getCell(x, y).setStyle(bodyStyles || styles);
			} else {
				this.getCell(x, y).setStyle(styles);
			}
		}
		this.resize.bind(this).defer();
	},
	setCellStyle: function(x, y, styles, container) {
		var c = this.getCell(x, y);
		if(container) c = $(c.firstChild);
		c.setStyle(styles);
	},
	hideRow: function(y) {
		if(this.options.freeze[1]) {
			this.getRow(y, true).hide();
		}
		this.getRow(y).hide();
	},
	showRow: function(y) {
		if(this.options.freeze[1]) {
			this.getRow(y, true).show();
		}
		this.getRow(y).show();
	},
	updateRow: function(y, data, blanks) {
		for(var x = 0; x < this.getColumnCount(); ++x) {
			if(x > data.length) {
				if(blanks) this.getCell(x, y).down("span").update("");
			} else {
				this.getCell(x, y).down("span").update(data[x]);
			}
		}
	},
	removeRow: function(y) {
		if(this.options.freeze[1]) {
			this.getRow(y, true).remove();
		}
		this.getRow(y).remove();
		this.resize();
	},
	refresh: function() {
		this.fire(this.options.events.refreshing);
		this.resetRows();
		this.element.update("");
		this.render();
		this.fire(this.options.events.refreshed);
	},
	resize: function() {
		this.fire(this.options.events.resizing);
		var s;
		if(this.options.freeze[0] == 0 && this.options.freeze[1] == 0) {
			s = { width: 0, height: 0 };
		} else if(this.options.freeze[0] == 0) {
			s = { width: 0, height: this.freezeTop.getScrollDimensions().height };
		} else if(this.options.freeze[1] == 0) {
			s = { width: this.freezeLeft.getScrollDimensions().width, height: 0 };
		} else {
			s = this.freezeStatic.getScrollDimensions();
		}
		var left = s.width;
		var top = s.height;
		var d = this.element.getClientDimensions();
		this.body.setStyle({
			top: top + "px",
			left: left + "px",
			width: Math.max(d.width - left, 0) + "px",
			height: Math.max(d.height - top, 0) + "px"
		});
		var i = this.body.getInnerMargins();
		this.freezeTop.setStyle({
			top: "0px",
			left: left + "px",
			width: Math.max(d.width - left - i.width, 0) + "px",
			height: top + "px"
		});
		this.freezeLeft.setStyle({
			top: top + "px",
			left: "0px",
			width: left + "px",
			height: Math.max(d.height - top - i.height, 0) + "px"
		});
		this.freezeStatic.setStyle({
			top: "0px",
			left: "0px",
			width: left + "px",
			height: top + "px"
		});
		try { //TODO: Figure out why IE fails on this sometimes
			this.freezeTop.scrollLeft = this.body.scrollLeft;
			this.freezeLeft.scrollTop = this.body.scrollTop;
		} catch(ex) {}
		this.fire(this.options.events.resized);
	},
	fire: function(e, o) {
		return this.element.fire(e, Object.extend({ table: this }, o || {}));
	},
	resetRows: function() {
		this.freezeStaticRows = null;
		this.freezeLeftRows = null;
		this.freezeTopRows = null;
		this.bodyRows = null;
	},
	render: function() {
		this.fire(this.options.events.rendering);
		this.resetRows();
		this.freezeStatic = new Element("div", {
			style: "position: absolute; overflow: hidden;",
			className: "freezePane freezeStatic"
		});
		this.freezeTop = new Element("div", {
			style: "position: absolute; overflow: hidden;",
			className: "freezePane freezeTop"
		});
		this.freezeLeft = new Element("div", {
			style: "position: absolute; overflow: hidden;",
			className: "freezePane freezeLeft"
		});
		this.body = new Element("div", {
			style: "position: absolute; overflow: auto;",
			className: "bodyPane"
		}).observe("scroll", this.scroll.bind(this));
		this.element.insert(this.freezeStatic);
		this.element.insert(this.freezeTop);
		this.element.insert(this.freezeLeft);
		this.element.insert(this.body);

		if(this.options.rowHeights) {
			this.heights = this.model.getHeights();
		}
		this.widths = this.model.getWidths();
		this.data = this.model.getData();
		var cols = this.model.getCols(), rows = this.model.getRows();

		this.renderFreezeStatic.bind(this).defer();
	},
	renderFreezeStatic: function() {
		this.renderCells(this.freezeStatic, "freeze", "cell", 0, this.options.freeze[0], 0, this.options.freeze[1]);
		this.renderFreezeTop.bind(this).defer();
	},
	renderFreezeTop: function() {
		var cols = this.model.getCols(), rows = this.model.getRows();
		this.renderCells(this.freezeTop, "freeze", "cell", this.options.freeze[0], cols, 0, this.options.freeze[1]);
		this.renderFreezeLeft.bind(this).defer();
		this.freezeTop.setStyle({ left: this.freezeStatic.getWidth() + "px" });
	},
	renderFreezeLeft: function() {
		var cols = this.model.getCols(), rows = this.model.getRows();
		this.renderCells(this.freezeLeft, "freeze", "cell", 0, this.options.freeze[0], this.options.freeze[1], rows);
		this.renderBody.bind(this).defer();
		this.freezeLeft.setStyle({ top: this.freezeStatic.getHeight() + "px" });
	},
	renderBody: function() {
		var cols = this.model.getCols(), rows = this.model.getRows();
		this.renderCells(this.body, "body", "cell", this.options.freeze[0], cols, this.options.freeze[1], rows);
		//var tl = this.freezeStatic.getDimensions();
		//var el = this.element.getClientDimensions();
		//this.body.setStyle({ left: tl.width + "px", top: tl.height + "px", width: (el.width - tl.width) + "px", height: (el.height + tl.height) + "px" });
		this.resize.bind(this).defer();

		this.fire(this.options.events.rendered);
	},
	renderCells: function(pane, cellClass, spanClass, x0, x1, y0, y1) {
		var hs = this.options.rowHeights ? this.heights : false, ws = this.widths, d = this.data, t = "<td class='" + cellClass + "'><span class='" + spanClass + "'>";
		var html = [ "<table>" ];
		html.push("<tr style='height:0px;'>");
		for(var x = x0; x < x1; ++x) {
			html.push("<td class='" + cellClass + "' style='width:");
			html.push(ws[x]);
			html.push("px;height:0px;padding:0px;'><span class='" + spanClass + "' style='height:0px;padding:0px;'></span></td>");
		}
		html.push("</tr>");
		for(var y = y0; y < y1; ++y) {
			if(hs) {
				html.push("<tr style='height:");
				html.push(hs[y]);
				html.push("px;'>");
			} else {
				html.push("<tr>");
			}
			for(var x = x0; x < x1; ++x) {
				html.push(t);
				html.push(d[y][x]);
				html.push("</span></td>");
			}
			html.push("</tr>");
		}
		html.push("</table>");
		pane.insert(html.join(""));
	},
	scroll: function() {
		this.freezeTop.scrollLeft = Math.max(this.body.scrollLeft, 0);
		this.freezeLeft.scrollTop = Math.max(this.body.scrollTop, 0);
	},
	destroy: function() {
		this.fire(this.options.events.destroying);
		this.freezeStatic.stopObserving();
		this.freezeTop.stopObserving();
		this.freezeLeft.stopObserving();
		this.body.stopObserving();
		this.element.update("");
		this.fire(this.options.events.destroyed);
		this.destroyed = true;
		if(this.options.destroyObservers) {
			this.stopObserving();
		}
	}
});

Object.extend(Widget.Table, {
	renderingEvent: "table:rendering",
	renderedEvent: "table:rendered",
	refreshingEvent: "table:refreshing",
	refreshedEvent: "table:refreshed",
	resizingEvent: "table:resizing",
	resizedEvent: "table:resized",
	destroyingEvent: "table:destroying",
	destroyedEvent: "table:destroyed"
});

//TODO: Mousing (alwaysShow false)
//TODO: fullRow
Widget.Table.RowCollapser = Class.create({
	initialize: function(table, options) {
		this.table = table;
		var classNames = Object.extend({
			collapsed: "collapser-collapsed",
			expanded: "collapser-expanded"
		}, options ? options.classNames : {} || {});
		this.options = Object.extend({
			delay: 0.5,
			fullRow: true,
			alwaysShow: true,
			isCollapsable: function(y) { return false; },
			isCollapsed: function(y) { return false; },
			getRows: function(y) { return 0; }
		}, options || {});
		this.options.classNames = classNames;

		this.renderedObserver = this.rendered.bind(this);
		this.clickObserver = this.click.bind(this);
		this.destroyObserver = this.destroy.bind(this);
		table.observe(table.options.events.rendered, this.renderedObserver);
		table.observe("click", this.clickObserver);
		table.observe(table.options.events.destroying, this.destroyObserver);
	},
	rendered: function(e) {
		if(!this.table || this.table.destroyed) return;
		if(this.options.delay) {
			this.apply.bind(this).delay(this.options.delay);
		} else {
			this.apply();
		}
	},
	apply: function() {
		if(!this.table || this.table.destroyed) return;
		var t = this.table;
		for(var y = 0; y < t.getRowCount(); ++y) {
			if(this.options.isCollapsable(y)) {
				var c = t.getCell(0, y);
				if(this.options.isCollapsed(y)) {
					c.addClassName(this.options.classNames.collapsed);
				} else {
					c.addClassName(this.options.classNames.expanded);
				}
			}
		}
		t.resize();
	},
	click: function(e) {
		var t = this.table;
		var l = t.getLocation(e.element());
		if(l.x == -1) return;
		this.toggle(l.y);
	},
	isCollapsed: function(y) {
		if(!this.options.isCollapsable(y)) return false;
		var c = true, t = this.table;
		for(var yy = y + 1; yy < this.options.getRows(y) + y + 1; ++yy) {
			c &= t.getRow(yy).getStyle("display") == "none";
		}
		return c;
	},
	getCollapsed: function() {
		var r = [];
		var t = this.table;
		for(var y = 0; y < t.getRowCount(); ++y) {
			if(this.options.isCollapsable(y) && this.isCollapsed(y)) {
				r.push(y);
			}
		}
		return y;
	},
	toggle: function(y) {
		if(this.options.isCollapsable(y)) {
			if(this.isCollapsed(y)) {
				this.expand(y);
			} else {
				this.collapse(y);
			}
		}
	},
	collapse: function(y, noResize) {
		var t = this.table;
		for(var yy = y + 1; yy < Math.min(this.options.getRows(y) + y + 1, t.getRowCount()); ++yy) {
			t.hideRow(yy);
		}
		t.getCell(0, y).removeClassName(this.options.classNames.expanded);
		t.getCell(0, y).addClassName(this.options.classNames.collapsed);
		if(!noResize) t.resize();
	},
	collapseAll: function() {
		var t = this.table;
		for(var y = 0; y < t.getRowCount(); ++y) {
			if(this.options.isCollapsable(y)) {
				this.collapse(y, true);
			}
		}
		t.resize();
	},
	expand: function(y, noResize) {
		var t = this.table;
		for(var yy = y + 1; yy < Math.min(this.options.getRows(y) + y + 1, t.getRowCount()); ++yy) {
			t.showRow(yy);
		}
		t.getCell(0, y).removeClassName(this.options.classNames.collapsed);
		t.getCell(0, y).addClassName(this.options.classNames.expanded);
		if(!noResize) t.resize();
	},
	expandAll: function() {
		var t = this.table;
		for(var y = 0; y < t.getRowCount(); ++y) {
			if(this.options.isCollapsable(y)) {
				this.expand(y, true);
			}
		}
		t.resize();
	},
	destroy: function() {
		this.table.stopObserving(this.table.options.events.rendered, this.renderedObserver);
		this.table.stopObserving("click", this.clickObserver);
		this.table.stopObserving(this.table.options.events.destroying, this.destroyObserver);
		this.table = null;
	}
});

Widget.Table.Docker = Class.create({
	initialize: function(table, docker, options) {
		this.table = table;
		this.docker = docker;
		this.renderedObserver = this.rendered.bind(this);
		table.observe(table.options.events.rendered, this.renderedObserver);
		this.destroyObserver = this.destroy.bind(this);
		table.observe(table.options.events.destroying, this.destroyObserver);
	},
	rendered: function() {
		if(!this.table || this.table.destroyed) return;
		this.resizeObserver = this.resized.bind(this);
		this.docker.observe(this.docker.options.events.resized, this.resizeObserver);
	},
	resized: function() {
		if(!this.table || this.table.destroyed) return;
		if(this.table.element.visible()) {
			this.table.resize();
		}
	},
	destroy: function() {
		this.docker.stopObserving(this.docker.options.events.resized, this.resizeObserver);
		if(this.resizeObserver) {
			this.table.stopObserving(this.table.options.events.resized, this.resizeObserver);
		}
		this.table.stopObserving(this.table.options.events.destroying, this.destroyObserver);
		this.table = null;
	}
});

Widget.Table.ColumnAutosizer = Class.create({
	initialize: function(table, colWidths, options) {
		this.table = table;
		this.colWidths = colWidths || [];
		this.options = Object.extend({
			margin: 1,
			minimum: 0,
			ignore: 1
		}, options || {});
		this.renderedObserver = this.rendered.bind(this);
		table.observe(table.options.events.rendered, this.renderedObserver);
		this.destroyObserver = this.destroy.bind(this);
		table.observe(table.options.events.destroying, this.destroyObserver);
	},
	rendered: function() {
		this.resizeObserver = this.resized.bind(this);
		this.table.observe(this.table.options.events.resized, this.resizeObserver);
	},
	resized: function() {
		if(this.ignore || !this.table.body || !this.table.body.down("table") || !this.table.element.visible()) return;
		this.ignore = true;

		var used = 0, prop = 0, count = 0, t = this.table, g = this.options.margin, m = this.options.minimum, c = this.table.model.getCols(), cw = this.colWidths, dw = this.table.model.getWidths();

		for(var i = 0; i < c; ++i) {
			if(!cw[i] || cw[i] == -1) {
				used += dw[i];
			} else {
				prop += cw[i];
				++count;
			}
		}
		var d = this.table.element.getClientDimensions().width - this.table.body.getInnerMargins().width;
		var w1 = Math.max(Math.floor((d - used - c * g - g) / prop), 0);
		var cols = [];

		var ww;
		for(var i = 0; i < c; ++i) {
			if(cw[i] && cw[i] != -1) {
				ww = Math.max(Math.floor(cw[i] * w1), m);
				t.setWidth(i, ww);
			}
		}

		this.clearIgnore.bind(this).delay(this.options.ignore);
	},
	clearIgnore: function() {
		this.ignore = false;
	},
	destroy: function() {
		this.table.stopObserving(this.table.options.events.rendered, this.renderedObserver);
		if(this.resizeObserver) {
			this.table.stopObserving(this.table.options.events.resized, this.resizeObserver);
		}
		this.table.stopObserving(this.table.options.events.destroying, this.destroyObserver);
	}
});

Widget.Table.RowSelector = Class.create({
	initialize: function(table, options) {
		this.table = table;

		var classNames = Object.extend({
			current: "selected-current",
			other: "selected-other"
		}, options ? options.classNames : {} || {});

		var events = Object.extend({
			rowSelected: Widget.Table.RowSelector.rowSelectedEvent,
			rowDblClick: Widget.Table.RowSelector.rowDblClickEvent
		}, options ? options.events : {} || {});

		this.options = Object.extend({
			multiple: false,
			tabIndex: -1,
			hidetableFocus: true,
			limitFreeze: true,
			limit: 0
		}, options || {});
		this.options.classNames = classNames;
		this.options.events = events;

		this.table.element.setAttribute("tabindex", this.options.tabIndex);
		if(this.options.hidetableFocus) {
			if(Prototype.Browser.IE) {
				this.table.element.setAttribute("hidefocus", true);
			} else {
				this.table.element.setStyle({ outline: "none" });
				/*Widget.Styles.addRule(
					"#" + this.table.element.identify() + ":focus",
					"outline: none;",
					table.styleSheet
				);*/
			}
		}

		if(this.options.limitFreeze) {
			if(this.options.limit < this.table.options.freeze[1]) {
				this.options.limit = this.table.options.freeze[1]
			}
		}

		this.selectedRow = null;
		this.selectedRows = [];

		this.keydownObserver = this.keydown.bind(this);
		this.clickObserver = this.click.bind(this);
		this.dblclickObserver = this.dblclick.bind(this);
		this.table.observe("keydown", this.keydownObserver);
		this.table.observe("click", this.clickObserver);
		this.table.observe("dblclick", this.dblclickObserver);

		this.destroyObserver = this.destroy.bind(this);
		this.table.observe(this.table.options.events.destroying, this.destroyObserver);
	},
	keydown: function(e) {
		var handled = false;
		var key = e.which || e.keyCode;
		var row;
		if(!this.selectedRow) {
			switch(key) {
				case Event.KEY_DOWN:
					row = this.options.limit;
					handled = true;
					break;
				case Event.KEY_UP:
					row = this.table.model.getRows() - 1;
					handled = true;
					break;
			}
			if(row) {
				this.highlight(row);
			}
		} else {
			var oldRow = this.selectedRow;
			switch(key) {
				case Event.KEY_DOWN:
					if(oldRow < this.table.model.getRows() - 1) {
						row = oldRow + 1;
					}
					handled = true;
					break;
				case Event.KEY_UP:
					if(oldRow > this.options.limit) {
						row = oldRow - 1;
					}
					handled = true;
					break;
			}
			if(row && oldRow != row) {
				this.selectedRow = row;
				this.unhighlight(oldRow);
				this.highlight(row);
			}
		}
		if(handled) {
			this.table.element.fire(this.options.events.rowSelected, { y: this.selectedRow });
			e.stop();
		}
	},
	click: function(e) {
		var pos = this.table.getLocation(e.element());
		var cell = this.table.getCell(pos.x, pos.y);
		if(!cell) return;
		if(pos.y >= this.options.limit) {
			if(this.selectedRow) {
				this.unhighlight(this.selectedRow);
			}
			this.selectedRow = pos.y;
			this.highlight(this.selectedRow);
			this.table.fire(this.options.events.rowSelected, { y: this.selectedRow });
		}
	},
	dblclick: function(e) {
		this.click(e);
		if(this.selectedRow) {
			this.table.fire(this.options.events.rowDblClick, { y: this.selectedRow });
		}
	},
	highlight: function(row) {
		//this.table.ensureVisible(0, row);
		this.table.getRow(row).addClassName(this.options.classNames.current);
	},
	unhighlight: function(row) {
		this.table.getRow(row).removeClassName(this.options.classNames.current);
	},
	destroy: function() {
		this.table.stopObserving("keydown", this.keydownObserver);
		this.table.stopObserving("click", this.clickObserver);
		this.table.stopObserving("dblclick", this.dblclickObserver);
		this.table.stopObserving(this.table.options.events.destroying, this.destroyObserver);
		this.table = null;
		//TODO: Remove selection
	}
});
Object.extend(Widget.Table.RowSelector, {
	rowSelectedEvent: "table:rowSelected",
	rowDblClickEvent: "table:rowDblClick"
});

Widget.Table.RowAdjuster = Class.create({
	initialize: function(table, options) {
		this.table = table;
		this.options = Object.extend({
			defaultHeight: 20,
			delay: 0.1,
			pause: 10,
			stop: false,
			rows: 10
		}, options || {});

		this.renderedObserver = this.rendered.bind(this);
		table.observe(table.options.events.rendered, this.renderedObserver);
		this.destroyObserver = this.destroy.bind(this);
		table.observe(table.options.events.destroying, this.destroyObserver);

		this.adjustHandler = this.adjust.bind(this);
	},
	rendered: function() {
		this.timeout = this.adjustHandler.delay(this.options.delay);
		this.y = this.table.options.freeze[1] - 1;
	},
	adjust: function() {
		if(!this.table || this.table.destroyed) return;
		var t = this.table, d = false, f0 = t.options.freeze[0], f1 = t.options.freeze[1] - 1;
		var s = this.y == f1, i = 0, cs = t.getColumnCount(), rs = t.getRowCount();
		for(i = 0; i < this.options.rows; ++i) {
			this.y++;
			if(this.y >= rs) {
				this.y = f1;
				break;
			} else {
				var h = -1;
				var r = t.getRow(this.y, true); //Freeze Part
				for(var x = 0; x < f0; ++x) {
					var c = r.cells[x].down();
					if(c.offsetHeight != c.scrollHeight) {
						h = Math.Max(h, c.scrollHeight);
					}
				}
				var r = t.getRow(this.y, false); //Body Part
				for(var x = f0; x < cs; ++x) {
					var c = r.cells[x].down();
					if(c.offsetHeight != c.scrollHeight) {
						h = c.scrollHeight;
					}
				}
				if(h != -1) {
					t.setHeight(this.y, h);
				}
			}
		}
		this.timeout = this.adjustHandler.delay(this.y == f1 ? this.options.pause : this.options.delay);
	},
	destroy: function() {
		this.table.stopObserving(this.table.options.events.rendered, this.renderedObserver);
		this.table.stopObserving(this.table.options.events.destroying, this.destroyObserver);
	}
});

Widget.Table.Sorter = Class.create({
	initialize: function(table, sortCallback, options) {
		this.table = table;
		this.sortCallback = sortCallback;
		
		this.options = options || {};
		
		var events = Object.extend({
			sorting: Widget.Table.Sorter.sorting,
			sorted: Widget.Table.Sorter.sorted
		}, options ? options.events : {} || {});
		this.options.events = events;
		
		var classes = Object.extend({
			ascending: "ascending",
			descending: "descending"
		}, options ? options.classes : {} || {});
		this.options.classes = classes;
		
		this.sortColumn = this.options.sortColumn || table.options.freeze[0];
		this.sortAscending = this.options.sortAscending ? !!options.sortAscending : true;
		
		this.clickObserver = this.click.bind(this);
		table.observe("click", this.clickObserver);
		this.renderedObserver = this.rendered.bind(this);
		table.observe(table.options.events.rendered, this.renderedObserver);
		this.destroyObserver = this.destroy.bind(this);
		table.observe(table.options.events.destroying, this.destroyObserver);
	},
	click: function(e) {
		var pos = this.table.getLocation(e.element());
		var cell = this.table.getCell(pos.x, pos.y);
		if(!cell) return;
		if(pos.x >= this.table.options.freeze[0] && pos.y < this.table.options.freeze[1]) {
			var asc = this.sortAscending;
			if(pos.x == this.sortColumn) asc = !asc;
			var sorting = this.table.fire(this.options.events.sorting, { column: pos.x, ascending: asc });
			if(sorting.cancelled) return;
			
			this.sortCallback(this.table, pos.x, asc)
			this.sortColumn = pos.x;
			this.sortAscending = asc;
			this.table.refresh();
			
			this.table.fire(this.options.events.sorted, { y: this.selectedRow });
		}
	},
	rendered: function(e) {
		if(this.sortColumn < 0) return;
		this.table.getCell(this.sortColumn, 0).addClassName(this.sortAscending ? this.options.classes.ascending : this.options.classes.descending);
	},
	destroy: function() {
		this.table.stopObserving("click", this.clickObserver);
		this.table.stopObserving(this.table.options.events.destroying, this.destroyObserver);
		this.table = null;
	}
});
Object.extend(Widget.Table.Sorter, {
	sorting: "table:sorting",
	sorted: "table:sorted"
});

/**
 * Plays nicely with Widget.DockManager
 **/
if(typeof Widget == "undefined") Widget = {};
Widget.Panel = Class.create({
	initialize: function(element, options) {
		this.options = Object.extend({
			title: null,
			status: null,
			id: null,
			panelClassName: "panel",
			headerLeftClassName: "headerLeft",
			headerClassName: "header",
			bodyLeftClassName: "bodyLeft",
			bodyClassName: "body",
			footerLeftClassName: "footerLeft",
			footerClassName: "footer",
			dock: null
		}, options || {});

		//Panel
		this.panel = new Element("div", {
			id: this.options.id,
			className: this.options.panelClassName + " dock-container" + (this.options.dock ? " dock-" + this.options.dock : "")
		});

		//Header
		var d = new Element("div", {
			className: this.options.headerLeftClassName + " dock-top dock-container"
		});
		this.header = new Element("div", {
			className: this.options.headerClassName + " dock-fill"
		});
		d.insert(this.header);
		this.panel.insert(d);
		if(this.options.title) {
			this.header.update(this.options.title);
		}

		//Body
		d = new Element("div", {
			className: this.options.bodyLeftClassName + " dock-fill dock-container"
		});
		if(!element) {
			$(document.body).insert(this.panel);
			this.body = new Element("div");
		} else {
			this.element = this.body = $(element);
			this.element.parentNode.replaceChild(this.panel, this.element);
			$w("top left bottom right fill").each(function(dock) {
				this.setDock(dock);
			}.bind(this));

		}
		this.body.addClassName(this.options.bodyClassName);
		this.body.addClassName("dock-fill");
		d.insert(this.body);
		this.panel.insert(d);

		//Footer
		var d = new Element("div", {
			className: this.options.footerLeftClassName + " dock-bottom dock-container"
		});
		this.footer = new Element("div", {
			className: this.options.footerClassName + " dock-fill"
		});
		d.insert(this.footer);
		this.panel.insert(d);
		if(this.options.status) {
			this.footer.update(this.options.status);
		}

	},
	//-- Private functions -----------------------------------------------------
	setDock: function(dock) {
		if(this.element.hasClassName("dock-" + dock)) {
			this.element.removeClassName("dock-" + dock);
			if(!this.options.dock) {
				this.panel.addClassName("dock-" + dock);
			}
			this.oldDock = dock;
		}
	}
});

Widget.PanelManager = Class.create({
	initialize: function() {
		var panels = this.panels = {};
		$$("[class~=panel]").each(function(panel, i) {
			var title = panel.getAttribute("title");
			panels[i] = panels[panel.identify()] = new Widget.Panel(panel, { title: title });
			panel.setAttribute("title", "")
			panel._oldTitle = title;
		});
	}
});/**
 * @fileoverview Widget.Popup <br />
 * Creates a popup, with optional overlay and included IE fix iframe, that can
 * be used for windows, dialog, menus, datepickers and such.<br />
 * <br />
 * Requires Prototype  1.6 (http://www.prototypejs.org) or later <br />
 * and Scriptaculous 1.8 (http://script.aculo.us) or later. <br />
 * <br />
 * Widget.Popup is licensed under the Creative Commons Attribution 2.5 South Africa License<br />
 * (more information at: http://creativecommons.org/licenses/by/2.5/za/)<br />
 * Under this license you are free to<br />
 * - to copy, distribute and transmit the work<br />
 * - to adapt the work<br />
 * However you must<br />
 * - You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).<br />
 * - That is by providing a link back to http://www.eternal.co.za or to the specific script page. This link need not be on the page using the script, or even nescessarily even on the same domain, as long as it's accessible from the site.<br />
 * I'd also like an email telling me where you are using the script, although this is not required. More often than not I will link back to the site using the script.<br />
 * Other than that, you may use this class in any way you like, but don't blame me if things go <br />
 * pear shaped. If you use this library I'd like a mention on your website, but <br />
 * it's not required. If you like it, send me an email. If you find bugs, send <br />
 * me an email. If you don't like it, don't tell me: you'll hurt my feelings. <br />
 * <br />
 * Change History:<br />
 * Version 0.1.2: 28 Jul 2008<br />
 * - Added cursorPosition anchor element option<br />
 * - Added curried observe, stopObserving and fire methods<br />
 * - Added original event to memo of events fired<br />
 * Version 0.1.1: 22 Mar 2008<br />
 * - Fixed destroy to remove overlay div<br />
 * Version 0.1.0: 5 Mar 2008<br />
 * - Initial release with numerous TODO's in the code (containment for one)<br />
 * @class Widget.Popup
 * @version 0.1.0
 * @author Marc Heiligers marc@eternal.co.za http://www.eternal.co.za
 */
if(typeof Widget == "undefined") Widget = {};
/**
 * The Widget.Popup class constructor. <br />
 * @constructor Widget.Popup
 * @param {string|element} element The id of or actual element from which the popup is created.
 * @param {object} [options] An object of options.
 */
Widget.Popup = Class.create({
	initialize: function(element, options) {
		this.element = $(element);
		/**
		 * The default options.anchor object.
		 * @class
		 * @param {string|element} [element] The id of or actual element used as the anchor of the popup. (default: null)
		 * @param {string} [horizontal] The horizontal placement of the popup with respect to the anchor. Can be "before|left|center|right|after" (default: "left")
		 * @param {string} [vertical] The vertical placement of the popup with respect to the anchor. Can be "above|top|middle|bottom|below" (default: "below")
		 * @param {string} [show] The name of an event on the anchor which will cause the popup to be shown (e.g. "click" or "mouseover"). (default: null)
		 */
		var anchor = Object.extend({
			element: null,
			horizontal: "left", //TODO: add cursor as an option
			vertical: "below", //TODO: add cursor as an option
			show: null //eventname: click, mouseover
			//TODO: add delay option - for tooltips
		}, options ? options.anchor || {} : {});
		/**
		 * The default options.hide object.
		 * @class
		 * @param {string} [on] A string representation of when to hide the popup. Can be "leave|click". (default: "leave")
		 * @param {float} [delay] The delay, in seconds, before the popup is hidden. (default: "left")
		 * @param {bool} [anchor] Whether to include the anchor in the hiding action. (default: true)
		 */
		var hide = Object.extend({
			on: "leave", //TODO: document - when the mouse clicks the document | TODO: delay - after a specified delay
			delay: 0.2,
			anchor: true //TODO: click?
		}, options ? options.hide || {} : {});
		/**
		 * The default options.overlay object.
		 * @class
		 * @param {bool} [show] Whether the overlay should be shown. (default: false)
		 * @param {float} [opacity] The opacity of the overlay. (default: 0.5)
		 * @param {string} [background] The background style of the overlay. (default: "#333")
		 */
		var overlay = Object.extend({
			show: false,
			opacity: 0.5,
			background: "#333",
			zIndex: 1023
		}, options ? options.overlay || {} : {});
		/**
		 * The default options.events object.
		 * @class
		 * @param {string} [showing] The name of the showing event. (default: Widget.Popup.showingEvent = "popup:showing")
		 * @param {string} [shown] The name of the shown event. (default: Widget.Popup.shownEvent = "popup:shown")
		 * @param {string} [hiding] The name of the hiding event. (default: Widget.Popup.hidingEvent = "popup:hiding")
		 * @param {string} [hidden] The name of the hidden event. (default: Widget.Popup.hiddenEvent = "popup:hidden")
		 * @param {string} [positioned] The name of the positioned event. (default: Widget.Popup.positionedEvent = "popup:positioned")
		 */
		var events = Object.extend({
			showing: Widget.Popup.showingEvent,
			shown: Widget.Popup.shownEvent,
			hiding: Widget.Popup.hidingEvent,
			hidden: Widget.Popup.hiddenEvent,
			positioned: Widget.Popup.positionedEvent
		}, options ? options.events || {} : {});
		/**
		 * The default options object.
		 * @class
		 * @param {string} [containment] An element (or the viewport) that the popup should remain inside. (default: "viewport")
		 * @param {object} [anchor] The anchor options.
		 * @param {object} [hide] The hide options.
		 * @param {object} [overlay] The overlay options.
		 * @param {object} [events] The events options.
		 */
		this.options = Object.extend({
			containment: "viewport", //TODO: viewport - in the browser viewport | TODO: element - within an element | null/false
			zIndex: 1024
		}, options || {});
		this.options.anchor = anchor;
		this.options.hide = hide;
		this.options.overlay = overlay;
		this.options.events = events;
		if(this.options.anchor.element) this.options.anchor.element = $(this.options.anchor.element);

		if(this.options.overlay.show) {
			this.overlayDiv = new Element("div", {
				style: "background: " + this.options.overlay.background
			}).setOpacity(this.options.overlay.opacity).hide();
			$$("body")[0].insert(this.overlayDiv);
			this.overlay = new Widget.Popup(this.overlayDiv, {
				anchor: {
					element: document,
					vertical: "top",
					horizontal: "left",
					width: true,
					height: true
				},
				hide: {
					on: false
				},
				zIndex: this.options.overlay.zIndex
			});
		}

		if(Prototype.Browser.IE) { // && !Prototype.Browser.IE7 - still covered by SVG
			this.iframe = new Element("iframe", {
				style: "position: absolute; z-index: " + (this.options.zIndex - 1) + ";"
			}).setOpacity(0).hide();
			$$("body")[0].insert(this.iframe);
		}

		this.element.hide();
		this.element.setStyle({
			position: "absolute",
			zIndex: this.options.zIndex
		});
		$$("body")[0].insert(this.element);

		//Events
		this.mouseoverObserver = this.mouseover.bind(this);
		this.mouseoutObserver = this.mouseout.bind(this);
		if(this.options.anchor.element && this.options.hide.anchor.element != Widget.Popup.mouseCursor) {
			this.options.anchor.element = $(this.options.anchor.element);
			if(this.options.hide.anchor && this.options.hide.on == "leave") {
				this.options.anchor.element.observe("mouseover", this.mouseoverObserver);
				this.options.anchor.element.observe("mouseout", this.mouseoutObserver);
			}
		}

		if(this.options.hide.on == "leave") {
			this.element.observe("mouseover", this.mouseoverObserver);
			this.element.observe("mouseout", this.mouseoutObserver);
		}

		if(this.options.hide.on == "click") {
			this.element.observe("click", this.mouseoutObserver);
		}

		if(this.options.anchor.show && this.options.anchor.element) {
			this.options.anchor.element.observe(this.options.anchor.show, this.show.bind(this));
		}

		this.observe = Event.observe.curry(this.element);
		this.stopObserving = Event.stopObserving.curry(this.element);
		this.fire = Event.fire.curry(this.element);

		Event.observe(window, "resize", this.position.bind(this));
	},

	show: function(event) {
		var e = this.fire(this.options.events.showing, { popup: this, event: event });
		if(!e.stopped) {
			this.position(event);
			if(this.overlay) {
				this.overlay.show();
			}
			if(this.iframe) {
				this.iframe.show();
			}
			this.element.show();
			this.visible = true;
		}
		this.fire(this.options.events.shown, this, { popup: this, event: event });
	},

	hide: function(event) {
		var e = this.fire(this.options.events.hiding, this, { popup: this, event: event });
		if(!e.stopped) {
			this.element.hide();
			if(this.iframe) {
				this.iframe.hide();
			}
			if(this.overlay) {
				this.overlay.hide();
			}
			this.visible = false;
		}
		this.fire(this.options.events.hidden, this, { popup: this, event: event });
	},

	toggle: function(event) {
		if(this.visible) {
			this.hide(event);
		} else {
			this.show(event);
		}
	},

	position: function(event) {
		var d = this.sizeTo(this.element, this.options.anchor, event);
		var p = this.positionTo(this.element, this.options.anchor, event, d);
		this.contain(this.element, this.options.containment, event, d, p);

		if(this.iframe) {
			this.iframe.style.top = Element.getStyle(this.element, "top");
			this.iframe.style.left = Element.getStyle(this.element, "left");
			var el = this.element.getDimensions();
			el.width += this.margin(this.element, "left") + this.margin(this.element, "right") + this.border(this.element, "left") + this.border(this.element, "right");
			el.height += this.margin(this.element, "top") + this.margin(this.element, "bottom") + this.border(this.element, "top") + this.border(this.element, "bottom");
			this.iframe.style.width = Math.max(el.width, 0) + "px";
			this.iframe.style.height = Math.max(el.height, 0) + "px";
		}
		this.element.fire(this.options.events.positioned, this);
	},

	sizeTo: function(element, anchor, event) {
		if(!element || !anchor || !anchor.element || anchor.element == Widget.Popup.cursorPosition) return;
		var el = element.getDimensions();
		var mx = this.margin(element, "left") + this.margin(element, "right") + this.border(element, "left") + this.border(element, "right");
		var my = this.margin(element, "top") + this.margin(element, "bottom") + this.border(element, "top") + this.border(element, "bottom");
		var pos, dim;
		if(anchor.element == document) {
			var b = $$("body")[0];
			dim = document.viewport.getDimensions();
		} else {
			dim = anchor.element.getDimensions();
		}

		var edim = element.getDimensions();
		if(anchor.width) {
			edim.width = Math.max(dim.width - mx, 0);
			element.setStyle({ width: edim.width + "px" });
		}

		if(anchor.height) {
			edim.height = Math.max(dim.height - my, 0);
			element.setStyle({ height: edim.height + "px" });
		}

		return edim;
	},

	positionTo: function(element, anchor, event, dimensions) {
		if(!element || !anchor || !anchor.element) return;
		if(!dimensions) dimensions = element.getDimensions();
		dimensions.width += this.margin(element, "left") + this.margin(element, "right") + this.border(element, "left") + this.border(element, "right");
		dimensions.height += this.margin(element, "top") + this.margin(element, "bottom") + this.border(element, "top") + this.border(element, "bottom");
		var pos, dim;
		if(anchor.element == document) {
			var b = $$("body")[0];
			pos = { top: b.scrollTop, left: b.scrollLeft };
			dim = document.viewport.getDimensions();
		} else if (anchor.element == Widget.Popup.cursorPosition) {
			pos = this.cursor(event);
			dim = { width: 1, height: 1 };
		} else {
			pos = anchor.element.cumulativeOffset();
			var sca = anchor.element.cumulativeScrollOffset();
			var scv = document.viewport.getScrollOffsets();
			pos.top += scv.top - sca.top;
			pos.left += scv.left - sca.left;
			dim = anchor.element.getDimensions();
		}

		var p = element.cumulativeOffset();
		switch(anchor.horizontal) {
			case "farLeft":
			case "before":
				p.left = pos.left - dimensions.width;
				break;
			case "left":
				p.left = pos.left;
				break;
			case "center":
			case "middle":
				p.left = pos.left + (dim.width - dimensions.width) / 2;
				break;
			case "right":
				p.left = pos.left + dim.width - dimensions.width;
				break;
			case "farRight":
			case "after":
				p.left = pos.left + dim.width;
				break;
		}
		element.setStyle({ left: p.left + "px" });

		switch(anchor.vertical) {
			case "farTop":
			case "above":
				p.top = pos.top - dimensions.height;
				break;
			case "top":
				p.top = pos.top;
				break;
			case "middle":
			case "center":
				p.top = pos.top + (dim.height - dimensions.height) / 2;
				break;
			case "bottom":
				p.top = pos.top + dim.height - dimensions.height;
				break;
			case "farBottom":
			case "below":
				p.top = pos.top + dim.height;
				break;
		}
		element.setStyle({ top: p.top + "px" });

		return p;
	},

	contain: function(element, containment, event, dimensions, position) {
		if(!dimensions) dimensions = element.getDimensions();
		if(!position) position = element.cumulativeOffset();
		switch(containment) {
			case "viewport":
				var vp = document.viewport.getDimensions();
				var sc = document.viewport.getScrollOffsets();

				if(position.top < sc.top) {
					element.setStyle({ top: sc.top + "px" });
				}
				if(position.top + dimensions.height > sc.top + vp.height) {
					element.setStyle({ top: sc.top + vp.height - dimensions.height + "px" });
				}
				if(position.left < sc.left) {
					element.setStyle({ left: sc.left + "px" });
				}
				if(position.left + dimensions.width > sc.left + vp.width) {
					element.setStyle({ left: sc.left + vp.width - dimensions.width + "px" });
				}
		}
		//TODO: containment
	},

	margin: function(e, s) {
		var margin = parseInt(e.getStyle("margin-" + s) || 0);
		if(isNaN(margin)) margin = 0;
		return margin;
	},

	border: function(e, s) {
		var border = parseInt(e.getStyle("border-" + s + "-width") || 0);
		if(isNaN(border)) border = 0;
		return border;
	},

	cursor: function(event) {
		if(event.pageX) return { top: event.pageY, left: event.pageX };
		return { top: event.clientY, left: event.clientX };
	},

	mouseout: function() {
		this.mouseover(); //Kill the timeout
		this.hideTimeout = this.hide.bind(this).delay(this.options.hide.delay);
	},

	mouseover: function() {
		if(this.hideTimeout) {
			clearTimeout(this.hideTimeout);
		}
		this.hideTimeout = null;
	},

	destroy: function() {
		if(this.options.hide.on == "leave") {
			if(this.options.hide.anchor && this.options.anchor.element) {
				this.options.anchor.element.stopObserving("mouseover", this.mouseoverObserver);
				this.options.anchor.element.stopObserving("mouseout", this.mouseoutObserver);
			}
			this.element.stopObserving("mouseover", this.mouseoverObserver);
			this.element.stopObserving("mouseout", this.mouseoutObserver);
		}
		if(this.overlay) {
			this.overlay.destroy();
			this.overlayDiv.remove();
		}
		if(this.iframe) {
			this.iframe.remove();
		}
		this.mouseover(); //Kill the timeout
	}
});
Object.extend(Widget.Popup, {
	showingEvent: "popup:showing",
	shownEvent: "popup:shown",
	hidingEvent: "popup:hiding",
	hiddenEvent: "popup:hidden",
	positionedEvent: "popup:positioned",
	cursorPosition: {}
});if(typeof Widget == "undefined") Widget = {};
//TODO: Submenus
//TODO: Auto wire-up show events on button <-- done in popup
//TODO: Add seperators
//TODO: Add events
Widget.Menu = Class.create({
	initialize: function(/* [element], [options] */) {
		var options = {};
		this.element = null;
		if(arguments.length == 1) {
			if(Object.isString(arguments[0]) || Object.isElement(arguments[0])) {
				this.element = $(arguments[0]);
			} else {
				options = arguments[0];
			}
		} else if(arguments.length == 2) {
			this.element = $(arguments[0]);
			options = arguments[1];
		}

		var classNames = Object.extend({
			menu: "menu",
			item: "menu-item",
			title: "menu-title"
		}, options.classNames || {});
		var attributes = Object.extend({
			menu: null,
			item: null,
			title: null
		}, options.attributes || {});
		this.options = Object.extend({
			selector: "##{id} > .#{className}",
			itemTag: "div",
			activator: null,
			popup: null
		}, options);
		var events = Object.extend({
			mouseover: Widget.Menu.mouseoverEvent,
			mouseout: Widget.Menu.mouseoutEvent,
			mousedown: Widget.Menu.mousedownEvent,
			mouseup: Widget.Menu.mouseupEvent,
			click: Widget.Menu.clickEvent
		}, options ? options.events || {} : {});
		this.options.classNames = classNames;
		this.options.attributes = attributes;
		this.options.events = events;
		this.options.activator = options.activator || {};
		//TODO: ensure that overrides remain
		this.options.activator.events = events;

		if(!this.element) {
			this.element = new Element("div", this.options.attributes.menu);
		}
		this.element.addClassName(this.options.classNames.menu);

		this.activator = new Widget.Activator(
			this.options.selector.interpolate({ id: this.element.identify(), className: this.options.classNames.item }),
			Object.extend({ container: this.element }, this.options.activator)
		);
		this.popup = new Widget.Popup(this.element, this.options.popup);

		this.show = this.popup.show.bind(this.popup);
		this.hide = this.popup.hide.bind(this.popup);

		//TODO: improve event handling
		this.observe = this.activator.observe.bind(this.activator);
		this.stopObserving = this.activator.stopObserving.bind(this.activator);

		//Activator methods
		this.setSelected = this.activator.setSelected.bind(this.activator);
		this.setEnabled = this.activator.setEnabled.bind(this.activator);
		this.selectAll = this.activator.selectAll.bind(this.activator);
		this.enableAll = this.activator.enableAll.bind(this.activator);
		this.getSelected = this.activator.getSelected.bind(this.activator);
		this.isSelected = this.activator.isSelected.bind(this.activator);

		//TODO: improve item getting. this does not get titles
		this.getItem = this.activator.getElement.bind(this.activator);

		this.observe(events.mouseup, this.itemClicked.bind(this));
	},
	insertItem: function(label, observer, position) {
		//TODO: Fix. Doesn't count titles
		if(!Object.isFunction(observer)) {
			position = observer;
			observer = null;
		}
		var existing = this.activator.getElement(position);
		if(!existing) {
			return this.addItem(label, observer);
		}
		var item = this.createItem(label, observer);
		existing.insert({ before: item });
		this.activator.add(item);
		return item;
	},
	addItem: function(label, observer) {
		var item = this.createItem(label, observer);
		this.element.insert(item);
		this.activator.add(item);
		return item;
	},
	createItem: function(label, observer) {
		var item = new Element(
			this.options.itemTag,
			Object.extend({
				className: this.options.classNames.item
			}, this.options.attributes.item || {})
		);
		if(label) item.update(label);
		if(observer) {
			item.observe("click", observer);
		}
		return item;
	},
	addTitle: function(label) {
		var item = new Element(
			this.options.itemTag,
			Object.extend({
				className: this.options.classNames.title
			}, this.options.attributes.title || {})
		);
		if(label) item.update(label);
		this.element.insert(item);
		return item;
	},
	itemClicked: function(e) {
		this.popup.hide();
	},
	clear: function() {
		this.activator.remove("*");
		this.element.update("");
	},
	destroy: function() {
		this.clear();
		this.activator.destroy();
		this.popup.destroy();
	}
});
Object.extend(Widget.Menu, {
	/**
	 * Name of mouseover event fired by the menu.
	 */
	mouseoverEvent: "menu:mouseover",
	/**
	 * Name of mouseout event fired by the menu.
	 */
	mouseoutEvent: "menu:mouseout",
	/**
	 * Name of mousedown event fired by the menu.
	 */
	mousedownEvent: "menu:mousedown",
	/**
	 * Name of mouseup event fired by the menu.
	 */
	mouseupEvent: "menu:mouseup",
	/**
	 * Name of click event fired by the menu.
	 */
	clickEvent: "menu:click"
});
if(typeof Widget == "undefined") Widget = {};
//TODO: Add overlay options - overlay a div over a certain element
//TODO: Add events
//TODO: Add docs
Widget.Task = Class.create({
    initialize: function(callback, options) {
        this.callback = callback;
        this.options = Object.extend({
            autoStart: true,
            timeout: 100,
            onProgress: null,
            onComplete: null
        }, options || {});
        if(this.options.autoStart) {
            this.start();
        }
    },
    start: function() {
        if(this.running) return;
        this.running = true;
        this.run();
    },
    stop: function() {
        clearTimeout(this.timeout);
        this.running = false;
    },
    toggle: function() {
        if(this.running) {
            this.stop();
        } else {
            this.start();
        }
    },
    run: function() {
        if(this.callback()) {
            if(this.options.onProgress) {
                this.options.onProgress();
            }
            this.timeout = setTimeout(this.run.bind(this), this.options.timeout);
        } else {
            this.running = false;
            if(this.options.onComplete) {
                this.options.onComplete();
            }
        }
    }
});
