From: Corey Frang Date: Mon, 23 Apr 2012 19:05:12 +0000 (-0400) Subject: Effects: 1.8 Animation Rewrite - thanks @mikesherov and @gibson042 X-Git-Tag: 1.8b1~190 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=58ed62e;p=jquery.git Effects: 1.8 Animation Rewrite - thanks @mikesherov and @gibson042 --- diff --git a/src/css.js b/src/css.js index adfd4fda3..c3a6f7725 100644 --- a/src/css.js +++ b/src/css.js @@ -1,10 +1,12 @@ (function( jQuery ) { +jQuery.cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + var ralpha = /alpha\([^)]*\)/i, ropacity = /opacity=([^)]*)/, // fixed for IE9, see #8346 rupper = /([A-Z]|^ms)/g, - rnum = /^[\-+]?(?:\d*\.)?\d+$/i, + rnumsplit = /^([\-+]?(?:\d*\.)?\d+)(.*)$/i, rnumnonpx = /^-?(?:\d*\.)?\d+(?!px)[^\d\s]+$/i, rrelNum = /^([\-+])=([\-+.\de]+)/, rmargin = /^margin/, @@ -12,7 +14,7 @@ var ralpha = /alpha\([^)]*\)/i, cssShow = { position: "absolute", visibility: "hidden", display: "block" }, // order is important! - cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssExpand = jQuery.cssExpand, cssPrefixes = [ "O", "Webkit", "Moz", "ms" ], curCSS; @@ -264,6 +266,13 @@ if ( document.defaultView && document.defaultView.getComputedStyle ) { }; } +function setPositiveNumber( elem, value ) { + var matches = rnumsplit.exec( value ); + return matches ? + Math.max( 0, matches[ 1 ] ) + ( matches [ 2 ] || "px" ) + : value; +} + function getWidthOrHeight( elem, name, extra ) { // Start with offset property, which is equivalent to the border-box value @@ -348,11 +357,7 @@ jQuery.each([ "height", "width" ], function( i, name ) { } }, - set: function( elem, value ) { - return rnum.test( value ) ? - value + "px" : - value; - } + set: setPositiveNumber }; }); @@ -436,7 +441,6 @@ jQuery.each({ padding: "", border: "Width" }, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { expand: function( value ) { var i, @@ -453,6 +457,10 @@ jQuery.each({ return expanded; } }; + + if ( !rmargin.test( prefix ) ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } }); })( jQuery ); diff --git a/src/effects.js b/src/effects.js index 24d3d34a8..b6a374fb4 100644 --- a/src/effects.js +++ b/src/effects.js @@ -1,274 +1,487 @@ (function( jQuery ) { -var elemdisplay = {}, - iframe, iframeDoc, +var fxNow, timerId, iframe, iframeDoc, + elemdisplay = {}, rfxtypes = /^(?:toggle|show|hide)$/, - rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i, - rMarginProp = /^margin/, - timerId, - fxAttrs = [ - // height animations - [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ], - // width animations - [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ], - // opacity animations - [ "opacity" ] - ], - fxNow; - -jQuery.fn.extend({ - show: function( speed, easing, callback ) { - var elem, display; + rfxnum = /^([\-+]=)?((?:\d*\.)?\d+)([a-z%]*)$/i, + rrun = /\.run$/, + animationPrefilters = [], + tweeners = { + "*": [function( prop, value ) { + var end, unit, + tween = this.createTween( prop, value ), + parts = rfxnum.exec( value ), + start = tween.cur(); + + if ( parts ) { + end = +parts[2]; + unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + + // We need to compute starting value + if ( unit !== "px" ) { + jQuery.style( this, prop, (end || 1) + unit); + start = start * (end || 1) / tween.cur() || 0; + jQuery.style( this, prop, start + unit); + } - if ( speed || speed === 0 ) { - return this.animate( genFx("show", 3), speed, easing, callback ); + tween.unit = unit; + tween.start = start; + // If a +=/-= token was provided, we're doing a relative animation + tween.end = parts[1] ? start + end * ( parts[1] === "-=" ? -1 : 1 ) : end; + } + return tween; + }] + }, + oldToggle = jQuery.fn.toggle; - } else { - for ( var i = 0, j = this.length; i < j; i++ ) { - elem = this[ i ]; +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout(function() { + fxNow = undefined; + }, 0 ); + return ( fxNow = jQuery.now() ); +} - if ( elem.style ) { - display = elem.style.display; +function callTweeners( animation, props ) { + jQuery.each( props, function( prop, value ) { + var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( collection[ index ].call( animation, prop, value ) ) { + // we're done with this property + return; + } + } + }); +} - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { - display = elem.style.display = ""; - } +function Animation( elem, properties, options ) { + var result, + index = 0, + tweenerIndex = 0, + length = animationPrefilters.length, + finished = jQuery.Deferred(), + deferred = jQuery.Deferred().always(function( ended ) { + // remove cirular reference + delete animation.tick; + + if ( deferred.state() === "resolved" || ended ) { + // fire callbacks + finished.resolveWith( this ); + } + }), + animation = deferred.promise({ + elem: elem, + originalProperties: properties, + originalOptions: options, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( {}, options ), + startTime: fxNow || createFxNow(), + duration: options.duration, + finish: finished.done, + tweens: [], + createTween: function( prop, end, easing ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + tick: function() { + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + percent = 1 - ( remaining / animation.duration || 0 ), + index = 0, + length = animation.tweens.length; + + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( percent ); + } - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( (display === "" && jQuery.css(elem, "display") === "none") || - !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { - jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); - } + if ( percent < 1 && length ) { + return remaining; + } else { + deferred.resolveWith( elem, [ currentTime ] ); + return false; + } + }, + stop: function( gotoEnd ) { + var index = 0, + // if we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( 1 ); } + deferred.rejectWith( elem, [ gotoEnd ] ); + return this; } + }), + props = animation.props; - // Set the display of most of the elements in a second loop - // to avoid the constant reflow - for ( i = 0; i < j; i++ ) { - elem = this[ i ]; + propFilter( props ); - if ( elem.style ) { - display = elem.style.display; + for ( ; index < length ; index++ ) { + result = animationPrefilters[ index ].call( animation, + elem, props, animation.opts ); + if ( result ) { + return result; + } + } - if ( display === "" || display === "none" ) { - elem.style.display = jQuery._data( elem, "olddisplay" ) || ""; - } - } - } + callTweeners( animation, props ); - return this; - } - }, + jQuery.extend( animation.tick, { + anim: animation, + queue: animation.opts.queue, + elem: elem + }); - hide: function( speed, easing, callback ) { - if ( speed || speed === 0 ) { - return this.animate( genFx("hide", 3), speed, easing, callback); + jQuery.fx.timer( animation.tick ); + return animation; +} - } else { - var elem, display, - i = 0, - j = this.length; +function propFilter( props ) { + var index, name, hooks, replace; - for ( ; i < j; i++ ) { - elem = this[i]; - if ( elem.style ) { - display = jQuery.css( elem, "display" ); + // camelCase and expand cssHook pass + for ( index in props ) { + name = jQuery.camelCase( index ); + if ( index !== name ) { + props[ name ] = props[ index ]; + delete props[ index ]; + } - if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) { - jQuery._data( elem, "olddisplay", display ); - } - } - } + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + replace = hooks.expand( props[ name ] ); + delete props[ name ]; - // Set the display of the elements in a second loop - // to avoid the constant reflow - for ( i = 0; i < j; i++ ) { - if ( this[i].style ) { - this[i].style.display = "none"; + // not quite $.extend, this wont overwrite keys already present. + // also - reusing 'index' from above because we have the correct "name" + for ( index in replace ) { + if ( index in props ) { + continue; } + props[ index ] = replace[ index ]; } + } + } +} - return this; +jQuery.Animation = jQuery.extend( Animation, { + + tweener: function( props, callback ) { + if ( jQuery.isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.split(" "); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length ; index++ ) { + prop = props[ index ]; + tweeners[ prop ] = tweeners[ prop ] || []; + tweeners[ prop ].unshift( callback ); } }, - // Save the old toggle function - _toggle: jQuery.fn.toggle, + prefilter: function( callback, prepend ) { + if ( prepend ) { + animationPrefilters.unshift( callback ); + } else { + animationPrefilters.push( callback ); + } + } +}); - toggle: function( fn, fn2, callback ) { - var bool = typeof fn === "boolean"; +Animation.prefilter(function( elem, props, opts ) { + var index, value, + style = elem.style; + + // custom easing pass + opts.specialEasing = opts.specialEasing || {}; + for ( index in props ) { + value = props[ index ]; + if ( jQuery.isArray( value ) ) { + opts.specialEasing[ index ] = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + } - if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) { - this._toggle.apply( this, arguments ); + // height/width overflow pass + if ( elem.nodeType === 1 && ( props.height || props.width ) ) { + // Make sure that nothing sneaks out + // Record all 3 overflow attributes because IE does not + // change the overflow attribute when overflowX and + // overflowY are set to the same value + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - } else if ( fn == null || bool ) { - this.each(function() { - var state = bool ? fn : jQuery(this).is(":hidden"); - jQuery(this)[ state ? "show" : "hide" ](); - }); + // Set display property to inline-block for height/width + // animations on inline elements that are having width/height animated + if ( jQuery.css( elem, "display" ) === "inline" && + jQuery.css( elem, "float" ) === "none" ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( elem.nodeName ) === "inline" ) { + style.display = "inline-block"; + + } else { + style.zoom = 1; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + this.finish(function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + }); + } +}); +// special case show/hide prefilter +Animation.prefilter(function( elem, props, opts ) { + var index, prop, value, length, dataShow, tween, + orig = {}, + handled = [], + hidden = jQuery( elem ).is(":hidden"); + + for ( index in props ) { + value = props[ index ]; + if ( rfxtypes.exec( value ) ) { + delete props[ index ]; + if ( value === ( hidden ? "hide" : "show" ) ) { + continue; + } + handled.push( index ); + } + } + + length = handled.length; + if ( length ) { + dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); + if ( hidden ) { + showHide([ elem ], true ); } else { - this.animate(genFx("toggle", 3), fn, fn2, callback); + this.finish(function() { + showHide([ elem ]); + }); } + this.finish(function() { + var prop; + jQuery.removeData( elem, "fxshow", true ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + }); + for ( index = 0 ; index < length ; index++ ) { + prop = handled[ index ]; + tween = this.createTween( prop, hidden ? dataShow[ prop ] : 0 ); + orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop ); + + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = tween.start; + if ( hidden ) { + tween.end = tween.start; + tween.start = prop === "width" || prop === "height" ? 1 : 0; + } + } + } + } +}); - return this; +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || "swing"; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; - fadeTo: function( speed, to, easing, callback ) { - return this.filter(":hidden").css("opacity", 0).show().end() - .animate({opacity: to}, speed, easing, callback); + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; - animate: function( prop, speed, easing, callback ) { - var optall = jQuery.speed( speed, easing, callback ); + this.pos = eased = jQuery.easing[ this.easing ]( percent, this.options.duration * percent, 0, 1, this.options.duration ); + this.now = ( this.end - this.start ) * eased + this.start; - if ( jQuery.isEmptyObject( prop ) ) { - return this.each( optall.complete, [ false ] ); + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); } - // Do not change referenced properties as per-property easing will be lost - prop = jQuery.extend( {}, prop ); - - function doAnimation() { - // XXX 'this' does not always have a nodeName when running the - // test suite - - if ( optall.queue === false ) { - jQuery._mark( this ); - } + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; - var opt = jQuery.extend( {}, optall ), - isElement = this.nodeType === 1, - hidden = isElement && jQuery(this).is(":hidden"), - name, val, p, e, hooks, replace, - parts, start, end, unit, - method; - - // will store per property easing and be used to determine when an animation is complete - opt.animatedProperties = {}; - - // first pass over propertys to expand / normalize - for ( p in prop ) { - name = jQuery.camelCase( p ); - if ( p !== name ) { - prop[ name ] = prop[ p ]; - delete prop[ p ]; - } +Tween.prototype.init.prototype = Tween.prototype; - if ( ( hooks = jQuery.cssHooks[ name ] ) && "expand" in hooks ) { - replace = hooks.expand( prop[ name ] ); - delete prop[ name ]; +Tween.propHooks = { + _default: { + get: function( tween ) { + var parsed, result; - // not quite $.extend, this wont overwrite keys already present. - // also - reusing 'p' from above because we have the correct "name" - for ( p in replace ) { - if ( ! ( p in prop ) ) { - prop[ p ] = replace[ p ]; - } - } - } + if ( tween.elem[ tween.prop ] != null && + (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { + return tween.elem[ tween.prop ]; } - for ( name in prop ) { - val = prop[ name ]; - // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default) - if ( jQuery.isArray( val ) ) { - opt.animatedProperties[ name ] = val[ 1 ]; - val = prop[ name ] = val[ 0 ]; - } else { - opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing'; - } + result = jQuery.css( tween.elem, tween.prop ); + // Empty strings, null, undefined and "auto" are converted to 0, + // complex values such as "rotate(1rad)" are returned as is, + // simple values such as "10px" are parsed to Float. + return isNaN( parsed = parseFloat( result ) ) ? + !result || result === "auto" ? 0 : result : parsed; + }, + set: function( tween ) { + // use step hook for back compat - use cssHook if its there - use .style if its + // available and use plain properties where available + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; - if ( val === "hide" && hidden || val === "show" && !hidden ) { - return opt.complete.call( this ); - } +function showHide( elements, show ) { + var elem, display, + values = [], + index = 0, + length = elements.length; - if ( isElement && ( name === "height" || name === "width" ) ) { - // Make sure that nothing sneaks out - // Record all 3 overflow attributes because IE does not - // change the overflow attribute when overflowX and - // overflowY are set to the same value - opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; - - // Set display property to inline-block for height/width - // animations on inline elements that are having width/height animated - if ( jQuery.css( this, "display" ) === "inline" && - jQuery.css( this, "float" ) === "none" ) { - - // inline-level elements accept inline-block; - // block-level elements need to be inline with layout - if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) { - this.style.display = "inline-block"; - - } else { - this.style.zoom = 1; - } - } - } + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + values[ index ] = jQuery._data( elem, "olddisplay" ); + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && elem.style.display === "none" ) { + elem.style.display = ""; } - if ( opt.overflow != null ) { - this.style.overflow = "hidden"; + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( (elem.style.display === "" && jQuery.css( elem, "display" ) === "none") || + !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); } + } else { + display = jQuery.css( elem, "display" ); - for ( p in prop ) { - e = new jQuery.fx( this, opt, p ); - val = prop[ p ]; - - if ( rfxtypes.test( val ) ) { + if ( !values[ index ] && display !== "none" ) { + jQuery._data( elem, "olddisplay", display ); + } + } + } - // Tracks whether to show or hide based on private - // data attached to the element - method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 ); - if ( method ) { - jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" ); - e[ method ](); - } else { - e[ val ](); - } + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } - } else { - parts = rfxnum.exec( val ); - start = e.cur(); + return elements; +} - if ( parts ) { - end = parseFloat( parts[2] ); - unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" ); +jQuery.fn.extend({ + show: function( speed, easing, callback ) { + return speed || speed === 0 ? + this.animate( genFx( "show", true ), speed, easing, callback ) : + showHide( this, true ); + }, + hide: function( speed, easing, callback ) { + return speed || speed === 0 ? + this.animate( genFx( "hide", true ), speed, easing, callback ) : + showHide( this ); + }, + toggle: function( fn, fn2, callback ) { + var bool = typeof fn === "boolean"; - // We need to compute starting value - if ( unit !== "px" ) { - jQuery.style( this, p, (end || 1) + unit); - start = ( (end || 1) / e.cur() ) * start; - jQuery.style( this, p, start + unit); - } + if ( jQuery.isFunction( fn ) && jQuery.isFunction( fn2 ) ) { + oldToggle.apply( this, arguments ); - // If a +=/-= token was provided, we're doing a relative animation - if ( parts[1] ) { - end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start; - } + } else if ( fn == null || bool ) { + this.each(function() { + var state = bool ? fn : jQuery( this ).is(":hidden"); + showHide([ this ], state ); + }); - e.custom( start, end, unit ); + } else { + this.animate( genFx( "toggle", true ), fn, fn2, callback ); + } - } else { - e.custom( start, val, "" ); - } - } - } + return this; + }, + fadeTo: function( speed, to, easing, callback ) { + return this.filter(":hidden").css("opacity", 0).show().end() + .animate({opacity: to}, speed, easing, callback); + }, + animate: function( prop, speed, easing, callback ) { + var optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + Animation( this, prop, optall ).finish( optall.complete ); + }; - // For JS strict compliance - return true; + if ( jQuery.isEmptyObject( prop ) ) { + return this.each( optall.complete, [ false ] ); } + // Do not change referenced properties as per-property easing will be lost + prop = jQuery.extend( {}, prop ); + return optall.queue === false ? this.each( doAnimation ) : this.queue( optall.queue, doAnimation ); }, - stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( elem, data, index ) { + var hooks = data[ index ]; + jQuery.removeData( elem, index, true ); + hooks.stop( gotoEnd ); + }; + if ( typeof type !== "string" ) { gotoEnd = clearQueue; clearQueue = type; @@ -289,15 +502,9 @@ jQuery.fn.extend({ jQuery._unmark( true, this ); } - function stopQueue( elem, data, index ) { - var hooks = data[ index ]; - jQuery.removeData( elem, index, true ); - hooks.stop( gotoEnd ); - } - if ( type == null ) { for ( index in data ) { - if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { stopQueue( this, data, index ); } } @@ -307,13 +514,7 @@ jQuery.fn.extend({ for ( index = timers.length; index--; ) { if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { - if ( gotoEnd ) { - - // force the next step to be the last - timers[ index ]( true ); - } else { - timers[ index ].saveState(); - } + timers[ index ].anim.stop( gotoEnd ); hadTimers = true; timers.splice( index, 1 ); } @@ -327,35 +528,33 @@ jQuery.fn.extend({ } }); } - }); -// Animations created synchronously will run synchronously -function createFxNow() { - setTimeout( clearFxNow, 0 ); - return ( fxNow = jQuery.now() ); -} - -function clearFxNow() { - fxNow = undefined; -} - // Generate parameters to create a standard animation -function genFx( type, num ) { - var obj = {}; +function genFx( type, includeWidth ) { + var which, + attrs = { height: type }, + i = 0; + + // if we include width, step value is 1 to do all cssExpand values, + // if we don't include width, step value is 2 to skip over Left and Right + for( ; i < 4 ; i += 2 - includeWidth ) { + which = jQuery.cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } - jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() { - obj[ this ] = type; - }); + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } - return obj; + return attrs; } // Generate shortcuts for custom animations jQuery.each({ - slideDown: genFx( "show", 1 ), - slideUp: genFx( "hide", 1 ), - slideToggle: genFx( "toggle", 1 ), + slideDown: genFx("show"), + slideUp: genFx("hide"), + slideToggle: genFx("toggle"), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } @@ -365,285 +564,91 @@ jQuery.each({ }; }); -jQuery.extend({ - speed: function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - jQuery.isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing - }; - - opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : - opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; - - // normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function( noUnmark ) { - if ( jQuery.isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } else if ( noUnmark !== false ) { - jQuery._unmark( this ); - } - }; - - return opt; - }, - - easing: { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return ( -Math.cos( p*Math.PI ) / 2 ) + 0.5; - } - }, +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; - timers: [], + opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; - fx: function( elem, options, prop ) { - this.options = options; - this.elem = elem; - this.prop = prop; - - options.orig = options.orig || {}; + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; } -}); - -jQuery.fx.prototype = { - // Simple function for setting a style value - update: function() { - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this ); - }, - - // Get the current size - cur: function() { - if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) { - return this.elem[ this.prop ]; - } - - var parsed, - r = jQuery.css( this.elem, this.prop ); - // Empty strings, null, undefined and "auto" are converted to 0, - // complex values such as "rotate(1rad)" are returned as is, - // simple values such as "10px" are parsed to Float. - return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed; - }, - - // Start an animation from one number to another - custom: function( from, to, unit ) { - var self = this, - fx = jQuery.fx; - - this.startTime = fxNow || createFxNow(); - this.end = to; - this.now = this.start = from; - this.pos = this.state = 0; - this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); - - function t( gotoEnd ) { - return self.step( gotoEnd ); - } - - t.queue = this.options.queue; - t.elem = this.elem; - t.saveState = function() { - if ( jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) { - if ( self.options.hide ) { - jQuery._data( self.elem, "fxshow" + self.prop, self.start ); - } else if ( self.options.show ) { - jQuery._data( self.elem, "fxshow" + self.prop, self.end ); - } - } - }; + // Queueing + opt.old = opt.complete; - if ( t() && jQuery.timers.push(t) && !timerId ) { - timerId = setInterval( fx.tick, fx.interval ); + opt.complete = function( noUnmark ) { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); } - }, - - // Simple 'show' function - show: function() { - var dataShow = jQuery._data( this.elem, "fxshow" + this.prop ); - // Remember where we started, so that we can go back to it later - this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop ); - this.options.show = true; - - // Begin the animation - // Make sure that we start at a small width/height to avoid any flash of content - if ( dataShow !== undefined ) { - // This show is picking up where a previous hide or show left off - this.custom( this.cur(), dataShow ); - } else { - this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() ); + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } else if ( noUnmark !== false ) { + jQuery._unmark( this ); } + }; - // Start by showing the element - jQuery( this.elem ).show(); - }, - - // Simple 'hide' function - hide: function() { - // Remember where we started, so that we can go back to it later - this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop ); - this.options.hide = true; + return opt; +}; - // Begin the animation - this.custom( this.cur(), 0 ); +jQuery.easing = { + linear: function( p ) { + return p; }, - - // Each step of an animation - step: function( gotoEnd ) { - var p, n, complete, - t = fxNow || createFxNow(), - done = true, - elem = this.elem, - options = this.options; - - if ( gotoEnd || t >= options.duration + this.startTime ) { - this.now = this.end; - this.pos = this.state = 1; - this.update(); - - options.animatedProperties[ this.prop ] = true; - - for ( p in options.animatedProperties ) { - if ( options.animatedProperties[ p ] !== true ) { - done = false; - } - } - - if ( done ) { - // Reset the overflow - if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { - - jQuery.each( [ "", "X", "Y" ], function( index, value ) { - elem.style[ "overflow" + value ] = options.overflow[ index ]; - }); - } - - // Hide the element if the "hide" operation was done - if ( options.hide ) { - jQuery( elem ).hide(); - } - - // Reset the properties, if the item has been hidden or shown - if ( options.hide || options.show ) { - for ( p in options.animatedProperties ) { - jQuery.style( elem, p, options.orig[ p ] ); - jQuery.removeData( elem, "fxshow" + p, true ); - // Toggle data is no longer needed - jQuery.removeData( elem, "toggle" + p, true ); - } - } - - // Execute the complete function - // in the event that the complete function throws an exception - // we must ensure it won't be called twice. #5684 - - complete = options.complete; - if ( complete ) { - - options.complete = false; - complete.call( elem ); - } - } - - return false; - - } else { - // classical easing cannot be used with an Infinity duration - if ( options.duration == Infinity ) { - this.now = t; - } else { - n = t - this.startTime; - this.state = n / options.duration; - - // Perform the easing function, defaults to swing - this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration ); - this.now = this.start + ( (this.end - this.start) * this.pos ); - } - // Perform the next step of the animation - this.update(); - } - - return true; + swing: function( p ) { + return 0.5 - Math.cos( p*Math.PI ) / 2; } }; -jQuery.extend( jQuery.fx, { - tick: function() { - var timer, - timers = jQuery.timers, - i = 0; - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - // Checks the timer has not already been removed - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); +jQuery.timers = []; +jQuery.fx = Tween.prototype.init; +jQuery.fx.tick = function() { + var timer, + timers = jQuery.timers, + i = 0; + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); } - }, + } - interval: 13, + if ( !timers.length ) { + jQuery.fx.stop(); + } +}; - stop: function() { - clearInterval( timerId ); - timerId = null; - }, +jQuery.fx.timer = function( timer ) { + if ( timer() && jQuery.timers.push( timer ) && !timerId ) { + timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); + } +}; - speeds: { - slow: 600, - fast: 200, - // Default speed - _default: 400 - }, +jQuery.fx.interval = 13; - step: { - opacity: function( fx ) { - jQuery.style( fx.elem, "opacity", fx.now ); - }, +jQuery.fx.stop = function() { + clearInterval( timerId ); + timerId = null; +}; - _default: function( fx ) { - if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { - fx.elem.style[ fx.prop ] = fx.now + fx.unit; - } else { - fx.elem[ fx.prop ] = fx.now; - } - } - } -}); +jQuery.fx.speeds = { + slow: 600, + fast: 200, + // Default speed + _default: 400 +}; -// Ensure props that can't be negative don't go there on undershoot easing -jQuery.each( fxAttrs.concat.apply( [], fxAttrs ), function( i, prop ) { - // Exclude marginTop, marginLeft, marginBottom and marginRight from this list - if ( !rMarginProp.test( prop ) ) { - jQuery.fx.step[ prop ] = function( fx ) { - jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit ); - }; - } -}); +// Back Compat <1.8 extension point +jQuery.fx.step = {}; if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.animated = function( elem ) { @@ -655,47 +660,45 @@ if ( jQuery.expr && jQuery.expr.filters ) { // Try to restore the default display value of an element function defaultDisplay( nodeName ) { + if ( elemdisplay[ nodeName ] ) { + return elemdisplay[ nodeName ]; + } - if ( !elemdisplay[ nodeName ] ) { - - var body = document.body, - elem = jQuery( "<" + nodeName + ">" ).appendTo( body ), - display = elem.css( "display" ); - elem.remove(); - - // If the simple way fails, - // get element's real default display by attaching it to a temp iframe - if ( display === "none" || display === "" ) { - // No iframe to use yet, so create it - if ( !iframe ) { - iframe = document.createElement( "iframe" ); - iframe.frameBorder = iframe.width = iframe.height = 0; - } - - body.appendChild( iframe ); - - // Create a cacheable copy of the iframe document on first call. - // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML - // document to it; WebKit & Firefox won't allow reusing the iframe document. - if ( !iframeDoc || !iframe.createElement ) { - iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; - iframeDoc.write( ( jQuery.support.boxModel ? "" : "" ) + "" ); - iframeDoc.close(); - } - - elem = iframeDoc.createElement( nodeName ); - - iframeDoc.body.appendChild( elem ); - - display = jQuery.css( elem, "display" ); - body.removeChild( iframe ); + var elem = jQuery( "<" + nodeName + ">" ).appendTo( document.body ), + display = elem.css("display"); + elem.remove(); + + // If the simple way fails, + // get element's real default display by attaching it to a temp iframe + if ( display === "none" || display === "" ) { + // Use the already-created iframe if possible + iframe = document.body.appendChild( + iframe || jQuery.extend( document.createElement("iframe"), { + frameBorder: 0, + width: 0, + height: 0 + }) + ); + + // Create a cacheable copy of the iframe document on first call. + // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML + // document to it; WebKit & Firefox won't allow reusing the iframe document. + if ( !iframeDoc || !iframe.createElement ) { + iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; + iframeDoc.write( ( jQuery.support.boxModel ? "" : "" ) + "" ); + iframeDoc.close(); } - // Store the correct default display - elemdisplay[ nodeName ] = display; + elem = iframeDoc.body.appendChild( iframeDoc.createElement(nodeName) ); + + display = jQuery.css( elem, "display" ); + document.body.removeChild( iframe ); } - return elemdisplay[ nodeName ]; + // Store the correct default display + elemdisplay[ nodeName ] = display; + + return display; } })( jQuery ); diff --git a/test/unit/css.js b/test/unit/css.js index 6d0b39f81..a192c1b5b 100644 --- a/test/unit/css.js +++ b/test/unit/css.js @@ -37,13 +37,13 @@ test("css(String|Hash)", function() { div2.remove(); - // handle negative numbers by ignoring #1599, #4216 + // handle negative numbers by setting to zero #11604 jQuery("#nothiddendiv").css( {width: 1, height: 1} ); var width = parseFloat(jQuery("#nothiddendiv").css("width")), height = parseFloat(jQuery("#nothiddendiv").css("height")); jQuery("#nothiddendiv").css({ width: -1, height: -1 }); - equal( parseFloat(jQuery("#nothiddendiv").css("width")), width, "Test negative width ignored"); - equal( parseFloat(jQuery("#nothiddendiv").css("height")), height, "Test negative height ignored"); + equal( parseFloat(jQuery("#nothiddendiv").css("width")), 0, "Test negative width set to 0"); + equal( parseFloat(jQuery("#nothiddendiv").css("height")), 0, "Test negative height set to 0"); equal( jQuery("
").css("display"), "none", "Styles on disconnected nodes"); diff --git a/test/unit/effects.js b/test/unit/effects.js index 25c9834e4..74f83e7c4 100644 --- a/test/unit/effects.js +++ b/test/unit/effects.js @@ -792,7 +792,7 @@ jQuery.checkOverflowDisplay = function(){ start(); }; -test( "jQuery.fx.prototype.cur()", 6, function() { +test( "jQuery.fx.prototype.cur() - <1.8 Back Compat", 7, function() { var div = jQuery( "
" ).appendTo( "#qunit-fixture" ).css({ color: "#ABC", border: "5px solid black", @@ -814,6 +814,8 @@ test( "jQuery.fx.prototype.cur()", 6, function() { // backgroundPosition actually returns 0% 0% in most browser // this fakes a "" return + // hook now gets called twice because Tween will grab the current + // value as it is being newed jQuery.cssHooks.backgroundPosition = { get: function() { ok( true, "hook used" ); @@ -1387,3 +1389,62 @@ test("animate will scale margin properties individually", function() { }); start(); }); + +// Start 1.8 Animation tests +asyncTest( "jQuery.Animation( object, props, opts )", 1, function() { + var testObject = { + foo: 0, + bar: 1, + width: 100 + }, + testDest = { + foo: 1, + bar: 0, + width: 200 + }; + + jQuery.Animation( testObject, testDest, { duration: 1 }) + .done( function() { + deepEqual( testObject, testDest, "Animated foo and bar" ); + start(); + }); +}); + +asyncTest( "Animate Option: step: function( percent, tween )", 1, function() { + var counter = {}; + jQuery( "#foo" ).animate({ + prop1: 1, + prop2: 2, + prop3: 3 + }, { + duration: 1, + step: function( value, tween ) { + calls = counter[ tween.prop ] = counter[ tween.prop ] || []; + calls.push( value ); + } + }).queue( function( next ) { + deepEqual( counter, { + prop1: [0, 1], + prop2: [0, 2], + prop3: [0, 3] + }, "Step function was called once at 0% and once at 100% for each property"); + next(); + start(); + }); +}); + + +asyncTest( "Animate callbacks have correct context", 2, function() { + var foo = jQuery( "#foo" ); + foo.animate({ + height: 10 + }, 10, function() { + equal( foo[ 0 ], this, "Complete callback after stop(true) `this` is element" ); + }).stop( true, true ); + foo.animate({ + height: 100 + }, 10, function() { + equal( foo[ 0 ], this, "Complete callback `this` is element" ); + start(); + }); +});