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