Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

AnimationComplete: Add transition, fallbacks and remove memory leaks

Also no its own module including support tests for animations and
transitions

Fixes gh-5156
Fixes gh-6816
Fixes gh-6697
Fixes gh-6895
Fixes gh-6148
Closes gh-7001
  • Loading branch information...
commit 749c78e6407da683a720d15635bc938b3b864df2 1 parent bf9ce84
@arschmitz arschmitz authored
View
1  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;
}
View
1  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',
View
105 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;

default is a reserved keyword and this results in syntax error on a lot of old browsers including Android 2.3 and IE8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+ }
+
+ // 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");
View
1  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",
View
17 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 );
View
38 js/jquery.mobile.support.js
@@ -23,43 +23,10 @@ var fakeBody = $( "<body>" ).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 ||
View
2  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( $ ) {
View
13 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();
}
View
2  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 ) {
View
5 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();
}
View
25 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();
View
5 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;
}
}
View
220 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 );
View
30 tests/integration/animation-complete/index.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>jQuery Mobile animationComplete Test</title>
+
+ <script src="../../../external/requirejs/require.js"></script>
+ <script src="../../../js/requirejs.config.js"></script>
+ <script src="../../../js/jquery.tag.inserter.js"></script>
+ <script src="../../jquery.setNameSpace.js"></script>
+ <script src="../../jquery.testHelper.js"></script>
+ <script src="../../../external/qunit.js"></script>
+ <script>
+ $.testHelper.asyncLoad([
+ [ "jquery.mobile.animationComplete" ],
+ [ "animationComplete.js" ]
+ ]);
+ </script>
+ <link rel="stylesheet" href="../../../css/themes/default/jquery.mobile.css" />
+ <link rel="stylesheet" href="../../../external/qunit.css"/>
+
+ <script src="../../swarminject.js"></script>
+</head>
+<body>
+ <div id="qunit"></div>
+ <div id="animation-test" class="pop"></div>
+ <div id="transition-test"></div>
+</body>
+</html>
View
1  tests/unit/support/index.html
@@ -15,6 +15,7 @@
<script>
$.testHelper.asyncLoad([
[
+ "jquery.mobile.animationComplete",
"jquery.mobile.support"
],
[
Please sign in to comment.
Something went wrong with that request. Please try again.