1 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
3 // Parts (c) 2005 Justin Palmer (http://encytemedia.com/)
4 // Parts (c) 2005 Mark Pilgrim (http://diveintomark.org/)
6 // Permission is hereby granted, free of charge, to any person obtaining
7 // a copy of this software and associated documentation files (the
8 // "Software"), to deal in the Software without restriction, including
9 // without limitation the rights to use, copy, modify, merge, publish,
10 // distribute, sublicense, and/or sell copies of the Software, and to
11 // permit persons to whom the Software is furnished to do so, subject to
12 // the following conditions:
14 // The above copyright notice and this permission notice shall be
15 // included in all copies or substantial portions of the Software.
17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 tagifyText: function(element) {
27 var tagifyStyle = "position:relative";
28 if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1";
30 var children = element.childNodes;
31 for (var i = 0; i < children.length; i++)
32 if(children[i].nodeType==3) {
33 var child = children[i];
34 for (var j = 0; j < child.nodeValue.length; j++)
36 Builder.node('span',{style: tagifyStyle},
37 child.nodeValue.substr(j,1) == " " ? String.fromCharCode(160) :
38 child.nodeValue.substr(j,1)), child);
39 Element.remove(child);
42 multiple: function(element, effect) {
43 if(((typeof element == 'object') ||
44 (typeof element == 'function')) &&
46 var elements = element;
48 var elements = $(element).childNodes;
50 var options = Object.extend({
53 }, arguments[2] || {});
54 var speed = options.speed;
55 var delay = options.delay;
57 for(var i = 0; i < elements.length; i++)
58 new effect(elements[i],
59 Object.extend(options, { delay: delay + i*speed }));
63 var Effect2 = Effect; // deprecated
65 /* ------------- transitions ------------- */
67 Effect.Transitions = {}
69 Effect.Transitions.linear = function(pos) {
72 Effect.Transitions.sinoidal = function(pos) {
73 return (-Math.cos(pos*Math.PI)/2) + 0.5;
75 Effect.Transitions.reverse = function(pos) {
78 Effect.Transitions.flicker = function(pos) {
79 return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random(0.25);
81 Effect.Transitions.wobble = function(pos) {
82 return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
84 Effect.Transitions.pulse = function(pos) {
85 return (Math.floor(pos*10) % 2 == 0 ?
86 (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
88 Effect.Transitions.none = function(pos) {
91 Effect.Transitions.full = function(pos) {
95 /* ------------- core effects ------------- */
100 findLast: function() {
101 var timestamp = false;
102 for(var i = 0; i < this.effects.length; i++)
103 if(!timestamp || (this.effects[i].finishOn>timestamp))
104 timestamp = this.effects[i].finishOn;
107 add: function(effect) {
108 var timestamp = new Date().getTime();
110 switch(effect.options.queue) {
112 // move unstarted effects after this effect
113 for(var i = 0; i < this.effects.length; i++)
114 if(this.effects[i].state == 'idle') {
115 this.effects[i].startOn += effect.finishOn;
116 this.effects[i].finishOn += effect.finishOn;
120 // start effect after last queued effect has finished
121 timestamp = this.findLast() || timestamp;
125 effect.startOn += timestamp;
126 effect.finishOn += timestamp;
128 this.effects.push(effect);
131 this.interval = setInterval(this.loop.bind(this), 40);
133 remove: function(effect) {
134 for(var i = 0; i < this.effects.length; i++)
135 if(this.effects[i]==effect) this.effects.splice(i,1);
136 if(this.effects.length == 0) {
137 clearInterval(this.interval);
138 this.interval = null;
142 var timePos = new Date().getTime();
143 for(var i = 0; i < this.effects.length; i++) {
144 this.effects[i].loop(timePos);
149 Effect.Base = function() {};
150 Effect.Base.prototype = {
151 setOptions: function(options) {
152 this.options = Object.extend({
153 transition: Effect.Transitions.sinoidal,
154 duration: 1.0, // seconds
155 fps: 25.0, // max. 25fps due to Effect.Queue implementation
156 sync: false, // true for combining
163 start: function(options) {
164 this.setOptions(options || {});
165 this.currentFrame = 0;
167 this.startOn = this.options.delay*1000;
168 this.finishOn = this.startOn + (this.options.duration*1000);
169 if(this.options.beforeStart) this.options.beforeStart(this);
170 if(!this.options.sync) Effect.Queue.add(this);
172 loop: function(timePos) {
173 if(timePos >= this.startOn) {
174 if(timePos >= this.finishOn) {
177 if(this.finish) this.finish();
178 if(this.options.afterFinish) this.options.afterFinish(this);
181 var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
182 var frame = Math.round(pos * this.options.fps * this.options.duration);
183 if(frame > this.currentFrame) {
185 this.currentFrame = frame;
189 render: function(pos) {
190 if(this.state == 'idle') {
191 this.state = 'running';
192 if(this.setup) this.setup();
194 if(this.options.transition) pos = this.options.transition(pos);
195 pos *= (this.options.to-this.options.from);
196 pos += this.options.from;
197 if(this.options.beforeUpdate) this.options.beforeUpdate(this);
198 if(this.update) this.update(pos);
199 if(this.options.afterUpdate) this.options.afterUpdate(this);
202 if(!this.options.sync) Effect.Queue.remove(this);
203 this.state = 'finished';
207 Effect.Parallel = Class.create();
208 Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
209 initialize: function(effects) {
210 this.effects = effects || [];
211 this.start(arguments[1]);
213 update: function(position) {
214 for (var i = 0; i < this.effects.length; i++)
215 this.effects[i].render(position);
217 finish: function(position) {
218 for (var i = 0; i < this.effects.length; i++) {
219 this.effects[i].cancel();
220 if(this.effects[i].finish) this.effects[i].finish(position);
225 // Internet Explorer caveat: works only on elements that have
226 // a 'layout', meaning having a given width or height.
227 // There is no way to safely set this automatically.
228 Effect.Opacity = Class.create();
229 Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
230 initialize: function(element) {
231 this.element = $(element);
232 var options = Object.extend({
235 }, arguments[1] || {});
238 update: function(position) {
239 this.setOpacity(position);
241 setOpacity: function(opacity) {
242 if(opacity<0.0001) opacity = 0; // fix errors with things like 6.152242992829571e-8
244 this.element.style.opacity = '0.999999';
245 this.element.style.filter = null;
247 this.element.style.opacity = opacity;
248 this.element.style.filter = "alpha(opacity:"+opacity*100+")";
253 Effect.MoveBy = Class.create();
254 Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
255 initialize: function(element, toTop, toLeft) {
256 this.element = $(element);
258 this.toLeft = toLeft;
259 this.start(arguments[3]);
262 this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0');
263 this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
264 Element.makePositioned(this.element);
266 update: function(position) {
267 topd = this.toTop * position + this.originalTop;
268 leftd = this.toLeft * position + this.originalLeft;
269 this.setPosition(topd, leftd);
271 setPosition: function(topd, leftd) {
272 this.element.style.top = topd + "px";
273 this.element.style.left = leftd + "px";
277 Effect.Scale = Class.create();
278 Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
279 initialize: function(element, percent) {
280 this.element = $(element)
281 var options = Object.extend({
285 scaleFromCenter: false,
286 scaleMode: 'box', // 'box' or 'contents' or {} with provided values
289 }, arguments[2] || {});
293 this.originalTop = this.element.offsetTop;
294 this.originalLeft = this.element.offsetLeft;
295 if(Element.getStyle(this.element,'font-size')=="") this.sizeEm = 1.0;
296 if(Element.getStyle(this.element,'font-size') && Element.getStyle(this.element,'font-size').indexOf("em")>0)
297 this.sizeEm = parseFloat(Element.getStyle(this.element,'font-size'));
298 this.factor = (this.options.scaleTo/100.0) - (this.options.scaleFrom/100.0);
299 if(this.options.scaleMode=='box') {
300 this.originalHeight = this.element.clientHeight;
301 this.originalWidth = this.element.clientWidth;
303 if(this.options.scaleMode=='contents') {
304 this.originalHeight = this.element.scrollHeight;
305 this.originalWidth = this.element.scrollWidth;
307 this.originalHeight = this.options.scaleMode.originalHeight;
308 this.originalWidth = this.options.scaleMode.originalWidth;
311 update: function(position) {
312 var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
313 if(this.options.scaleContent && this.sizeEm)
314 this.element.style.fontSize = this.sizeEm*currentScale + "em";
316 this.originalWidth * currentScale,
317 this.originalHeight * currentScale);
319 setDimensions: function(width, height) {
320 if(this.options.scaleX) this.element.style.width = width + 'px';
321 if(this.options.scaleY) this.element.style.height = height + 'px';
322 if(this.options.scaleFromCenter) {
323 var topd = (height - this.originalHeight)/2;
324 var leftd = (width - this.originalWidth)/2;
325 if(Element.getStyle(this.element,'position')=='absolute') {
326 if(this.options.scaleY) this.element.style.top = this.originalTop-topd + "px";
327 if(this.options.scaleX) this.element.style.left = this.originalLeft-leftd + "px";
329 if(this.options.scaleY) this.element.style.top = -topd + "px";
330 if(this.options.scaleX) this.element.style.left = -leftd + "px";
336 Effect.Highlight = Class.create();
337 Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
338 initialize: function(element) {
339 this.element = $(element);
340 var options = Object.extend({
341 startcolor: "#ffff99"
342 }, arguments[1] || {});
346 // try to parse current background color as default for endcolor
347 // browser stores this as: "rgb(255, 255, 255)", convert to "#ffffff" format
348 if(!this.options.endcolor) {
349 var endcolor = "#ffffff";
350 var current = Element.getStyle(this.element, 'background-color');
351 if(current && current.slice(0,4) == "rgb(") {
353 var cols = current.slice(4,current.length-1).split(',');
354 var i=0; do { endcolor += parseInt(cols[i]).toColorPart() } while (++i<3);
356 this.options.endcolor = endcolor;
358 // init color calculations
360 parseInt(this.options.startcolor.slice(1,3),16),
361 parseInt(this.options.startcolor.slice(3,5),16),
362 parseInt(this.options.startcolor.slice(5),16) ];
363 this.colors_delta = [
364 parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
365 parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
366 parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
368 update: function(position) {
370 Math.round(this.colors_base[0]+(this.colors_delta[0]*position)),
371 Math.round(this.colors_base[1]+(this.colors_delta[1]*position)),
372 Math.round(this.colors_base[2]+(this.colors_delta[2]*position)) ];
373 this.element.style.backgroundColor = "#" +
374 colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
377 this.element.style.backgroundColor = this.options.restorecolor;
381 Effect.ScrollTo = Class.create();
382 Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
383 initialize: function(element) {
384 this.element = $(element);
385 this.start(arguments[1] || {});
389 var offsets = Position.cumulativeOffset(this.element);
390 var max = window.innerHeight ?
391 window.height - window.innerHeight :
392 document.body.scrollHeight -
393 (document.documentElement.clientHeight ?
394 document.documentElement.clientHeight : document.body.clientHeight);
395 this.scrollStart = Position.deltaY;
396 this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
398 update: function(position) {
400 window.scrollTo(Position.deltaX,
401 this.scrollStart + (position*this.delta));
405 /* ------------- combination effects ------------- */
407 Effect.Fade = function(element) {
408 var options = Object.extend({
411 afterFinish: function(effect)
412 { Element.hide(effect.element);
413 effect.setOpacity(1); }
414 }, arguments[1] || {});
415 return new Effect.Opacity(element,options);
418 Effect.Appear = function(element) {
419 var options = Object.extend({
422 beforeStart: function(effect)
423 { effect.setOpacity(0);
424 Element.show(effect.element); },
425 afterUpdate: function(effect)
426 { Element.show(effect.element); }
427 }, arguments[1] || {});
428 return new Effect.Opacity(element,options);
431 Effect.Puff = function(element) {
432 return new Effect.Parallel(
433 [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true }),
434 new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ],
435 Object.extend({ duration: 1.0,
436 beforeUpdate: function(effect)
437 { effect.effects[0].element.style.position = 'absolute'; },
438 afterFinish: function(effect)
439 { Element.hide(effect.effects[0].element); }
440 }, arguments[1] || {})
444 Effect.BlindUp = function(element) {
445 element = $(element);
446 Element.makeClipping(element);
447 return new Effect.Scale(element, 0,
448 Object.extend({ scaleContent: false,
450 afterFinish: function(effect)
452 Element.hide(effect.element);
453 Element.undoClipping(effect.element);
455 }, arguments[1] || {})
459 Effect.BlindDown = function(element) {
460 element = $(element);
461 element.style.height = '0px';
462 Element.makeClipping(element);
463 Element.show(element);
464 return new Effect.Scale(element, 100,
465 Object.extend({ scaleContent: false,
467 scaleMode: 'contents',
469 afterFinish: function(effect) {
470 Element.undoClipping(effect.element);
472 }, arguments[1] || {})
476 Effect.SwitchOff = function(element) {
477 return new Effect.Appear(element,
479 transition: Effect.Transitions.flicker,
480 afterFinish: function(effect)
481 { effect.element.style.overflow = 'hidden';
482 new Effect.Scale(effect.element, 1,
483 { duration: 0.3, scaleFromCenter: true,
484 scaleX: false, scaleContent: false,
485 afterUpdate: function(effect) {
486 if(effect.element.style.position=="")
487 effect.element.style.position = 'relative'; },
488 afterFinish: function(effect) { Element.hide(effect.element); }
494 Effect.DropOut = function(element) {
495 return new Effect.Parallel(
496 [ new Effect.MoveBy(element, 100, 0, { sync: true }),
497 new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ],
500 afterFinish: function(effect)
501 { Element.hide(effect.effects[0].element); }
502 }, arguments[1] || {}));
505 Effect.Shake = function(element) {
506 return new Effect.MoveBy(element, 0, 20,
507 { duration: 0.05, afterFinish: function(effect) {
508 new Effect.MoveBy(effect.element, 0, -40,
509 { duration: 0.1, afterFinish: function(effect) {
510 new Effect.MoveBy(effect.element, 0, 40,
511 { duration: 0.1, afterFinish: function(effect) {
512 new Effect.MoveBy(effect.element, 0, -40,
513 { duration: 0.1, afterFinish: function(effect) {
514 new Effect.MoveBy(effect.element, 0, 40,
515 { duration: 0.1, afterFinish: function(effect) {
516 new Effect.MoveBy(effect.element, 0, -20,
517 { duration: 0.05, afterFinish: function(effect) {
518 }}) }}) }}) }}) }}) }});
521 Effect.SlideDown = function(element) {
522 element = $(element);
523 element.style.height = '0px';
524 Element.makeClipping(element);
525 Element.cleanWhitespace(element);
526 Element.makePositioned(element.firstChild);
527 Element.show(element);
528 return new Effect.Scale(element, 100,
529 Object.extend({ scaleContent: false,
531 scaleMode: 'contents',
533 afterUpdate: function(effect)
534 { effect.element.firstChild.style.bottom =
535 (effect.originalHeight - effect.element.clientHeight) + 'px'; },
536 afterFinish: function(effect)
537 { Element.undoClipping(effect.element); }
538 }, arguments[1] || {})
542 Effect.SlideUp = function(element) {
543 element = $(element);
544 Element.makeClipping(element);
545 Element.cleanWhitespace(element);
546 Element.makePositioned(element.firstChild);
547 Element.show(element);
548 return new Effect.Scale(element, 0,
549 Object.extend({ scaleContent: false,
551 afterUpdate: function(effect)
552 { effect.element.firstChild.style.bottom =
553 (effect.originalHeight - effect.element.clientHeight) + 'px'; },
554 afterFinish: function(effect)
556 Element.hide(effect.element);
557 Element.undoClipping(effect.element);
559 }, arguments[1] || {})
563 Effect.Squish = function(element) {
564 return new Effect.Scale(element, 0,
565 { afterFinish: function(effect) { Element.hide(effect.element); } });
568 Effect.Grow = function(element) {
569 element = $(element);
570 var options = arguments[1] || {};
572 var originalWidth = element.clientWidth;
573 var originalHeight = element.clientHeight;
574 element.style.overflow = 'hidden';
575 Element.show(element);
577 var direction = options.direction || 'center';
578 var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
579 var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
580 var opacityTransition = options.opacityTransition || Effect.Transitions.full;
582 var initialMoveX, initialMoveY;
587 initialMoveX = initialMoveY = moveX = moveY = 0;
590 initialMoveX = originalWidth;
591 initialMoveY = moveY = 0;
592 moveX = -originalWidth;
595 initialMoveX = moveX = 0;
596 initialMoveY = originalHeight;
597 moveY = -originalHeight;
600 initialMoveX = originalWidth;
601 initialMoveY = originalHeight;
602 moveX = -originalWidth;
603 moveY = -originalHeight;
606 initialMoveX = originalWidth / 2;
607 initialMoveY = originalHeight / 2;
608 moveX = -originalWidth / 2;
609 moveY = -originalHeight / 2;
613 return new Effect.MoveBy(element, initialMoveY, initialMoveX, {
615 beforeUpdate: function(effect) { $(element).style.height = '0px'; },
616 afterFinish: function(effect) {
618 [ new Effect.Opacity(element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
619 new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }),
620 new Effect.Scale(element, 100, {
621 scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth },
622 sync: true, scaleFrom: 0, scaleTo: 100, transition: scaleTransition })],
627 Effect.Shrink = function(element) {
628 element = $(element);
629 var options = arguments[1] || {};
631 var originalWidth = element.clientWidth;
632 var originalHeight = element.clientHeight;
633 element.style.overflow = 'hidden';
634 Element.show(element);
636 var direction = options.direction || 'center';
637 var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
638 var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
639 var opacityTransition = options.opacityTransition || Effect.Transitions.none;
648 moveX = originalWidth;
653 moveY = originalHeight;
656 moveX = originalWidth;
657 moveY = originalHeight;
660 moveX = originalWidth / 2;
661 moveY = originalHeight / 2;
665 return new Effect.Parallel(
666 [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
667 new Effect.Scale(element, 0, { sync: true, transition: moveTransition }),
668 new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: scaleTransition }) ],
672 Effect.Pulsate = function(element) {
673 element = $(element);
674 var options = arguments[1] || {};
675 var transition = options.transition || Effect.Transitions.sinoidal;
676 var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
677 reverser.bind(transition);
678 return new Effect.Opacity(element,
679 Object.extend(Object.extend({ duration: 3.0,
680 afterFinish: function(effect) { Element.show(effect.element); }
681 }, options), {transition: reverser}));
684 Effect.Fold = function(element) {
685 element = $(element);
686 element.style.overflow = 'hidden';
687 return new Effect.Scale(element, 5, Object.extend({
691 afterFinish: function(effect) {
692 new Effect.Scale(element, 1, {
696 afterFinish: function(effect) { Element.hide(effect.element) } });
697 }}, arguments[1] || {}));
700 // old: new Effect.ContentZoom(element, percent)
701 // new: Element.setContentZoom(element, percent)
703 Element.setContentZoom = function(element, percent) {
704 element = $(element);
705 element.style.fontSize = (percent/100) + "em";
706 if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);