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