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