Files
angular.js/src/ngAnimate/animateQueue.js
T
Georgios Kalpakas d406a15e8c fix(modules): allow modules to be loaded in any order when using angular-loader
Some modules used to assume that the angular helpers would always be available when their script was
executed. This could be a problem when using `angular-loader` and the module file happened to get
loaded before the core `angular.js` file.
This commit fixes the issue by delaying the access to angular helpers, until they are guaranteed to
be available.

Affected modules:
- `ngAnimate`
- `ngMessageFormat`
- `ngMessages`
- `ngRoute`
- `ngSanitize`

Fixes #9140

Closes #14794
2016-06-17 23:56:14 +03:00

726 lines
28 KiB
JavaScript

'use strict';
var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var PRE_DIGEST_STATE = 1;
var RUNNING_STATE = 2;
var ONE_SPACE = ' ';
var rules = this.rules = {
skip: [],
cancel: [],
join: []
};
function makeTruthyCssClassMap(classString) {
if (!classString) {
return null;
}
var keys = classString.split(ONE_SPACE);
var map = Object.create(null);
forEach(keys, function(key) {
map[key] = true;
});
return map;
}
function hasMatchingClasses(newClassString, currentClassString) {
if (newClassString && currentClassString) {
var currentClassMap = makeTruthyCssClassMap(currentClassString);
return newClassString.split(ONE_SPACE).some(function(className) {
return currentClassMap[className];
});
}
}
function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
return rules[ruleType].some(function(fn) {
return fn(element, currentAnimation, previousAnimation);
});
}
function hasAnimationClasses(animation, and) {
var a = (animation.addClass || '').length > 0;
var b = (animation.removeClass || '').length > 0;
return and ? a && b : a || b;
}
rules.join.push(function(element, newAnimation, currentAnimation) {
// if the new animation is class-based then we can just tack that on
return !newAnimation.structural && hasAnimationClasses(newAnimation);
});
rules.skip.push(function(element, newAnimation, currentAnimation) {
// there is no need to animate anything if no classes are being added and
// there is no structural animation that will be triggered
return !newAnimation.structural && !hasAnimationClasses(newAnimation);
});
rules.skip.push(function(element, newAnimation, currentAnimation) {
// why should we trigger a new structural animation if the element will
// be removed from the DOM anyway?
return currentAnimation.event === 'leave' && newAnimation.structural;
});
rules.skip.push(function(element, newAnimation, currentAnimation) {
// if there is an ongoing current animation then don't even bother running the class-based animation
return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural;
});
rules.cancel.push(function(element, newAnimation, currentAnimation) {
// there can never be two structural animations running at the same time
return currentAnimation.structural && newAnimation.structural;
});
rules.cancel.push(function(element, newAnimation, currentAnimation) {
// if the previous animation is already running, but the new animation will
// be triggered, but the new animation is structural
return currentAnimation.state === RUNNING_STATE && newAnimation.structural;
});
rules.cancel.push(function(element, newAnimation, currentAnimation) {
// cancel the animation if classes added / removed in both animation cancel each other out,
// but only if the current animation isn't structural
if (currentAnimation.structural) return false;
var nA = newAnimation.addClass;
var nR = newAnimation.removeClass;
var cA = currentAnimation.addClass;
var cR = currentAnimation.removeClass;
// early detection to save the global CPU shortage :)
if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) {
return false;
}
return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA);
});
this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
'$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
'$$isDocumentHidden',
function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
$$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow,
$$isDocumentHidden) {
var activeAnimationsLookup = new $$HashMap();
var disabledElementsLookup = new $$HashMap();
var animationsEnabled = null;
function postDigestTaskFactory() {
var postDigestCalled = false;
return function(fn) {
// we only issue a call to postDigest before
// it has first passed. This prevents any callbacks
// from not firing once the animation has completed
// since it will be out of the digest cycle.
if (postDigestCalled) {
fn();
} else {
$rootScope.$$postDigest(function() {
postDigestCalled = true;
fn();
});
}
};
}
// Wait until all directive and route-related templates are downloaded and
// compiled. The $templateRequest.totalPendingRequests variable keeps track of
// all of the remote templates being currently downloaded. If there are no
// templates currently downloading then the watcher will still fire anyway.
var deregisterWatch = $rootScope.$watch(
function() { return $templateRequest.totalPendingRequests === 0; },
function(isEmpty) {
if (!isEmpty) return;
deregisterWatch();
// Now that all templates have been downloaded, $animate will wait until
// the post digest queue is empty before enabling animations. By having two
// calls to $postDigest calls we can ensure that the flag is enabled at the
// very end of the post digest queue. Since all of the animations in $animate
// use $postDigest, it's important that the code below executes at the end.
// This basically means that the page is fully downloaded and compiled before
// any animations are triggered.
$rootScope.$$postDigest(function() {
$rootScope.$$postDigest(function() {
// we check for null directly in the event that the application already called
// .enabled() with whatever arguments that it provided it with
if (animationsEnabled === null) {
animationsEnabled = true;
}
});
});
}
);
var callbackRegistry = {};
// remember that the classNameFilter is set during the provider/config
// stage therefore we can optimize here and setup a helper function
var classNameFilter = $animateProvider.classNameFilter();
var isAnimatableClassName = !classNameFilter
? function() { return true; }
: function(className) {
return classNameFilter.test(className);
};
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
function normalizeAnimationDetails(element, animation) {
return mergeAnimationDetails(element, animation, {});
}
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
var contains = window.Node.prototype.contains || function(arg) {
// jshint bitwise: false
return this === arg || !!(this.compareDocumentPosition(arg) & 16);
// jshint bitwise: true
};
function findCallbacks(parent, element, event) {
var targetNode = getDomNode(element);
var targetParentNode = getDomNode(parent);
var matches = [];
var entries = callbackRegistry[event];
if (entries) {
forEach(entries, function(entry) {
if (contains.call(entry.node, targetNode)) {
matches.push(entry.callback);
} else if (event === 'leave' && contains.call(entry.node, targetParentNode)) {
matches.push(entry.callback);
}
});
}
return matches;
}
function filterFromRegistry(list, matchContainer, matchCallback) {
var containerNode = extractElementNode(matchContainer);
return list.filter(function(entry) {
var isMatch = entry.node === containerNode &&
(!matchCallback || entry.callback === matchCallback);
return !isMatch;
});
}
function cleanupEventListeners(phase, element) {
if (phase === 'close' && !element[0].parentNode) {
// If the element is not attached to a parentNode, it has been removed by
// the domOperation, and we can safely remove the event callbacks
$animate.off(element);
}
}
var $animate = {
on: function(event, container, callback) {
var node = extractElementNode(container);
callbackRegistry[event] = callbackRegistry[event] || [];
callbackRegistry[event].push({
node: node,
callback: callback
});
// Remove the callback when the element is removed from the DOM
jqLite(container).on('$destroy', function() {
var animationDetails = activeAnimationsLookup.get(node);
if (!animationDetails) {
// If there's an animation ongoing, the callback calling code will remove
// the event listeners. If we'd remove here, the callbacks would be removed
// before the animation ends
$animate.off(event, container, callback);
}
});
},
off: function(event, container, callback) {
if (arguments.length === 1 && !isString(arguments[0])) {
container = arguments[0];
for (var eventType in callbackRegistry) {
callbackRegistry[eventType] = filterFromRegistry(callbackRegistry[eventType], container);
}
return;
}
var entries = callbackRegistry[event];
if (!entries) return;
callbackRegistry[event] = arguments.length === 1
? null
: filterFromRegistry(entries, container, callback);
},
pin: function(element, parentElement) {
assertArg(isElement(element), 'element', 'not an element');
assertArg(isElement(parentElement), 'parentElement', 'not an element');
element.data(NG_ANIMATE_PIN_DATA, parentElement);
},
push: function(element, event, options, domOperation) {
options = options || {};
options.domOperation = domOperation;
return queueAnimation(element, event, options);
},
// this method has four signatures:
// () - global getter
// (bool) - global setter
// (element) - element getter
// (element, bool) - element setter<F37>
enabled: function(element, bool) {
var argCount = arguments.length;
if (argCount === 0) {
// () - Global getter
bool = !!animationsEnabled;
} else {
var hasElement = isElement(element);
if (!hasElement) {
// (bool) - Global setter
bool = animationsEnabled = !!element;
} else {
var node = getDomNode(element);
var recordExists = disabledElementsLookup.get(node);
if (argCount === 1) {
// (element) - Element getter
bool = !recordExists;
} else {
// (element, bool) - Element setter
disabledElementsLookup.put(node, !bool);
}
}
}
return bool;
}
};
return $animate;
function queueAnimation(element, event, initialOptions) {
// we always make a copy of the options since
// there should never be any side effects on
// the input data when running `$animateCss`.
var options = copy(initialOptions);
var node, parent;
element = stripCommentsFromElement(element);
if (element) {
node = getDomNode(element);
parent = element.parent();
}
options = prepareAnimationOptions(options);
// we create a fake runner with a working promise.
// These methods will become available after the digest has passed
var runner = new $$AnimateRunner();
// this is used to trigger callbacks in postDigest mode
var runInNextPostDigestOrNow = postDigestTaskFactory();
if (isArray(options.addClass)) {
options.addClass = options.addClass.join(' ');
}
if (options.addClass && !isString(options.addClass)) {
options.addClass = null;
}
if (isArray(options.removeClass)) {
options.removeClass = options.removeClass.join(' ');
}
if (options.removeClass && !isString(options.removeClass)) {
options.removeClass = null;
}
if (options.from && !isObject(options.from)) {
options.from = null;
}
if (options.to && !isObject(options.to)) {
options.to = null;
}
// there are situations where a directive issues an animation for
// a jqLite wrapper that contains only comment nodes... If this
// happens then there is no way we can perform an animation
if (!node) {
close();
return runner;
}
var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' ');
if (!isAnimatableClassName(className)) {
close();
return runner;
}
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
var documentHidden = $$isDocumentHidden();
// this is a hard disable of all animations for the application or on
// the element itself, therefore there is no need to continue further
// past this point if not enabled
// Animations are also disabled if the document is currently hidden (page is not visible
// to the user), because browsers slow down or do not flush calls to requestAnimationFrame
var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node);
var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
var hasExistingAnimation = !!existingAnimation.state;
// there is no point in traversing the same collection of parent ancestors if a followup
// animation will be run on the same element that already did all that checking work
if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state !== PRE_DIGEST_STATE)) {
skipAnimations = !areAnimationsAllowed(element, parent, event);
}
if (skipAnimations) {
// Callbacks should fire even if the document is hidden (regression fix for issue #14120)
if (documentHidden) notifyProgress(runner, event, 'start');
close();
if (documentHidden) notifyProgress(runner, event, 'close');
return runner;
}
if (isStructural) {
closeChildAnimations(element);
}
var newAnimation = {
structural: isStructural,
element: element,
event: event,
addClass: options.addClass,
removeClass: options.removeClass,
close: close,
options: options,
runner: runner
};
if (hasExistingAnimation) {
var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation);
if (skipAnimationFlag) {
if (existingAnimation.state === RUNNING_STATE) {
close();
return runner;
} else {
mergeAnimationDetails(element, existingAnimation, newAnimation);
return existingAnimation.runner;
}
}
var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation);
if (cancelAnimationFlag) {
if (existingAnimation.state === RUNNING_STATE) {
// this will end the animation right away and it is safe
// to do so since the animation is already running and the
// runner callback code will run in async
existingAnimation.runner.end();
} else if (existingAnimation.structural) {
// this means that the animation is queued into a digest, but
// hasn't started yet. Therefore it is safe to run the close
// method which will call the runner methods in async.
existingAnimation.close();
} else {
// this will merge the new animation options into existing animation options
mergeAnimationDetails(element, existingAnimation, newAnimation);
return existingAnimation.runner;
}
} else {
// a joined animation means that this animation will take over the existing one
// so an example would involve a leave animation taking over an enter. Then when
// the postDigest kicks in the enter will be ignored.
var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation);
if (joinAnimationFlag) {
if (existingAnimation.state === RUNNING_STATE) {
normalizeAnimationDetails(element, newAnimation);
} else {
applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
event = newAnimation.event = existingAnimation.event;
options = mergeAnimationDetails(element, existingAnimation, newAnimation);
//we return the same runner since only the option values of this animation will
//be fed into the `existingAnimation`.
return existingAnimation.runner;
}
}
}
} else {
// normalization in this case means that it removes redundant CSS classes that
// already exist (addClass) or do not exist (removeClass) on the element
normalizeAnimationDetails(element, newAnimation);
}
// when the options are merged and cleaned up we may end up not having to do
// an animation at all, therefore we should check this before issuing a post
// digest callback. Structural animations will always run no matter what.
var isValidAnimation = newAnimation.structural;
if (!isValidAnimation) {
// animate (from/to) can be quickly checked first, otherwise we check if any classes are present
isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0)
|| hasAnimationClasses(newAnimation);
}
if (!isValidAnimation) {
close();
clearElementAnimationState(element);
return runner;
}
// the counter keeps track of cancelled animations
var counter = (existingAnimation.counter || 0) + 1;
newAnimation.counter = counter;
markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation);
$rootScope.$$postDigest(function() {
var animationDetails = activeAnimationsLookup.get(node);
var animationCancelled = !animationDetails;
animationDetails = animationDetails || {};
// if addClass/removeClass is called before something like enter then the
// registered parent element may not be present. The code below will ensure
// that a final value for parent element is obtained
var parentElement = element.parent() || [];
// animate/structural/class-based animations all have requirements. Otherwise there
// is no point in performing an animation. The parent node must also be set.
var isValidAnimation = parentElement.length > 0
&& (animationDetails.event === 'animate'
|| animationDetails.structural
|| hasAnimationClasses(animationDetails));
// this means that the previous animation was cancelled
// even if the follow-up animation is the same event
if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) {
// if another animation did not take over then we need
// to make sure that the domOperation and options are
// handled accordingly
if (animationCancelled) {
applyAnimationClasses(element, options);
applyAnimationStyles(element, options);
}
// if the event changed from something like enter to leave then we do
// it, otherwise if it's the same then the end result will be the same too
if (animationCancelled || (isStructural && animationDetails.event !== event)) {
options.domOperation();
runner.end();
}
// in the event that the element animation was not cancelled or a follow-up animation
// isn't allowed to animate from here then we need to clear the state of the element
// so that any future animations won't read the expired animation data.
if (!isValidAnimation) {
clearElementAnimationState(element);
}
return;
}
// this combined multiple class to addClass / removeClass into a setClass event
// so long as a structural event did not take over the animation
event = !animationDetails.structural && hasAnimationClasses(animationDetails, true)
? 'setClass'
: animationDetails.event;
markElementAnimationState(element, RUNNING_STATE);
var realRunner = $$animation(element, event, animationDetails.options);
// this will update the runner's flow-control events based on
// the `realRunner` object.
runner.setHost(realRunner);
notifyProgress(runner, event, 'start', {});
realRunner.done(function(status) {
close(!status);
var animationDetails = activeAnimationsLookup.get(node);
if (animationDetails && animationDetails.counter === counter) {
clearElementAnimationState(getDomNode(element));
}
notifyProgress(runner, event, 'close', {});
});
});
return runner;
function notifyProgress(runner, event, phase, data) {
runInNextPostDigestOrNow(function() {
var callbacks = findCallbacks(parent, element, event);
if (callbacks.length) {
// do not optimize this call here to RAF because
// we don't know how heavy the callback code here will
// be and if this code is buffered then this can
// lead to a performance regression.
$$rAF(function() {
forEach(callbacks, function(callback) {
callback(element, phase, data);
});
cleanupEventListeners(phase, element);
});
} else {
cleanupEventListeners(phase, element);
}
});
runner.progress(event, phase, data);
}
function close(reject) { // jshint ignore:line
clearGeneratedClasses(element, options);
applyAnimationClasses(element, options);
applyAnimationStyles(element, options);
options.domOperation();
runner.complete(!reject);
}
}
function closeChildAnimations(element) {
var node = getDomNode(element);
var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
forEach(children, function(child) {
var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME));
var animationDetails = activeAnimationsLookup.get(child);
if (animationDetails) {
switch (state) {
case RUNNING_STATE:
animationDetails.runner.end();
/* falls through */
case PRE_DIGEST_STATE:
activeAnimationsLookup.remove(child);
break;
}
}
});
}
function clearElementAnimationState(element) {
var node = getDomNode(element);
node.removeAttribute(NG_ANIMATE_ATTR_NAME);
activeAnimationsLookup.remove(node);
}
function isMatchingElement(nodeOrElmA, nodeOrElmB) {
return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB);
}
/**
* This fn returns false if any of the following is true:
* a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed
* b) a parent element has an ongoing structural animation, and animateChildren is false
* c) the element is not a child of the body
* d) the element is not a child of the $rootElement
*/
function areAnimationsAllowed(element, parentElement, event) {
var bodyElement = jqLite($document[0].body);
var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
var rootElementDetected = isMatchingElement(element, $rootElement);
var parentAnimationDetected = false;
var animateChildren;
var elementDisabled = disabledElementsLookup.get(getDomNode(element));
var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA);
if (parentHost) {
parentElement = parentHost;
}
parentElement = getDomNode(parentElement);
while (parentElement) {
if (!rootElementDetected) {
// angular doesn't want to attempt to animate elements outside of the application
// therefore we need to ensure that the rootElement is an ancestor of the current element
rootElementDetected = isMatchingElement(parentElement, $rootElement);
}
if (parentElement.nodeType !== ELEMENT_NODE) {
// no point in inspecting the #document element
break;
}
var details = activeAnimationsLookup.get(parentElement) || {};
// either an enter, leave or move animation will commence
// therefore we can't allow any animations to take place
// but if a parent animation is class-based then that's ok
if (!parentAnimationDetected) {
var parentElementDisabled = disabledElementsLookup.get(parentElement);
if (parentElementDisabled === true && elementDisabled !== false) {
// disable animations if the user hasn't explicitly enabled animations on the
// current element
elementDisabled = true;
// element is disabled via parent element, no need to check anything else
break;
} else if (parentElementDisabled === false) {
elementDisabled = false;
}
parentAnimationDetected = details.structural;
}
if (isUndefined(animateChildren) || animateChildren === true) {
var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA);
if (isDefined(value)) {
animateChildren = value;
}
}
// there is no need to continue traversing at this point
if (parentAnimationDetected && animateChildren === false) break;
if (!bodyElementDetected) {
// we also need to ensure that the element is or will be a part of the body element
// otherwise it is pointless to even issue an animation to be rendered
bodyElementDetected = isMatchingElement(parentElement, bodyElement);
}
if (bodyElementDetected && rootElementDetected) {
// If both body and root have been found, any other checks are pointless,
// as no animation data should live outside the application
break;
}
if (!rootElementDetected) {
// If no rootElement is detected, check if the parentElement is pinned to another element
parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA);
if (parentHost) {
// The pin target element becomes the next parent element
parentElement = getDomNode(parentHost);
continue;
}
}
parentElement = parentElement.parentNode;
}
var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true;
return allowAnimation && rootElementDetected && bodyElementDetected;
}
function markElementAnimationState(element, state, details) {
details = details || {};
details.state = state;
var node = getDomNode(element);
node.setAttribute(NG_ANIMATE_ATTR_NAME, state);
var oldValue = activeAnimationsLookup.get(node);
var newValue = oldValue
? extend(oldValue, details)
: details;
activeAnimationsLookup.put(node, newValue);
}
}];
}];