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