PageRenderTime 141ms CodeModel.GetById 30ms app.highlight 101ms RepoModel.GetById 1ms app.codeStats 0ms

/public/effects.js

http://github.com/mtravers/wuwei
JavaScript | 1121 lines | 1014 code | 83 blank | 24 comment | 156 complexity | 5b233f9dbd14a380f6c74afb775c91f3 MD5 | raw file
   1// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
   2// Contributors:
   3//  Justin Palmer (http://encytemedia.com/)
   4//  Mark Pilgrim (http://diveintomark.org/)
   5//  Martin Bialasinki
   6//
   7// script.aculo.us is freely distributable under the terms of an MIT-style license.
   8// For details, see the script.aculo.us web site: http://script.aculo.us/
   9
  10// converts rgb() and #xxx to #xxxxxx format,
  11// returns self (or first argument) if not convertable
  12String.prototype.parseColor = function() {
  13  var color = '#';
  14  if (this.slice(0,4) == 'rgb(') {
  15    var cols = this.slice(4,this.length-1).split(',');
  16    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
  17  } else {
  18    if (this.slice(0,1) == '#') {
  19      if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
  20      if (this.length==7) color = this.toLowerCase();
  21    }
  22  }
  23  return (color.length==7 ? color : (arguments[0] || this));
  24};
  25
  26/*--------------------------------------------------------------------------*/
  27
  28Element.collectTextNodes = function(element) {
  29  return $A($(element).childNodes).collect( function(node) {
  30    return (node.nodeType==3 ? node.nodeValue :
  31      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
  32  }).flatten().join('');
  33};
  34
  35Element.collectTextNodesIgnoreClass = function(element, className) {
  36  return $A($(element).childNodes).collect( function(node) {
  37    return (node.nodeType==3 ? node.nodeValue :
  38      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
  39        Element.collectTextNodesIgnoreClass(node, className) : ''));
  40  }).flatten().join('');
  41};
  42
  43Element.setContentZoom = function(element, percent) {
  44  element = $(element);
  45  element.setStyle({fontSize: (percent/100) + 'em'});
  46  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
  47  return element;
  48};
  49
  50Element.getInlineOpacity = function(element){
  51  return $(element).style.opacity || '';
  52};
  53
  54Element.forceRerendering = function(element) {
  55  try {
  56    element = $(element);
  57    var n = document.createTextNode(' ');
  58    element.appendChild(n);
  59    element.removeChild(n);
  60  } catch(e) { }
  61};
  62
  63/*--------------------------------------------------------------------------*/
  64
  65var Effect = {
  66  _elementDoesNotExistError: {
  67    name: 'ElementDoesNotExistError',
  68    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  69  },
  70  Transitions: {
  71    linear: Prototype.K,
  72    sinoidal: function(pos) {
  73      return (-Math.cos(pos*Math.PI)/2) + .5;
  74    },
  75    reverse: function(pos) {
  76      return 1-pos;
  77    },
  78    flicker: function(pos) {
  79      var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
  80      return pos > 1 ? 1 : pos;
  81    },
  82    wobble: function(pos) {
  83      return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
  84    },
  85    pulse: function(pos, pulses) {
  86      return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
  87    },
  88    spring: function(pos) {
  89      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
  90    },
  91    none: function(pos) {
  92      return 0;
  93    },
  94    full: function(pos) {
  95      return 1;
  96    }
  97  },
  98  DefaultOptions: {
  99    duration:   1.0,   // seconds
 100    fps:        100,   // 100= assume 66fps max.
 101    sync:       false, // true for combining
 102    from:       0.0,
 103    to:         1.0,
 104    delay:      0.0,
 105    queue:      'parallel'
 106  },
 107  tagifyText: function(element) {
 108    var tagifyStyle = 'position:relative';
 109    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
 110
 111    element = $(element);
 112    $A(element.childNodes).each( function(child) {
 113      if (child.nodeType==3) {
 114        child.nodeValue.toArray().each( function(character) {
 115          element.insertBefore(
 116            new Element('span', {style: tagifyStyle}).update(
 117              character == ' ' ? String.fromCharCode(160) : character),
 118              child);
 119        });
 120        Element.remove(child);
 121      }
 122    });
 123  },
 124  multiple: function(element, effect) {
 125    var elements;
 126    if (((typeof element == 'object') ||
 127        Object.isFunction(element)) &&
 128       (element.length))
 129      elements = element;
 130    else
 131      elements = $(element).childNodes;
 132
 133    var options = Object.extend({
 134      speed: 0.1,
 135      delay: 0.0
 136    }, arguments[2] || { });
 137    var masterDelay = options.delay;
 138
 139    $A(elements).each( function(element, index) {
 140      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
 141    });
 142  },
 143  PAIRS: {
 144    'slide':  ['SlideDown','SlideUp'],
 145    'blind':  ['BlindDown','BlindUp'],
 146    'appear': ['Appear','Fade']
 147  },
 148  toggle: function(element, effect, options) {
 149    element = $(element);
 150    effect  = (effect || 'appear').toLowerCase();
 151    
 152    return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({
 153      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
 154    }, options || {}));
 155  }
 156};
 157
 158Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
 159
 160/* ------------- core effects ------------- */
 161
 162Effect.ScopedQueue = Class.create(Enumerable, {
 163  initialize: function() {
 164    this.effects  = [];
 165    this.interval = null;
 166  },
 167  _each: function(iterator) {
 168    this.effects._each(iterator);
 169  },
 170  add: function(effect) {
 171    var timestamp = new Date().getTime();
 172
 173    var position = Object.isString(effect.options.queue) ?
 174      effect.options.queue : effect.options.queue.position;
 175
 176    switch(position) {
 177      case 'front':
 178        // move unstarted effects after this effect
 179        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
 180            e.startOn  += effect.finishOn;
 181            e.finishOn += effect.finishOn;
 182          });
 183        break;
 184      case 'with-last':
 185        timestamp = this.effects.pluck('startOn').max() || timestamp;
 186        break;
 187      case 'end':
 188        // start effect after last queued effect has finished
 189        timestamp = this.effects.pluck('finishOn').max() || timestamp;
 190        break;
 191    }
 192
 193    effect.startOn  += timestamp;
 194    effect.finishOn += timestamp;
 195
 196    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
 197      this.effects.push(effect);
 198
 199    if (!this.interval)
 200      this.interval = setInterval(this.loop.bind(this), 15);
 201  },
 202  remove: function(effect) {
 203    this.effects = this.effects.reject(function(e) { return e==effect });
 204    if (this.effects.length == 0) {
 205      clearInterval(this.interval);
 206      this.interval = null;
 207    }
 208  },
 209  loop: function() {
 210    var timePos = new Date().getTime();
 211    for(var i=0, len=this.effects.length;i<len;i++)
 212      this.effects[i] && this.effects[i].loop(timePos);
 213  }
 214});
 215
 216Effect.Queues = {
 217  instances: $H(),
 218  get: function(queueName) {
 219    if (!Object.isString(queueName)) return queueName;
 220
 221    return this.instances.get(queueName) ||
 222      this.instances.set(queueName, new Effect.ScopedQueue());
 223  }
 224};
 225Effect.Queue = Effect.Queues.get('global');
 226
 227Effect.Base = Class.create({
 228  position: null,
 229  start: function(options) {
 230    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
 231    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
 232    this.currentFrame = 0;
 233    this.state        = 'idle';
 234    this.startOn      = this.options.delay*1000;
 235    this.finishOn     = this.startOn+(this.options.duration*1000);
 236    this.fromToDelta  = this.options.to-this.options.from;
 237    this.totalTime    = this.finishOn-this.startOn;
 238    this.totalFrames  = this.options.fps*this.options.duration;
 239
 240    this.render = (function() {
 241      function dispatch(effect, eventName) {
 242        if (effect.options[eventName + 'Internal'])
 243          effect.options[eventName + 'Internal'](effect);
 244        if (effect.options[eventName])
 245          effect.options[eventName](effect);
 246      }
 247
 248      return function(pos) {
 249        if (this.state === "idle") {
 250          this.state = "running";
 251          dispatch(this, 'beforeSetup');
 252          if (this.setup) this.setup();
 253          dispatch(this, 'afterSetup');
 254        }
 255        if (this.state === "running") {
 256          pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
 257          this.position = pos;
 258          dispatch(this, 'beforeUpdate');
 259          if (this.update) this.update(pos);
 260          dispatch(this, 'afterUpdate');
 261        }
 262      };
 263    })();
 264
 265    this.event('beforeStart');
 266    if (!this.options.sync)
 267      Effect.Queues.get(Object.isString(this.options.queue) ?
 268        'global' : this.options.queue.scope).add(this);
 269  },
 270  loop: function(timePos) {
 271    if (timePos >= this.startOn) {
 272      if (timePos >= this.finishOn) {
 273        this.render(1.0);
 274        this.cancel();
 275        this.event('beforeFinish');
 276        if (this.finish) this.finish();
 277        this.event('afterFinish');
 278        return;
 279      }
 280      var pos   = (timePos - this.startOn) / this.totalTime,
 281          frame = (pos * this.totalFrames).round();
 282      if (frame > this.currentFrame) {
 283        this.render(pos);
 284        this.currentFrame = frame;
 285      }
 286    }
 287  },
 288  cancel: function() {
 289    if (!this.options.sync)
 290      Effect.Queues.get(Object.isString(this.options.queue) ?
 291        'global' : this.options.queue.scope).remove(this);
 292    this.state = 'finished';
 293  },
 294  event: function(eventName) {
 295    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
 296    if (this.options[eventName]) this.options[eventName](this);
 297  },
 298  inspect: function() {
 299    var data = $H();
 300    for(property in this)
 301      if (!Object.isFunction(this[property])) data.set(property, this[property]);
 302    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
 303  }
 304});
 305
 306Effect.Parallel = Class.create(Effect.Base, {
 307  initialize: function(effects) {
 308    this.effects = effects || [];
 309    this.start(arguments[1]);
 310  },
 311  update: function(position) {
 312    this.effects.invoke('render', position);
 313  },
 314  finish: function(position) {
 315    this.effects.each( function(effect) {
 316      effect.render(1.0);
 317      effect.cancel();
 318      effect.event('beforeFinish');
 319      if (effect.finish) effect.finish(position);
 320      effect.event('afterFinish');
 321    });
 322  }
 323});
 324
 325Effect.Tween = Class.create(Effect.Base, {
 326  initialize: function(object, from, to) {
 327    object = Object.isString(object) ? $(object) : object;
 328    var args = $A(arguments), method = args.last(),
 329      options = args.length == 5 ? args[3] : null;
 330    this.method = Object.isFunction(method) ? method.bind(object) :
 331      Object.isFunction(object[method]) ? object[method].bind(object) :
 332      function(value) { object[method] = value };
 333    this.start(Object.extend({ from: from, to: to }, options || { }));
 334  },
 335  update: function(position) {
 336    this.method(position);
 337  }
 338});
 339
 340Effect.Event = Class.create(Effect.Base, {
 341  initialize: function() {
 342    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
 343  },
 344  update: Prototype.emptyFunction
 345});
 346
 347Effect.Opacity = Class.create(Effect.Base, {
 348  initialize: function(element) {
 349    this.element = $(element);
 350    if (!this.element) throw(Effect._elementDoesNotExistError);
 351    // make this work on IE on elements without 'layout'
 352    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
 353      this.element.setStyle({zoom: 1});
 354    var options = Object.extend({
 355      from: this.element.getOpacity() || 0.0,
 356      to:   1.0
 357    }, arguments[1] || { });
 358    this.start(options);
 359  },
 360  update: function(position) {
 361    this.element.setOpacity(position);
 362  }
 363});
 364
 365Effect.Move = Class.create(Effect.Base, {
 366  initialize: function(element) {
 367    this.element = $(element);
 368    if (!this.element) throw(Effect._elementDoesNotExistError);
 369    var options = Object.extend({
 370      x:    0,
 371      y:    0,
 372      mode: 'relative'
 373    }, arguments[1] || { });
 374    this.start(options);
 375  },
 376  setup: function() {
 377    this.element.makePositioned();
 378    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
 379    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
 380    if (this.options.mode == 'absolute') {
 381      this.options.x = this.options.x - this.originalLeft;
 382      this.options.y = this.options.y - this.originalTop;
 383    }
 384  },
 385  update: function(position) {
 386    this.element.setStyle({
 387      left: (this.options.x  * position + this.originalLeft).round() + 'px',
 388      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
 389    });
 390  }
 391});
 392
 393// for backwards compatibility
 394Effect.MoveBy = function(element, toTop, toLeft) {
 395  return new Effect.Move(element,
 396    Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
 397};
 398
 399Effect.Scale = Class.create(Effect.Base, {
 400  initialize: function(element, percent) {
 401    this.element = $(element);
 402    if (!this.element) throw(Effect._elementDoesNotExistError);
 403    var options = Object.extend({
 404      scaleX: true,
 405      scaleY: true,
 406      scaleContent: true,
 407      scaleFromCenter: false,
 408      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
 409      scaleFrom: 100.0,
 410      scaleTo:   percent
 411    }, arguments[2] || { });
 412    this.start(options);
 413  },
 414  setup: function() {
 415    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
 416    this.elementPositioning = this.element.getStyle('position');
 417
 418    this.originalStyle = { };
 419    ['top','left','width','height','fontSize'].each( function(k) {
 420      this.originalStyle[k] = this.element.style[k];
 421    }.bind(this));
 422
 423    this.originalTop  = this.element.offsetTop;
 424    this.originalLeft = this.element.offsetLeft;
 425
 426    var fontSize = this.element.getStyle('font-size') || '100%';
 427    ['em','px','%','pt'].each( function(fontSizeType) {
 428      if (fontSize.indexOf(fontSizeType)>0) {
 429        this.fontSize     = parseFloat(fontSize);
 430        this.fontSizeType = fontSizeType;
 431      }
 432    }.bind(this));
 433
 434    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
 435
 436    this.dims = null;
 437    if (this.options.scaleMode=='box')
 438      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
 439    if (/^content/.test(this.options.scaleMode))
 440      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
 441    if (!this.dims)
 442      this.dims = [this.options.scaleMode.originalHeight,
 443                   this.options.scaleMode.originalWidth];
 444  },
 445  update: function(position) {
 446    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
 447    if (this.options.scaleContent && this.fontSize)
 448      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
 449    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
 450  },
 451  finish: function(position) {
 452    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
 453  },
 454  setDimensions: function(height, width) {
 455    var d = { };
 456    if (this.options.scaleX) d.width = width.round() + 'px';
 457    if (this.options.scaleY) d.height = height.round() + 'px';
 458    if (this.options.scaleFromCenter) {
 459      var topd  = (height - this.dims[0])/2;
 460      var leftd = (width  - this.dims[1])/2;
 461      if (this.elementPositioning == 'absolute') {
 462        if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
 463        if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
 464      } else {
 465        if (this.options.scaleY) d.top = -topd + 'px';
 466        if (this.options.scaleX) d.left = -leftd + 'px';
 467      }
 468    }
 469    this.element.setStyle(d);
 470  }
 471});
 472
 473Effect.Highlight = Class.create(Effect.Base, {
 474  initialize: function(element) {
 475    this.element = $(element);
 476    if (!this.element) throw(Effect._elementDoesNotExistError);
 477    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
 478    this.start(options);
 479  },
 480  setup: function() {
 481    // Prevent executing on elements not in the layout flow
 482    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
 483    // Disable background image during the effect
 484    this.oldStyle = { };
 485    if (!this.options.keepBackgroundImage) {
 486      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
 487      this.element.setStyle({backgroundImage: 'none'});
 488    }
 489    if (!this.options.endcolor)
 490      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
 491    if (!this.options.restorecolor)
 492      this.options.restorecolor = this.element.getStyle('background-color');
 493    // init color calculations
 494    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
 495    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));
 496  },
 497  update: function(position) {
 498    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
 499      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
 500  },
 501  finish: function() {
 502    this.element.setStyle(Object.extend(this.oldStyle, {
 503      backgroundColor: this.options.restorecolor
 504    }));
 505  }
 506});
 507
 508Effect.ScrollTo = function(element) {
 509  var options = arguments[1] || { },
 510  scrollOffsets = document.viewport.getScrollOffsets(),
 511  elementOffsets = $(element).cumulativeOffset();
 512
 513  if (options.offset) elementOffsets[1] += options.offset;
 514
 515  return new Effect.Tween(null,
 516    scrollOffsets.top,
 517    elementOffsets[1],
 518    options,
 519    function(p){ scrollTo(scrollOffsets.left, p.round()); }
 520  );
 521};
 522
 523/* ------------- combination effects ------------- */
 524
 525Effect.Fade = function(element) {
 526  element = $(element);
 527  var oldOpacity = element.getInlineOpacity();
 528  var options = Object.extend({
 529    from: element.getOpacity() || 1.0,
 530    to:   0.0,
 531    afterFinishInternal: function(effect) {
 532      if (effect.options.to!=0) return;
 533      effect.element.hide().setStyle({opacity: oldOpacity});
 534    }
 535  }, arguments[1] || { });
 536  return new Effect.Opacity(element,options);
 537};
 538
 539Effect.Appear = function(element) {
 540  element = $(element);
 541  var options = Object.extend({
 542  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
 543  to:   1.0,
 544  // force Safari to render floated elements properly
 545  afterFinishInternal: function(effect) {
 546    effect.element.forceRerendering();
 547  },
 548  beforeSetup: function(effect) {
 549    effect.element.setOpacity(effect.options.from).show();
 550  }}, arguments[1] || { });
 551  return new Effect.Opacity(element,options);
 552};
 553
 554Effect.Puff = function(element) {
 555  element = $(element);
 556  var oldStyle = {
 557    opacity: element.getInlineOpacity(),
 558    position: element.getStyle('position'),
 559    top:  element.style.top,
 560    left: element.style.left,
 561    width: element.style.width,
 562    height: element.style.height
 563  };
 564  return new Effect.Parallel(
 565   [ new Effect.Scale(element, 200,
 566      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
 567     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
 568     Object.extend({ duration: 1.0,
 569      beforeSetupInternal: function(effect) {
 570        Position.absolutize(effect.effects[0].element);
 571      },
 572      afterFinishInternal: function(effect) {
 573         effect.effects[0].element.hide().setStyle(oldStyle); }
 574     }, arguments[1] || { })
 575   );
 576};
 577
 578Effect.BlindUp = function(element) {
 579  element = $(element);
 580  element.makeClipping();
 581  return new Effect.Scale(element, 0,
 582    Object.extend({ scaleContent: false,
 583      scaleX: false,
 584      restoreAfterFinish: true,
 585      afterFinishInternal: function(effect) {
 586        effect.element.hide().undoClipping();
 587      }
 588    }, arguments[1] || { })
 589  );
 590};
 591
 592Effect.BlindDown = function(element) {
 593  element = $(element);
 594  var elementDimensions = element.getDimensions();
 595  return new Effect.Scale(element, 100, Object.extend({
 596    scaleContent: false,
 597    scaleX: false,
 598    scaleFrom: 0,
 599    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
 600    restoreAfterFinish: true,
 601    afterSetup: function(effect) {
 602      effect.element.makeClipping().setStyle({height: '0px'}).show();
 603    },
 604    afterFinishInternal: function(effect) {
 605      effect.element.undoClipping();
 606    }
 607  }, arguments[1] || { }));
 608};
 609
 610Effect.SwitchOff = function(element) {
 611  element = $(element);
 612  var oldOpacity = element.getInlineOpacity();
 613  return new Effect.Appear(element, Object.extend({
 614    duration: 0.4,
 615    from: 0,
 616    transition: Effect.Transitions.flicker,
 617    afterFinishInternal: function(effect) {
 618      new Effect.Scale(effect.element, 1, {
 619        duration: 0.3, scaleFromCenter: true,
 620        scaleX: false, scaleContent: false, restoreAfterFinish: true,
 621        beforeSetup: function(effect) {
 622          effect.element.makePositioned().makeClipping();
 623        },
 624        afterFinishInternal: function(effect) {
 625          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
 626        }
 627      });
 628    }
 629  }, arguments[1] || { }));
 630};
 631
 632Effect.DropOut = function(element) {
 633  element = $(element);
 634  var oldStyle = {
 635    top: element.getStyle('top'),
 636    left: element.getStyle('left'),
 637    opacity: element.getInlineOpacity() };
 638  return new Effect.Parallel(
 639    [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
 640      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
 641    Object.extend(
 642      { duration: 0.5,
 643        beforeSetup: function(effect) {
 644          effect.effects[0].element.makePositioned();
 645        },
 646        afterFinishInternal: function(effect) {
 647          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
 648        }
 649      }, arguments[1] || { }));
 650};
 651
 652Effect.Shake = function(element) {
 653  element = $(element);
 654  var options = Object.extend({
 655    distance: 20,
 656    duration: 0.5
 657  }, arguments[1] || {});
 658  var distance = parseFloat(options.distance);
 659  var split = parseFloat(options.duration) / 10.0;
 660  var oldStyle = {
 661    top: element.getStyle('top'),
 662    left: element.getStyle('left') };
 663    return new Effect.Move(element,
 664      { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
 665    new Effect.Move(effect.element,
 666      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
 667    new Effect.Move(effect.element,
 668      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
 669    new Effect.Move(effect.element,
 670      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
 671    new Effect.Move(effect.element,
 672      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
 673    new Effect.Move(effect.element,
 674      { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
 675        effect.element.undoPositioned().setStyle(oldStyle);
 676  }}); }}); }}); }}); }}); }});
 677};
 678
 679Effect.SlideDown = function(element) {
 680  element = $(element).cleanWhitespace();
 681  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
 682  var oldInnerBottom = element.down().getStyle('bottom');
 683  var elementDimensions = element.getDimensions();
 684  return new Effect.Scale(element, 100, Object.extend({
 685    scaleContent: false,
 686    scaleX: false,
 687    scaleFrom: window.opera ? 0 : 1,
 688    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
 689    restoreAfterFinish: true,
 690    afterSetup: function(effect) {
 691      effect.element.makePositioned();
 692      effect.element.down().makePositioned();
 693      if (window.opera) effect.element.setStyle({top: ''});
 694      effect.element.makeClipping().setStyle({height: '0px'}).show();
 695    },
 696    afterUpdateInternal: function(effect) {
 697      effect.element.down().setStyle({bottom:
 698        (effect.dims[0] - effect.element.clientHeight) + 'px' });
 699    },
 700    afterFinishInternal: function(effect) {
 701      effect.element.undoClipping().undoPositioned();
 702      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
 703    }, arguments[1] || { })
 704  );
 705};
 706
 707Effect.SlideUp = function(element) {
 708  element = $(element).cleanWhitespace();
 709  var oldInnerBottom = element.down().getStyle('bottom');
 710  var elementDimensions = element.getDimensions();
 711  return new Effect.Scale(element, window.opera ? 0 : 1,
 712   Object.extend({ scaleContent: false,
 713    scaleX: false,
 714    scaleMode: 'box',
 715    scaleFrom: 100,
 716    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
 717    restoreAfterFinish: true,
 718    afterSetup: function(effect) {
 719      effect.element.makePositioned();
 720      effect.element.down().makePositioned();
 721      if (window.opera) effect.element.setStyle({top: ''});
 722      effect.element.makeClipping().show();
 723    },
 724    afterUpdateInternal: function(effect) {
 725      effect.element.down().setStyle({bottom:
 726        (effect.dims[0] - effect.element.clientHeight) + 'px' });
 727    },
 728    afterFinishInternal: function(effect) {
 729      effect.element.hide().undoClipping().undoPositioned();
 730      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
 731    }
 732   }, arguments[1] || { })
 733  );
 734};
 735
 736// Bug in opera makes the TD containing this element expand for a instance after finish
 737Effect.Squish = function(element) {
 738  return new Effect.Scale(element, window.opera ? 1 : 0, {
 739    restoreAfterFinish: true,
 740    beforeSetup: function(effect) {
 741      effect.element.makeClipping();
 742    },
 743    afterFinishInternal: function(effect) {
 744      effect.element.hide().undoClipping();
 745    }
 746  });
 747};
 748
 749Effect.Grow = function(element) {
 750  element = $(element);
 751  var options = Object.extend({
 752    direction: 'center',
 753    moveTransition: Effect.Transitions.sinoidal,
 754    scaleTransition: Effect.Transitions.sinoidal,
 755    opacityTransition: Effect.Transitions.full
 756  }, arguments[1] || { });
 757  var oldStyle = {
 758    top: element.style.top,
 759    left: element.style.left,
 760    height: element.style.height,
 761    width: element.style.width,
 762    opacity: element.getInlineOpacity() };
 763
 764  var dims = element.getDimensions();
 765  var initialMoveX, initialMoveY;
 766  var moveX, moveY;
 767
 768  switch (options.direction) {
 769    case 'top-left':
 770      initialMoveX = initialMoveY = moveX = moveY = 0;
 771      break;
 772    case 'top-right':
 773      initialMoveX = dims.width;
 774      initialMoveY = moveY = 0;
 775      moveX = -dims.width;
 776      break;
 777    case 'bottom-left':
 778      initialMoveX = moveX = 0;
 779      initialMoveY = dims.height;
 780      moveY = -dims.height;
 781      break;
 782    case 'bottom-right':
 783      initialMoveX = dims.width;
 784      initialMoveY = dims.height;
 785      moveX = -dims.width;
 786      moveY = -dims.height;
 787      break;
 788    case 'center':
 789      initialMoveX = dims.width / 2;
 790      initialMoveY = dims.height / 2;
 791      moveX = -dims.width / 2;
 792      moveY = -dims.height / 2;
 793      break;
 794  }
 795
 796  return new Effect.Move(element, {
 797    x: initialMoveX,
 798    y: initialMoveY,
 799    duration: 0.01,
 800    beforeSetup: function(effect) {
 801      effect.element.hide().makeClipping().makePositioned();
 802    },
 803    afterFinishInternal: function(effect) {
 804      new Effect.Parallel(
 805        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
 806          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
 807          new Effect.Scale(effect.element, 100, {
 808            scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
 809            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
 810        ], Object.extend({
 811             beforeSetup: function(effect) {
 812               effect.effects[0].element.setStyle({height: '0px'}).show();
 813             },
 814             afterFinishInternal: function(effect) {
 815               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
 816             }
 817           }, options)
 818      );
 819    }
 820  });
 821};
 822
 823Effect.Shrink = function(element) {
 824  element = $(element);
 825  var options = Object.extend({
 826    direction: 'center',
 827    moveTransition: Effect.Transitions.sinoidal,
 828    scaleTransition: Effect.Transitions.sinoidal,
 829    opacityTransition: Effect.Transitions.none
 830  }, arguments[1] || { });
 831  var oldStyle = {
 832    top: element.style.top,
 833    left: element.style.left,
 834    height: element.style.height,
 835    width: element.style.width,
 836    opacity: element.getInlineOpacity() };
 837
 838  var dims = element.getDimensions();
 839  var moveX, moveY;
 840
 841  switch (options.direction) {
 842    case 'top-left':
 843      moveX = moveY = 0;
 844      break;
 845    case 'top-right':
 846      moveX = dims.width;
 847      moveY = 0;
 848      break;
 849    case 'bottom-left':
 850      moveX = 0;
 851      moveY = dims.height;
 852      break;
 853    case 'bottom-right':
 854      moveX = dims.width;
 855      moveY = dims.height;
 856      break;
 857    case 'center':
 858      moveX = dims.width / 2;
 859      moveY = dims.height / 2;
 860      break;
 861  }
 862
 863  return new Effect.Parallel(
 864    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
 865      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
 866      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
 867    ], Object.extend({
 868         beforeStartInternal: function(effect) {
 869           effect.effects[0].element.makePositioned().makeClipping();
 870         },
 871         afterFinishInternal: function(effect) {
 872           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
 873       }, options)
 874  );
 875};
 876
 877Effect.Pulsate = function(element) {
 878  element = $(element);
 879  var options    = arguments[1] || { },
 880    oldOpacity = element.getInlineOpacity(),
 881    transition = options.transition || Effect.Transitions.linear,
 882    reverser   = function(pos){
 883      return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
 884    };
 885
 886  return new Effect.Opacity(element,
 887    Object.extend(Object.extend({  duration: 2.0, from: 0,
 888      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
 889    }, options), {transition: reverser}));
 890};
 891
 892Effect.Fold = function(element) {
 893  element = $(element);
 894  var oldStyle = {
 895    top: element.style.top,
 896    left: element.style.left,
 897    width: element.style.width,
 898    height: element.style.height };
 899  element.makeClipping();
 900  return new Effect.Scale(element, 5, Object.extend({
 901    scaleContent: false,
 902    scaleX: false,
 903    afterFinishInternal: function(effect) {
 904    new Effect.Scale(element, 1, {
 905      scaleContent: false,
 906      scaleY: false,
 907      afterFinishInternal: function(effect) {
 908        effect.element.hide().undoClipping().setStyle(oldStyle);
 909      } });
 910  }}, arguments[1] || { }));
 911};
 912
 913Effect.Morph = Class.create(Effect.Base, {
 914  initialize: function(element) {
 915    this.element = $(element);
 916    if (!this.element) throw(Effect._elementDoesNotExistError);
 917    var options = Object.extend({
 918      style: { }
 919    }, arguments[1] || { });
 920
 921    if (!Object.isString(options.style)) this.style = $H(options.style);
 922    else {
 923      if (options.style.include(':'))
 924        this.style = options.style.parseStyle();
 925      else {
 926        this.element.addClassName(options.style);
 927        this.style = $H(this.element.getStyles());
 928        this.element.removeClassName(options.style);
 929        var css = this.element.getStyles();
 930        this.style = this.style.reject(function(style) {
 931          return style.value == css[style.key];
 932        });
 933        options.afterFinishInternal = function(effect) {
 934          effect.element.addClassName(effect.options.style);
 935          effect.transforms.each(function(transform) {
 936            effect.element.style[transform.style] = '';
 937          });
 938        };
 939      }
 940    }
 941    this.start(options);
 942  },
 943
 944  setup: function(){
 945    function parseColor(color){
 946      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
 947      color = color.parseColor();
 948      return $R(0,2).map(function(i){
 949        return parseInt( color.slice(i*2+1,i*2+3), 16 );
 950      });
 951    }
 952    this.transforms = this.style.map(function(pair){
 953      var property = pair[0], value = pair[1], unit = null;
 954
 955      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
 956        value = value.parseColor();
 957        unit  = 'color';
 958      } else if (property == 'opacity') {
 959        value = parseFloat(value);
 960        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
 961          this.element.setStyle({zoom: 1});
 962      } else if (Element.CSS_LENGTH.test(value)) {
 963          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
 964          value = parseFloat(components[1]);
 965          unit = (components.length == 3) ? components[2] : null;
 966      }
 967
 968      var originalValue = this.element.getStyle(property);
 969      return {
 970        style: property.camelize(),
 971        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
 972        targetValue: unit=='color' ? parseColor(value) : value,
 973        unit: unit
 974      };
 975    }.bind(this)).reject(function(transform){
 976      return (
 977        (transform.originalValue == transform.targetValue) ||
 978        (
 979          transform.unit != 'color' &&
 980          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
 981        )
 982      );
 983    });
 984  },
 985  update: function(position) {
 986    var style = { }, transform, i = this.transforms.length;
 987    while(i--)
 988      style[(transform = this.transforms[i]).style] =
 989        transform.unit=='color' ? '#'+
 990          (Math.round(transform.originalValue[0]+
 991            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
 992          (Math.round(transform.originalValue[1]+
 993            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
 994          (Math.round(transform.originalValue[2]+
 995            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
 996        (transform.originalValue +
 997          (transform.targetValue - transform.originalValue) * position).toFixed(3) +
 998            (transform.unit === null ? '' : transform.unit);
 999    this.element.setStyle(style, true);
1000  }
1001});
1002
1003Effect.Transform = Class.create({
1004  initialize: function(tracks){
1005    this.tracks  = [];
1006    this.options = arguments[1] || { };
1007    this.addTracks(tracks);
1008  },
1009  addTracks: function(tracks){
1010    tracks.each(function(track){
1011      track = $H(track);
1012      var data = track.values().first();
1013      this.tracks.push($H({
1014        ids:     track.keys().first(),
1015        effect:  Effect.Morph,
1016        options: { style: data }
1017      }));
1018    }.bind(this));
1019    return this;
1020  },
1021  play: function(){
1022    return new Effect.Parallel(
1023      this.tracks.map(function(track){
1024        var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
1025        var elements = [$(ids) || $$(ids)].flatten();
1026        return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
1027      }).flatten(),
1028      this.options
1029    );
1030  }
1031});
1032
1033Element.CSS_PROPERTIES = $w(
1034  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
1035  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
1036  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
1037  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
1038  'fontSize fontWeight height left letterSpacing lineHeight ' +
1039  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
1040  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
1041  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
1042  'right textIndent top width wordSpacing zIndex');
1043
1044Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
1045
1046String.__parseStyleElement = document.createElement('div');
1047String.prototype.parseStyle = function(){
1048  var style, styleRules = $H();
1049  if (Prototype.Browser.WebKit)
1050    style = new Element('div',{style:this}).style;
1051  else {
1052    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
1053    style = String.__parseStyleElement.childNodes[0].style;
1054  }
1055
1056  Element.CSS_PROPERTIES.each(function(property){
1057    if (style[property]) styleRules.set(property, style[property]);
1058  });
1059
1060  if (Prototype.Browser.IE && this.include('opacity'))
1061    styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
1062
1063  return styleRules;
1064};
1065
1066if (document.defaultView && document.defaultView.getComputedStyle) {
1067  Element.getStyles = function(element) {
1068    var css = document.defaultView.getComputedStyle($(element), null);
1069    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
1070      styles[property] = css[property];
1071      return styles;
1072    });
1073  };
1074} else {
1075  Element.getStyles = function(element) {
1076    element = $(element);
1077    var css = element.currentStyle, styles;
1078    styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
1079      results[property] = css[property];
1080      return results;
1081    });
1082    if (!styles.opacity) styles.opacity = element.getOpacity();
1083    return styles;
1084  };
1085}
1086
1087Effect.Methods = {
1088  morph: function(element, style) {
1089    element = $(element);
1090    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
1091    return element;
1092  },
1093  visualEffect: function(element, effect, options) {
1094    element = $(element);
1095    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
1096    new Effect[klass](element, options);
1097    return element;
1098  },
1099  highlight: function(element, options) {
1100    element = $(element);
1101    new Effect.Highlight(element, options);
1102    return element;
1103  }
1104};
1105
1106$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
1107  'pulsate shake puff squish switchOff dropOut').each(
1108  function(effect) {
1109    Effect.Methods[effect] = function(element, options){
1110      element = $(element);
1111      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
1112      return element;
1113    };
1114  }
1115);
1116
1117$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
1118  function(f) { Effect.Methods[f] = Element[f]; }
1119);
1120
1121Element.addMethods(Effect.Methods);