-
Notifications
You must be signed in to change notification settings - Fork 27.3k
fix(ngAnimate): defer DOM operations for changing classes to postDigest #9283
Changes from all commits
c3ce5d4
6233f0c
43dcb36
a6ee572
3e2ad02
bf84fd1
e954d22
cbf7d92
6949e5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,9 +81,57 @@ var $AnimateProvider = ['$provide', function($provide) { | |
return this.$$classNameFilter; | ||
}; | ||
|
||
this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) { | ||
this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) { | ||
|
||
var currentDefer; | ||
|
||
function runAnimationPostDigest(fn) { | ||
var cancelFn, defer = $$q.defer(); | ||
defer.promise.$$cancelFn = function ngAnimateMaybeCancel() { | ||
cancelFn && cancelFn(); | ||
}; | ||
|
||
$rootScope.$$postDigest(function ngAnimatePostDigest() { | ||
cancelFn = fn(function ngAnimateNotifyComplete() { | ||
defer.resolve(); | ||
}); | ||
}); | ||
|
||
return defer.promise; | ||
} | ||
|
||
function resolveElementClasses(element, cache) { | ||
var toAdd = [], toRemove = []; | ||
|
||
var hasClasses = {}; | ||
forEach((element.attr('class') || '').replace(/\s+/g, ' ').split(' '), function(className) { | ||
hasClasses[className] = true; | ||
}); | ||
|
||
forEach(cache.classes, function(status, className) { | ||
var hasClass = hasClasses[className] === true; | ||
|
||
// If the most recent class manipulation (via $animate) was to remove the class, and the | ||
// element currently has the class, the class is scheduled for removal. Otherwise, if | ||
// the most recent class manipulation (via $animate) was to add the class, and the | ||
// element does not currently have the class, the class is scheduled to be added. | ||
if (status === false && hasClass) { | ||
toRemove.push(className); | ||
} else if (status === true && !hasClass) { | ||
toAdd.push(className); | ||
} | ||
}); | ||
|
||
|
||
return (toAdd.length + toRemove.length) > 0 && [toAdd.length && toAdd, toRemove.length && toRemove]; | ||
} | ||
|
||
function cachedClassManipulation(cache, classes, op) { | ||
for (var i=0, ii = classes.length; i < ii; ++i) { | ||
var className = classes[i]; | ||
cache[className] = op; | ||
} | ||
} | ||
|
||
function asyncPromise() { | ||
// only serve one instance of a promise in order to save CPU cycles | ||
if (!currentDefer) { | ||
|
@@ -187,13 +235,17 @@ var $AnimateProvider = ['$provide', function($provide) { | |
* @return {Promise} the animation callback promise | ||
*/ | ||
addClass : function(element, className) { | ||
return this.setClass(element, className, []); | ||
}, | ||
|
||
$$addClassImmediately : function addClassImmediately(element, className) { | ||
element = jqLite(element); | ||
className = !isString(className) | ||
? (isArray(className) ? className.join(' ') : '') | ||
: className; | ||
forEach(element, function (element) { | ||
jqLiteAddClass(element, className); | ||
}); | ||
return asyncPromise(); | ||
}, | ||
|
||
/** | ||
|
@@ -209,6 +261,11 @@ var $AnimateProvider = ['$provide', function($provide) { | |
* @return {Promise} the animation callback promise | ||
*/ | ||
removeClass : function(element, className) { | ||
return this.setClass(element, [], className); | ||
}, | ||
|
||
$$removeClassImmediately : function removeClassImmediately(element, className) { | ||
element = jqLite(element); | ||
className = !isString(className) | ||
? (isArray(className) ? className.join(' ') : '') | ||
: className; | ||
|
@@ -231,10 +288,53 @@ var $AnimateProvider = ['$provide', function($provide) { | |
* @param {string} remove the CSS class which will be removed from the element | ||
* @return {Promise} the animation callback promise | ||
*/ | ||
setClass : function(element, add, remove) { | ||
this.addClass(element, add); | ||
this.removeClass(element, remove); | ||
return asyncPromise(); | ||
setClass : function(element, add, remove, runSynchronously) { | ||
var self = this; | ||
var STORAGE_KEY = '$$animateClasses'; | ||
var createdCache = false; | ||
element = jqLite(element); | ||
|
||
if (runSynchronously) { | ||
|
||
// TODO(@caitp/@matsko): Remove undocumented `runSynchronously` parameter, and always | ||
// perform DOM manipulation asynchronously or in postDigest. | ||
self.$$addClassImmediately(element, add); | ||
self.$$removeClassImmediately(element, remove); | ||
return asyncPromise(); | ||
} | ||
|
||
var cache = element.data(STORAGE_KEY); | ||
if (!cache) { | ||
cache = { | ||
classes: {} | ||
}; | ||
createdCache = true; | ||
} | ||
|
||
var classes = cache.classes; | ||
|
||
add = isArray(add) ? add : add.split(' '); | ||
remove = isArray(remove) ? remove : remove.split(' '); | ||
cachedClassManipulation(classes, add, true); | ||
cachedClassManipulation(classes, remove, false); | ||
|
||
if (createdCache) { | ||
cache.promise = runAnimationPostDigest(function(done) { | ||
var cache = element.data(STORAGE_KEY); | ||
element.removeData(STORAGE_KEY); | ||
|
||
var classes = cache && resolveElementClasses(element, cache); | ||
|
||
if (classes) { | ||
if (classes[0]) self.$$addClassImmediately(element, classes[0]); | ||
|
||
if (classes[1]) self.$$removeClassImmediately(element, classes[1]); | ||
|
||
} | ||
|
||
done(); | ||
}); | ||
element.data(STORAGE_KEY, cache); | ||
} | ||
|
||
return cache.promise; | ||
}, | ||
|
||
enabled : noop, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -477,9 +477,14 @@ angular.module('ngAnimate', ['ng']) | |
}); | ||
}); | ||
|
||
var hasClasses = {}; | ||
forEach((element.attr('class') || '').replace(/\s+/g, ' ').split(' '), function(className) { | ||
hasClasses[className] = true; | ||
}); | ||
|
||
var toAdd = [], toRemove = []; | ||
forEach(cache.classes, function(status, className) { | ||
var hasClass = angular.$$hasClass(element[0], className); | ||
var hasClass = hasClasses[className] === true; | ||
var matchingAnimation = lookup[className] || {}; | ||
|
||
// When addClass and removeClass is called then $animate will check to | ||
|
@@ -979,7 +984,10 @@ angular.module('ngAnimate', ['ng']) | |
element = stripCommentsFromElement(element); | ||
|
||
if (classBasedAnimationsBlocked(element)) { | ||
return $delegate.setClass(element, add, remove); | ||
// TODO(@caitp/@matsko): Don't use private/undocumented API here --- we should not be | ||
// changing the DOM synchronously in this case. The `true` parameter must eventually be | ||
// removed. | ||
return $delegate.setClass(element, add, remove, true); | ||
|
||
} | ||
|
||
// we're using a combined array for both the add and remove | ||
|
@@ -1033,7 +1041,8 @@ angular.module('ngAnimate', ['ng']) | |
return !classes | ||
? done() | ||
: performAnimation('setClass', classes, element, parentElement, null, function() { | ||
$delegate.setClass(element, classes[0], classes[1]); | ||
if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]); | ||
if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]); | ||
}, done); | ||
}); | ||
}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=== true
so that classes liketoString
orconstructor
don't cause problemsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use
createMap
instead and then you don't need this