text: // Copyright (c) 2005-2009 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) + .5;
      },
      reverse: function(pos) {
        return 1-pos;
      },
      flicker: function(pos) {
        var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
        return pos > 1 ? 1 : pos;
      },
      wobble: function(pos) {
        return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
      },
      pulse: function(pos, pulses) {
        return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
      },
      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, options) {
      element = $(element);
      effect  = (effect || 'appear').toLowerCase();
      
      return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({
        queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
      }, 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;
  
      return this.instances.get(queueName) ||
        this.instances.set(queueName, new Effect.ScopedQueue());
    }
  };
  Effect.Queue = Effect.Queues.get('global');
  
  Effect.Base = Class.create({
    position: null,
    start: function(options) {
      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;
  
      this.render = (function() {
        function dispatch(effect, eventName) {
          if (effect.options[eventName + 'Internal'])
            effect.options[eventName + 'Internal'](effect);
          if (effect.options[eventName])
            effect.options[eventName](effect);
        }
  
        return function(pos) {
          if (this.state === "idle") {
            this.state = "running";
            dispatch(this, 'beforeSetup');
            if (this.setup) this.setup();
            dispatch(this, 'afterSetup');
          }
          if (this.state === "running") {
            pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
            this.position = pos;
            dispatch(this, 'beforeUpdate');
            if (this.update) this.update(pos);
            dispatch(this, '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.set(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();
  
    if (options.offset) elementOffsets[1] += options.offset;
  
    return new Effect.Tween(null,
      scrollOffsets.top,
      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 options = Object.extend({
      distance: 20,
      duration: 0.5
    }, arguments[1] || {});
    var distance = parseFloat(options.distance);
    var split = parseFloat(options.duration) / 10.0;
    var oldStyle = {
      top: element.getStyle('top'),
      left: element.getStyle('left') };
      return new Effect.Move(element,
        { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
      new Effect.Move(effect.element,
        { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
      new Effect.Move(effect.element,
        { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
      new Effect.Move(effect.element,
        { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
      new Effect.Move(effect.element,
        { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
      new Effect.Move(effect.element,
        { x: -distance, y: 0, duration: split, 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] || { },
      oldOpacity = element.getInlineOpacity(),
      transition = options.transition || Effect.Transitions.linear,
      reverser   = function(pos){
        return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
      };
  
    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){
        track = $H(track);
        var data = track.values().first();
        this.tracks.push($H({
          ids:     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 ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
          var elements = [$(ids) || $$(ids)].flatten();
          return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, 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.set(property, style[property]);
    });
  
    if (Prototype.Browser.IE && this.include('opacity'))
      styleRules.set('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(results, property) {
        results[property] = css[property];
        return results;
      });
      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);