/
githubmirror
/
angular.js
Обзор
Документация
Войти
/
githubmirror
/
angular.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v1.6.1
src/ngAnimate/animateQueue.js
723 строки
28 KB
Georgios Kalpakas
chore(package): fix some warnings/errors
08 авг 2016, 18:40
08 авг 2016, 18:40
ec565dd
Код
Авторство
О чём код?
'use strict'; var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; var $$AnimateQueueProvider = ['$animateProvider', /** @this */ 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 = Object.create(null); // 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 || /** @this */ function(arg) { // eslint-disable-next-line no-bitwise return this === arg || !!(this.compareDocumentPosition(arg) & 16); }; 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); if (argCount === 1) { // (element) - Element getter bool = !disabledElementsLookup.get(node); } 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) { 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), 10); 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); } }]; }];