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