diff --git a/css/structure/jquery.mobile.panel.css b/css/structure/jquery.mobile.panel.css index 4b6304600e3..6c882e2f88e 100644 --- a/css/structure/jquery.mobile.panel.css +++ b/css/structure/jquery.mobile.panel.css @@ -69,6 +69,7 @@ /* Animate class is added to panel, wrapper and fixed toolbars */ .ui-panel-animate { -webkit-transition: -webkit-transform 300ms ease; + -webkit-transition-duration: 300ms; -moz-transition: -moz-transform 300ms ease; transition: transform 300ms ease; } diff --git a/js/index.php b/js/index.php index 63245a7be07..78666d8327b 100644 --- a/js/index.php +++ b/js/index.php @@ -22,6 +22,7 @@ 'jquery.mobile.defaults.js', 'jquery.mobile.helpers.js', 'jquery.mobile.data.js', + 'jquery.mobile.animationComplete.js', 'widgets/page.js', 'widgets/page.dialog.js', 'widgets/loader.js', diff --git a/js/jquery.mobile.animationComplete.js b/js/jquery.mobile.animationComplete.js new file mode 100644 index 00000000000..66606a01aec --- /dev/null +++ b/js/jquery.mobile.animationComplete.js @@ -0,0 +1,105 @@ +//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); +//>>description: A handler for css transition & animation end events to ensure callback is executed +//>>label: Animation Complete +//>>group: Core +define( [ + "jquery" +], function( jQuery ) { +//>>excludeEnd("jqmBuildExclude"); +(function( $, undefined ) { + var props = { + "animation": {}, + "transition": {} + }, + testElement = document.createElement( "a" ), + vendorPrefixes = [ "", "webkit-", "moz-", "o-" ]; + + $.each( [ "animation", "transition" ], function( i, test ) { + + // Get correct name for test + var testName = ( i === 0 ) ? test + "-" + "name" : test; + + $.each( vendorPrefixes, function( j, prefix ) { + if ( testElement.style[ $.camelCase( prefix + testName ) ] !== undefined ) { + props[ test ][ "prefix" ] = prefix; + return false; + } + }); + + // Set event and duration names for later use + props[ test ][ "duration" ] = + $.camelCase( props[ test ][ "prefix" ] + test + "-" + "duration" ); + props[ test ][ "event" ] = + $.camelCase( props[ test ][ "prefix" ] + test + "-" + "end" ); + + // All lower case if not a vendor prop + if ( props[ test ][ "prefix" ] === "" ) { + props[ test ][ "duration" ] = props[ test ][ "duration" ].toLowerCase(); + props[ test ][ "event" ] = props[ test ][ "event" ].toLowerCase(); + } + }); + + // If a valid prefix was found then the it is supported by the browser + $.support.cssTransitions = ( props[ "transition" ][ "prefix" ] !== undefined ); + $.support.cssAnimations = ( props[ "animation" ][ "prefix" ] !== undefined ); + + // Remove the testElement + $( testElement ).remove(); + + // Animation complete callback + $.fn.animationComplete = function( callback, type, fallbackTime ) { + var timer, duration, + that = this, + animationType = ( !type || type === "animation" ) ? "animation" : "transition"; + + // Make sure selected type is supported by browser + if ( ( $.support.cssTransitions && animationType === "transition" ) || + ( $.support.cssAnimations && animationType === "animation" ) ) { + + // If a fallback time was not passed set one + if ( fallbackTime === undefined ) { + + // Make sure the was not bound to document before checking .css + if ( $( this ).context !== document ) { + + // Parse the durration since its in second multiple by 1000 for milliseconds + // Multiply by 3 to make sure we give the animation plenty of time. + duration = parseFloat( + $( this ).css( props[ animationType ].duration ) + ) * 3000; + } + + // If we could not read a duration use the default + if ( duration === 0 || duration === undefined ) { + duration = $.fn.animationComplete.default; + } + } + + // Sets up the fallback if event never comes + timer = setTimeout( function() { + $( that ).off( props[ animationType ].event ); + callback.apply( that ); + }, duration ); + + // Bind the event + return $( this ).one( props[ animationType ].event, function() { + + // Clear the timer so we dont call callback twice + clearTimeout( timer ); + callback.call( this, arguments ); + }); + } else { + + // CSS animation / transitions not supported + // Defer execution for consistency between webkit/non webkit + setTimeout( $.proxy( callback, this ), 0 ); + return $( this ); + } + }; + + // Allow default callback to be configured on mobileInit + $.fn.animationComplete.default = 1000; +})( jQuery ); +//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); +}); +//>>excludeEnd("jqmBuildExclude"); diff --git a/js/jquery.mobile.js b/js/jquery.mobile.js index 57d62174ffb..bda1d1d081d 100644 --- a/js/jquery.mobile.js +++ b/js/jquery.mobile.js @@ -11,6 +11,7 @@ define([ "./navigation/method", "./transitions/handlers", "./transitions/visuals", + "./jquery.mobile.animationComplete.js", "./jquery.mobile.navigation", "./jquery.mobile.degradeInputs", "./widgets/page.dialog", diff --git a/js/jquery.mobile.navigation.js b/js/jquery.mobile.navigation.js index 661ad5a1829..588f185b94e 100644 --- a/js/jquery.mobile.navigation.js +++ b/js/jquery.mobile.navigation.js @@ -13,6 +13,7 @@ define( [ "./jquery.mobile.events", "./jquery.mobile.support", "jquery-plugins/jquery.hashchange", + "./jquery.mobile.animationComplete", "./widgets/pagecontainer", "./widgets/page", "./transitions/handlers" ], function( jQuery ) { @@ -86,7 +87,7 @@ define( [ } }; - //direct focus to the page title, or otherwise first focusable element + // Direct focus to the page title, or otherwise first focusable element $.mobile.focusPage = function ( page ) { var autofocus = page.find( "[autofocus]" ), pageTitle = page.find( ".ui-title:eq(0)" ); @@ -108,19 +109,7 @@ define( [ return transition; }; - /* exposed $.mobile methods */ - - //animation complete callback - $.fn.animationComplete = function( callback ) { - if ( $.support.cssTransitions ) { - return $( this ).one( "webkitAnimationEnd animationend", callback ); - } - else{ - // defer execution for consistency between webkit/non webkit - setTimeout( callback, 0 ); - return $( this ); - } - }; + // Exposed $.mobile methods $.mobile.changePage = function( to, options ) { $.mobile.pageContainer.pagecontainer( "change", to, options ); diff --git a/js/jquery.mobile.support.js b/js/jquery.mobile.support.js index 6ef4561f044..f8d4d846c9f 100644 --- a/js/jquery.mobile.support.js +++ b/js/jquery.mobile.support.js @@ -23,43 +23,10 @@ var fakeBody = $( "" ).prependTo( "html" ), fbCSS = fakeBody[ 0 ].style, vendors = [ "Webkit", "Moz", "O" ], webos = "palmGetResource" in window, //only used to rule out scrollTop - opera = window.opera, operamini = window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]", bb = window.blackberry && !propExists( "-webkit-transform" ), //only used to rule out box shadow, as it's filled opaque on BB 5 and lower nokiaLTE7_3; -function validStyle( prop, value, check_vend ) { - var div = document.createElement( "div" ), - uc = function( txt ) { - return txt.charAt( 0 ).toUpperCase() + txt.substr( 1 ); - }, - vend_pref = function( vend ) { - if ( vend === "" ) { - return ""; - } else { - return "-" + vend.charAt( 0 ).toLowerCase() + vend.substr( 1 ) + "-"; - } - }, - check_style = function( vend ) { - var vend_prop = vend_pref( vend ) + prop + ": " + value + ";", - uc_vend = uc( vend ), - propStyle = uc_vend + ( uc_vend === "" ? prop : uc( prop ) ); - - div.setAttribute( "style", vend_prop ); - - if ( !!div.style[ propStyle ] ) { - ret = true; - } - }, - check_vends = check_vend ? check_vend : vendors, - i, ret; - - for( i = 0; i < check_vends.length; i++ ) { - check_style( check_vends[i] ); - } - return !!ret; -} - // inline SVG support test function inlineSVG() { // Thanks Modernizr & Erik Dahlstrom @@ -206,10 +173,6 @@ function fixedPosition() { } $.extend( $.support, { - cssTransitions: "WebKitTransitionEvent" in window || - validStyle( "transition", "height 100ms linear", [ "Webkit", "Moz", "" ] ) && - !$.mobile.browser.oldIE && !opera, - // Note, Chrome for iOS has an extremely quirky implementation of popstate. // We've chosen to take the shortest path to a bug fix here for issue #5426 // See the following link for information about the regex chosen @@ -224,7 +187,6 @@ $.extend( $.support, { cssPseudoElement: !!propExists( "content" ), touchOverflow: !!propExists( "overflowScrolling" ), cssTransform3d: transform3dTest(), - cssAnimations: !!propExists( "animationName" ), boxShadow: !!propExists( "boxShadow" ) && !bb, fixedPosition: fixedPosition(), scrollTop: ("pageXOffset" in window || diff --git a/js/transitions/serial.js b/js/transitions/serial.js index 26b11fdf118..a3ea42b2fd1 100644 --- a/js/transitions/serial.js +++ b/js/transitions/serial.js @@ -3,7 +3,7 @@ //>>label: Transition Serial //>>group: Transitions -define( [ "jquery", "./transition" ], function( jQuery ) { +define( [ "jquery", "../jquery.mobile.animationComplete", "./transition" ], function( jQuery ) { //>>excludeEnd("jqmBuildExclude"); (function( $ ) { diff --git a/js/transitions/transition.js b/js/transitions/transition.js index bd16bb669d4..4a26351bb77 100644 --- a/js/transitions/transition.js +++ b/js/transitions/transition.js @@ -10,6 +10,7 @@ define( [ "jquery", // TODO event.special.scrollstart "../events/touch", + "../jquery.mobile.animationComplete", // TODO $.mobile.focusPage reference "../jquery.mobile.navigation" ], function( jQuery ) { @@ -109,17 +110,15 @@ define( [ "jquery", } }); - if ( !none ) { - this.$to.animationComplete( $.proxy(function() { - this.doneIn(); - }, this )); - } - this.$to .removeClass( this.toPreClass ) .addClass( this.name + " in " + reverseClass ); - if ( none ) { + if ( !none ) { + this.$to.animationComplete( $.proxy(function() { + this.doneIn(); + }, this )); + } else { this.doneIn(); } diff --git a/js/widgets/fixedToolbar.js b/js/widgets/fixedToolbar.js index 431bc5532e5..f40d099daaa 100644 --- a/js/widgets/fixedToolbar.js +++ b/js/widgets/fixedToolbar.js @@ -5,7 +5,7 @@ //>>css.structure: ../css/structure/jquery.mobile.fixedToolbar.css //>>css.theme: ../css/themes/default/jquery.mobile.theme.css -define( [ "jquery", "../jquery.mobile.widget", "../jquery.mobile.core", "../jquery.mobile.navigation", "./page","./toolbar","../jquery.mobile.zoom" ], function( jQuery ) { +define( [ "jquery", "../jquery.mobile.widget", "../jquery.mobile.core", "../jquery.mobile.animationComplete", "../jquery.mobile.navigation", "./page","./toolbar","../jquery.mobile.zoom" ], function( jQuery ) { //>>excludeEnd("jqmBuildExclude"); (function( $, undefined ) { diff --git a/js/widgets/forms/autogrow.js b/js/widgets/forms/autogrow.js index 1efcaa07c7f..4983624b4f0 100644 --- a/js/widgets/forms/autogrow.js +++ b/js/widgets/forms/autogrow.js @@ -62,10 +62,11 @@ define( [ if ( event.type !== "popupbeforeposition" ) { this.element .addClass( "ui-textinput-autogrow-resize" ) - .one( "transitionend webkitTransitionEnd oTransitionEnd", + .animationComplete( $.proxy( function() { this.element.removeClass( "ui-textinput-autogrow-resize" ); - }, this ) ); + }, this ), + "transition" ); } this._timeout(); } diff --git a/js/widgets/panel.js b/js/widgets/panel.js index 45d38d9fc2c..24458100e95 100644 --- a/js/widgets/panel.js +++ b/js/widgets/panel.js @@ -308,7 +308,7 @@ $.widget( "mobile.panel", { } if ( !immediate && $.support.cssTransform3d && !!o.animate ) { - self.document.on( self._transitionEndEvents, complete ); + self.element.animationComplete( complete, "transition" ); } else { setTimeout( complete, 0 ); } @@ -340,7 +340,6 @@ $.widget( "mobile.panel", { } }, complete = function() { - self.document.off( self._transitionEndEvents, complete ); if ( o.display !== "overlay" ) { self._wrapper().addClass( o.classes.pageContentPrefix + "-open" ); @@ -372,11 +371,6 @@ $.widget( "mobile.panel", { o = this.options, _closePanel = function() { - if ( !immediate && $.support.cssTransform3d && !!o.animate ) { - self.document.on( self._transitionEndEvents, complete ); - } else { - setTimeout( complete, 0 ); - } self.element.removeClass( o.classes.panelOpen ); @@ -385,13 +379,17 @@ $.widget( "mobile.panel", { self._fixedToolbars().removeClass( self._pageContentOpenClasses ); } + if ( !immediate && $.support.cssTransform3d && !!o.animate ) { + self.element.animationComplete( complete, "transition" ); + } else { + setTimeout( complete, 0 ); + } + if ( self._modal ) { self._modal.removeClass( self._modalOpenClasses ); } }, complete = function() { - self.document.off( self._transitionEndEvents, complete ); - if ( o.theme && o.display !== "overlay" ) { self._page().parent().removeClass( o.classes.pageContainer + "-themed " + o.classes.pageContainer + "-" + o.theme ); } @@ -430,8 +428,6 @@ $.widget( "mobile.panel", { this[ this._open ? "close" : "open" ](); }, - _transitionEndEvents: "webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd", - _destroy: function() { var otherPanels, o = this.options, @@ -465,10 +461,6 @@ $.widget( "mobile.panel", { this.document.off( "panelopen panelclose" ); - if ( this._open ) { - this.document.off( this._transitionEndEvents ); - $.mobile.resetActivePageHeight(); - } } if ( this._open ) { @@ -483,8 +475,7 @@ $.widget( "mobile.panel", { .off( "panelbeforeopen" ) .off( "panelhide" ) .off( "keyup.panel" ) - .off( "updatelayout" ) - .off( this._transitionEndEvents ); + .off( "updatelayout" ); if ( this._modal ) { this._modal.remove(); diff --git a/js/widgets/popup.js b/js/widgets/popup.js index 685a7ef86a7..3edaf2e482b 100644 --- a/js/widgets/popup.js +++ b/js/widgets/popup.js @@ -21,6 +21,7 @@ define( [ "../navigation/history", "../navigation/navigator", "../navigation/method", + "../jquery.mobile.animationComplete", "../jquery.mobile.navigation", "jquery-plugins/jquery.hashchange" ], function( jQuery ) { //>>excludeEnd("jqmBuildExclude"); @@ -542,9 +543,9 @@ $.widget( "mobile.popup", { } if ( this._fallbackTransition ) { this._ui.container - .animationComplete( $.proxy( args.prerequisites.container, "resolve" ) ) .addClass( args.containerClassToAdd ) - .removeClass( args.classToRemove ); + .removeClass( args.classToRemove ) + .animationComplete( $.proxy( args.prerequisites.container, "resolve" ) ); return; } } diff --git a/tests/integration/animation-complete/animationComplete.js b/tests/integration/animation-complete/animationComplete.js new file mode 100644 index 00000000000..56942030226 --- /dev/null +++ b/tests/integration/animation-complete/animationComplete.js @@ -0,0 +1,220 @@ +/* + * mobile AnimationComplete integration tests + */ +(function($){ + var oldTransitions, oldAnimations; + module( "Callbacks: Event", { + teardown: function() { + $( "#transition-test" ) + .removeClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ); + $( "#animation-test" ).removeClass( "in" ); + } + }); + asyncTest( "Make sure callback is executed and is cleared by actual event", function() { + expect( 2 ); + var transitionComplete = false, + animationComplete = false; + + $( "#transition-test" ) + .addClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ) + .animationComplete(function() { + transitionComplete = true; + }, "transition" ); + + $( "#animation-test" ).addClass( "in" ).animationComplete(function() { + animationComplete = true; + }); + + window.setTimeout(function() { + ok( transitionComplete, "transition completed" ); + ok( animationComplete, "animation completed" ); + start(); + }, 800 ); + }); + module( "Callbacks: fallback", { + teardown: function() { + $( "#transition-test" ) + .removeClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ); + $( "#animation-test" ).removeClass( "in" ); + } + }); + asyncTest( "Make sure callback is executed by fall back when no animation", function() { + expect( 2 ); + var transitionComplete = false, + animationComplete = false; + + $( "#transition-test" ).animationComplete(function() { + transitionComplete = true; + }, "transition" ); + + $( "#animation-test" ).animationComplete(function() { + animationComplete = true; + }); + + window.setTimeout(function() { + ok( transitionComplete, "transition callback called" ); + ok( animationComplete, "animation callback called" ); + start(); + }, 1200 ); + }); + module( "Callbacks: No support", { + setup: function() { + oldTransitions = $.support.cssTransitions, + oldAnimations = $.support.cssAnimations; + + $.support.cssAnimations = false; + $.support.cssTransitions = false; + }, + teardown: function() { + $.support.cssTransitions = oldTransitions; + $.support.cssAnimations = oldAnimations; + $( "#transition-test" ) + .removeClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ); + $( "#animation-test" ).removeClass( "in" ); + } + }); + asyncTest( "call back executes immeditly when animations not supported on device", function() { + expect( 2 ); + var transitionComplete = false, + animationComplete = false; + + $( "#transition-test" ) + .addClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ) + .animationComplete(function() { + transitionComplete = true; + }, "transition" ); + + $( "animation-test" ).addClass( "in" ).animationComplete(function() { + animationComplete = true; + }); + + window.setTimeout(function() { + ok( transitionComplete, "transition callback called" ); + ok( animationComplete, "animation callback called" ); + }, 10 ); + + window.setTimeout(function() { + start(); + }, 800 ); + }); + module( "Event Bindings", { + teardown: function() { + $( "#transition-test" ) + .removeClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ); + $( "#animation-test" ).removeClass( "in" ); + } + }); + asyncTest( "Ensure at most one event is bound", function() { + expect( 2 ); + + $( "#transition-test" ) + .addClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ) + .animationComplete(function() { + transitionComplete = true; + }, "transition" ); + $( "#animation-test" ).addClass( "in" ).animationComplete(function() { + animationComplete = true; + }); + ok( Object.keys( $._data( $("#animation-test")[0], "events" ) ).length === 1, + "Only one animation event" ); + + ok( Object.keys( $._data( $("#transition-test")[0], "events" ) ).length === 1, + "Only one transition event" ); + window.setTimeout(function() { + start(); + }, 800 ); + }); + module( "Event Bindings: no animation support", { + setup: function() { + oldTransitions = $.support.cssTransitions, + oldAnimations = $.support.cssAnimations; + + $.support.cssAnimations = false; + $.support.cssTransitions = false; + }, + teardown: function() { + $.support.cssTransitions = oldTransitions; + $.support.cssAnimations = oldAnimations; + $( "#transition-test" ) + .removeClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ); + $( "#animation-test" ).removeClass( "in" ); + } + }); + asyncTest( "Make sure no bidnings when no cssanimation support", function() { + expect( 2 ); + var transitionComplete = false, + animationComplete = false; + + window.setTimeout(function() { + $( "#transition-test" ).animationComplete(function() { + transitionComplete = true; + }, "transition" ); + + $( "#animation-test" ).animationComplete(function() { + animationComplete = true; + }); + ok( $._data( $("#animation-test")[0], "events" ) === undefined, + "no animation bindings remain" ); + ok( $._data( $("#transition-test")[0], "events" ) === undefined, + "no transition bindings remain" ); + start(); + }, 800 ); + }); + module( "Event Removal: event", { + teardown: function() { + $( "#transition-test" ) + .removeClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ); + $( "#animation-test" ).removeClass( "in" ); + } + }); + asyncTest( "Make sure no bidnings remain after event", function() { + expect( 2 ); + var transitionComplete = false, + animationComplete = false; + + $( "#transition-test" ) + .addClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ) + .animationComplete(function() { + transitionComplete = true; + }, "transition" ); + + $( "#animation-test" ).addClass( "in" ).animationComplete(function() { + animationComplete = true; + }); + window.setTimeout(function(){ + ok( $._data( $("#animation-test")[0], "events" ) === undefined, + "no animation bindings remain" ); + ok( $._data( $("#transition-test")[0], "events" ) === undefined, + "no transition bindings remain" ); + start(); + }, 800 ); + }); + module( "Event Removal: fallback", { + teardown: function() { + $( "#transition-test" ) + .removeClass( "ui-panel-animate ui-panel-position-left ui-panel-display-overlay" ); + $( "#animation-test" ).removeClass( "in" ); + } + }); + asyncTest( "Make sure no bidnings remain after fallback", function() { + expect( 2 ); + var transitionComplete = false, + animationComplete = false; + + $( "#transition-test" ).animationComplete(function() { + transitionComplete = true; + }, "transition" ); + + $( "#animation-test" ).animationComplete(function() { + animationComplete = true; + }); + + window.setTimeout(function(){ + ok( $._data( $("#animation-test")[0], "events" ) === undefined, + "no animation bindings remain" ); + ok( $._data( $("#transition-test")[0], "events" ) === undefined, + "no transition bindings remain" ); + start(); + }, 1200 ); + }); +})( jQuery ); \ No newline at end of file diff --git a/tests/integration/animation-complete/index.html b/tests/integration/animation-complete/index.html new file mode 100644 index 00000000000..393cd2322b0 --- /dev/null +++ b/tests/integration/animation-complete/index.html @@ -0,0 +1,30 @@ + + + + + + jQuery Mobile animationComplete Test + + + + + + + + + + + + + + +
+
+
+ + \ No newline at end of file diff --git a/tests/unit/support/index.html b/tests/unit/support/index.html index 8ccd60fc67d..2994c1cedd9 100644 --- a/tests/unit/support/index.html +++ b/tests/unit/support/index.html @@ -15,6 +15,7 @@