1 // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
3 // Justin Palmer (http://encytemedia.com/)
4 // Mark Pilgrim (http://diveintomark.org/)
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/
10 // converts rgb() and #xxx to #xxxxxx format,
11 // returns self (or first argument) if not convertable
12 String.prototype.parseColor = function() {
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);
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();
23 return(color.length==7 ? color : (arguments[0] || this));
26 /*--------------------------------------------------------------------------*/
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('');
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('');
43 Element.setContentZoom = function(element, percent) {
45 element.setStyle({fontSize: (percent/100) + 'em'});
46 if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
50 Element.getOpacity = function(element){
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;
60 Element.setOpacity = function(element, value){
63 element.setStyle({ opacity:
64 (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
66 if(/MSIE/.test(navigator.userAgent) && !window.opera)
67 element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
69 if(value < 0.00001) value = 0;
70 element.setStyle({opacity: value});
71 if(/MSIE/.test(navigator.userAgent) && !window.opera)
73 { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
74 'alpha(opacity='+value*100+')' });
79 Element.getInlineOpacity = function(element){
80 return $(element).style.opacity || '';
83 Element.forceRerendering = function(element) {
86 var n = document.createTextNode(' ');
87 element.appendChild(n);
88 element.removeChild(n);
92 /*--------------------------------------------------------------------------*/
94 Array.prototype.call = function() {
96 this.each(function(f){ f.apply(this, args) });
99 /*--------------------------------------------------------------------------*/
102 _elementDoesNotExistError: {
103 name: 'ElementDoesNotExistError',
104 message: 'The specified DOM element does not exist, but is required for this effect to operate'
106 tagifyText: function(element) {
107 if(typeof Builder == 'undefined')
108 throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
110 var tagifyStyle = 'position:relative';
111 if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
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),
122 Element.remove(child);
126 multiple: function(element, effect) {
128 if(((typeof element == 'object') ||
129 (typeof element == 'function')) &&
133 elements = $(element).childNodes;
135 var options = Object.extend({
138 }, arguments[2] || {});
139 var masterDelay = options.delay;
141 $A(elements).each( function(element, index) {
142 new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
146 'slide': ['SlideDown','SlideUp'],
147 'blind': ['BlindDown','BlindUp'],
148 'appear': ['Appear','Fade']
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);
161 var Effect2 = Effect; // deprecated
163 /* ------------- transitions ------------- */
165 Effect.Transitions = {
167 sinoidal: function(pos) {
168 return (-Math.cos(pos*Math.PI)/2) + 0.5;
170 reverse: function(pos) {
173 flicker: function(pos) {
174 return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
176 wobble: function(pos) {
177 return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
179 pulse: function(pos, pulses) {
180 pulses = pulses || 5;
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))
187 none: function(pos) {
190 full: function(pos) {
195 /* ------------- core effects ------------- */
197 Effect.ScopedQueue = Class.create();
198 Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
199 initialize: function() {
201 this.interval = null;
203 _each: function(iterator) {
204 this.effects._each(iterator);
206 add: function(effect) {
207 var timestamp = new Date().getTime();
209 var position = (typeof effect.options.queue == 'string') ?
210 effect.options.queue : effect.options.queue.position;
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;
221 timestamp = this.effects.pluck('startOn').max() || timestamp;
224 // start effect after last queued effect has finished
225 timestamp = this.effects.pluck('finishOn').max() || timestamp;
229 effect.startOn += timestamp;
230 effect.finishOn += timestamp;
232 if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
233 this.effects.push(effect);
236 this.interval = setInterval(this.loop.bind(this), 40);
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;
246 var timePos = new Date().getTime();
247 this.effects.invoke('loop', timePos);
253 get: function(queueName) {
254 if(typeof queueName != 'string') return queueName;
256 if(!this.instances[queueName])
257 this.instances[queueName] = new Effect.ScopedQueue();
259 return this.instances[queueName];
262 Effect.Queue = Effect.Queues.get('global');
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
275 Effect.Base = function() {};
276 Effect.Base.prototype = {
278 start: function(options) {
279 this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
280 this.currentFrame = 0;
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);
289 loop: function(timePos) {
290 if(timePos >= this.startOn) {
291 if(timePos >= this.finishOn) {
294 this.event('beforeFinish');
295 if(this.finish) this.finish();
296 this.event('afterFinish');
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) {
303 this.currentFrame = frame;
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');
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;
319 this.event('beforeUpdate');
320 if(this.update) this.update(pos);
321 this.event('afterUpdate');
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';
330 event: function(eventName) {
331 if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
332 if(this.options[eventName]) this.options[eventName](this);
334 inspect: function() {
335 return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
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]);
345 update: function(position) {
346 this.effects.invoke('render', position);
348 finish: function(position) {
349 this.effects.each( function(effect) {
352 effect.event('beforeFinish');
353 if(effect.finish) effect.finish(position);
354 effect.event('afterFinish');
359 Effect.Event = Class.create();
360 Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
361 initialize: function() {
362 var options = Object.extend({
364 }, arguments[0] || {});
367 update: Prototype.emptyFunction
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,
381 }, arguments[1] || {});
384 update: function(position) {
385 this.element.setOpacity(position);
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({
398 }, arguments[1] || {});
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;
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'
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] || {}));
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({
438 scaleFromCenter: false,
439 scaleMode: 'box', // 'box' or 'contents' or {} with provided values
442 }, arguments[2] || {});
446 this.restoreAfterFinish = this.options.restoreAfterFinish || false;
447 this.elementPositioning = this.element.getStyle('position');
449 this.originalStyle = {};
450 ['top','left','width','height','fontSize'].each( function(k) {
451 this.originalStyle[k] = this.element.style[k];
454 this.originalTop = this.element.offsetTop;
455 this.originalLeft = this.element.offsetLeft;
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;
465 this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
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];
473 this.dims = [this.options.scaleMode.originalHeight,
474 this.options.scaleMode.originalWidth];
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);
482 finish: function(position) {
483 if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
485 setDimensions: function(height, width) {
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';
496 if(this.options.scaleY) d.top = -topd + 'px';
497 if(this.options.scaleX) d.left = -leftd + 'px';
500 this.element.setStyle(d);
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] || {});
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
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));
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)) });
532 this.element.setStyle(Object.extend(this.oldStyle, {
533 backgroundColor: this.options.restorecolor
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] || {});
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;
556 update: function(position) {
558 window.scrollTo(Position.deltaX,
559 this.scrollStart + (position*this.delta));
563 /* ------------- combination effects ------------- */
565 Effect.Fade = function(element) {
566 element = $(element);
567 var oldOpacity = element.getInlineOpacity();
568 var options = Object.extend({
569 from: element.getOpacity() || 1.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);
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),
583 // force Safari to render floated elements properly
584 afterFinishInternal: function(effect) {
585 effect.element.forceRerendering();
587 beforeSetup: function(effect) {
588 effect.element.setOpacity(effect.options.from).show();
589 }}, arguments[1] || {});
590 return new Effect.Opacity(element,options);
593 Effect.Puff = function(element) {
594 element = $(element);
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
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)
611 afterFinishInternal: function(effect) {
612 effect.effects[0].element.hide().setStyle(oldStyle); }
613 }, arguments[1] || {})
617 Effect.BlindUp = function(element) {
618 element = $(element);
619 element.makeClipping();
620 return new Effect.Scale(element, 0,
621 Object.extend({ scaleContent: false,
623 restoreAfterFinish: true,
624 afterFinishInternal: function(effect) {
625 effect.element.hide().undoClipping();
627 }, arguments[1] || {})
631 Effect.BlindDown = function(element) {
632 element = $(element);
633 var elementDimensions = element.getDimensions();
634 return new Effect.Scale(element, 100, Object.extend({
638 scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
639 restoreAfterFinish: true,
640 afterSetup: function(effect) {
641 effect.element.makeClipping().setStyle({height: '0px'}).show();
643 afterFinishInternal: function(effect) {
644 effect.element.undoClipping();
646 }, arguments[1] || {}));
649 Effect.SwitchOff = function(element) {
650 element = $(element);
651 var oldOpacity = element.getInlineOpacity();
652 return new Effect.Appear(element, Object.extend({
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();
663 afterFinishInternal: function(effect) {
664 effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
668 }, arguments[1] || {}));
671 Effect.DropOut = function(element) {
672 element = $(element);
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 }) ],
682 beforeSetup: function(effect) {
683 effect.effects[0].element.makePositioned();
685 afterFinishInternal: function(effect) {
686 effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
688 }, arguments[1] || {}));
691 Effect.Shake = function(element) {
692 element = $(element);
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 }}) }}) }}) }}) }}) }});
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({
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();
729 afterUpdateInternal: function(effect) {
730 effect.element.down().setStyle({bottom:
731 (effect.dims[0] - effect.element.clientHeight) + 'px' });
733 afterFinishInternal: function(effect) {
734 effect.element.undoClipping().undoPositioned();
735 effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
736 }, arguments[1] || {})
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,
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();
755 afterUpdateInternal: function(effect) {
756 effect.element.down().setStyle({bottom:
757 (effect.dims[0] - effect.element.clientHeight) + 'px' });
759 afterFinishInternal: function(effect) {
760 effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
761 effect.element.down().undoPositioned();
763 }, arguments[1] || {})
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();
774 afterFinishInternal: function(effect) {
775 effect.element.hide().undoClipping();
780 Effect.Grow = function(element) {
781 element = $(element);
782 var options = Object.extend({
784 moveTransition: Effect.Transitions.sinoidal,
785 scaleTransition: Effect.Transitions.sinoidal,
786 opacityTransition: Effect.Transitions.full
787 }, arguments[1] || {});
789 top: element.style.top,
790 left: element.style.left,
791 height: element.style.height,
792 width: element.style.width,
793 opacity: element.getInlineOpacity() };
795 var dims = element.getDimensions();
796 var initialMoveX, initialMoveY;
799 switch (options.direction) {
801 initialMoveX = initialMoveY = moveX = moveY = 0;
804 initialMoveX = dims.width;
805 initialMoveY = moveY = 0;
809 initialMoveX = moveX = 0;
810 initialMoveY = dims.height;
811 moveY = -dims.height;
814 initialMoveX = dims.width;
815 initialMoveY = dims.height;
817 moveY = -dims.height;
820 initialMoveX = dims.width / 2;
821 initialMoveY = dims.height / 2;
822 moveX = -dims.width / 2;
823 moveY = -dims.height / 2;
827 return new Effect.Move(element, {
831 beforeSetup: function(effect) {
832 effect.element.hide().makeClipping().makePositioned();
834 afterFinishInternal: function(effect) {
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})
842 beforeSetup: function(effect) {
843 effect.effects[0].element.setStyle({height: '0px'}).show();
845 afterFinishInternal: function(effect) {
846 effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
854 Effect.Shrink = function(element) {
855 element = $(element);
856 var options = Object.extend({
858 moveTransition: Effect.Transitions.sinoidal,
859 scaleTransition: Effect.Transitions.sinoidal,
860 opacityTransition: Effect.Transitions.none
861 }, arguments[1] || {});
863 top: element.style.top,
864 left: element.style.left,
865 height: element.style.height,
866 width: element.style.width,
867 opacity: element.getInlineOpacity() };
869 var dims = element.getDimensions();
872 switch (options.direction) {
889 moveX = dims.width / 2;
890 moveY = dims.height / 2;
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 })
899 beforeStartInternal: function(effect) {
900 effect.effects[0].element.makePositioned().makeClipping();
902 afterFinishInternal: function(effect) {
903 effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
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}));
921 Effect.Fold = function(element) {
922 element = $(element);
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({
932 afterFinishInternal: function(effect) {
933 new Effect.Scale(element, 1, {
936 afterFinishInternal: function(effect) {
937 effect.element.hide().undoClipping().setStyle(oldStyle);
939 }}, arguments[1] || {}));
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({
949 }, arguments[1] || {});
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 )
960 this.transforms = this.options.style.parseStyle().map(function(property){
961 var originalValue = this.element.getStyle(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
970 }.bind(this)).reject(function(transform){
972 (transform.originalValue == transform.targetValue) ||
974 transform.unit != 'color' &&
975 (isNaN(transform.originalValue) || isNaN(transform.targetValue))
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;
991 this.element.setStyle(style);
995 Effect.Transform = Class.create();
996 Object.extend(Effect.Transform.prototype, {
997 initialize: function(tracks){
999 this.options = arguments[1] || {};
1000 this.addTracks(tracks);
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 }
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)) });
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'];
1042 Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
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();
1049 Element.CSS_PROPERTIES.each(function(property){
1050 if(style[property]) styleRules[property] = style[property];
1055 styleRules.each(function(pair){
1056 var property = pair[0], value = pair[1], unit = null;
1058 if(value.parseColor('#zzzzzz') != '#zzzzzz') {
1059 value = value.parseColor();
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;
1065 result[property.underscore().dasherize()] = $H({ value:value, unit:unit });
1071 Element.morph = function(element, style) {
1072 new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
1076 ['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
1077 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
1078 function(f) { Element.Methods[f] = Element[f]; }
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);
1088 Element.addMethods();