/
githubmirror
/
angular.js
Обзор
Документация
Войти
/
githubmirror
/
angular.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/ngAnimate/animation.js
449 строк
15 KB
Martin Staffa
fix(ngAnimate): remove prepare classes with multiple structural animations
06 сен 2018, 10:47
Не верифицирован
06 сен 2018, 10:47
44cc823
Код
Авторство
О чём код?
'use strict'; /* exported $$AnimationProvider */ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animateProvider) { var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; var drivers = this.drivers = []; var RUNNER_STORAGE_KEY = '$$animationRunner'; var PREPARE_CLASSES_KEY = '$$animatePrepareClasses'; function setRunner(element, runner) { element.data(RUNNER_STORAGE_KEY, runner); } function removeRunner(element) { element.removeData(RUNNER_STORAGE_KEY); } function getRunner(element) { return element.data(RUNNER_STORAGE_KEY); } this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler', '$$animateCache', function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler, $$animateCache) { var animationQueue = []; var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); function sortAnimations(animations) { var tree = { children: [] }; var i, lookup = new $$Map(); // this is done first beforehand so that the map // is filled with a list of the elements that will be animated for (i = 0; i < animations.length; i++) { var animation = animations[i]; lookup.set(animation.domNode, animations[i] = { domNode: animation.domNode, element: animation.element, fn: animation.fn, children: [] }); } for (i = 0; i < animations.length; i++) { processNode(animations[i]); } return flatten(tree); function processNode(entry) { if (entry.processed) return entry; entry.processed = true; var elementNode = entry.domNode; var parentNode = elementNode.parentNode; lookup.set(elementNode, entry); var parentEntry; while (parentNode) { parentEntry = lookup.get(parentNode); if (parentEntry) { if (!parentEntry.processed) { parentEntry = processNode(parentEntry); } break; } parentNode = parentNode.parentNode; } (parentEntry || tree).children.push(entry); return entry; } function flatten(tree) { var result = []; var queue = []; var i; for (i = 0; i < tree.children.length; i++) { queue.push(tree.children[i]); } var remainingLevelEntries = queue.length; var nextLevelEntries = 0; var row = []; for (i = 0; i < queue.length; i++) { var entry = queue[i]; if (remainingLevelEntries <= 0) { remainingLevelEntries = nextLevelEntries; nextLevelEntries = 0; result.push(row); row = []; } row.push(entry); entry.children.forEach(function(childEntry) { nextLevelEntries++; queue.push(childEntry); }); remainingLevelEntries--; } if (row.length) { result.push(row); } return result; } } // TODO(matsko): document the signature in a better way return function(element, event, options) { options = prepareAnimationOptions(options); var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; // there is no animation at the current moment, however // these runner methods will get later updated with the // methods leading into the driver's end/cancel methods // for now they just stop the animation from starting var runner = new $$AnimateRunner({ end: function() { close(); }, cancel: function() { close(true); } }); if (!drivers.length) { close(); return runner; } var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass)); var tempClasses = options.tempClasses; if (tempClasses) { classes += ' ' + tempClasses; options.tempClasses = null; } if (isStructural) { element.data(PREPARE_CLASSES_KEY, 'ng-' + event + PREPARE_CLASS_SUFFIX); } setRunner(element, runner); animationQueue.push({ // this data is used by the postDigest code and passed into // the driver step function element: element, classes: classes, event: event, structural: isStructural, options: options, beforeStart: beforeStart, close: close }); element.on('$destroy', handleDestroyedElement); // we only want there to be one function called within the post digest // block. This way we can group animations for all the animations that // were apart of the same postDigest flush call. if (animationQueue.length > 1) return runner; $rootScope.$$postDigest(function() { var animations = []; forEach(animationQueue, function(entry) { // the element was destroyed early on which removed the runner // form its storage. This means we can't animate this element // at all and it already has been closed due to destruction. if (getRunner(entry.element)) { animations.push(entry); } else { entry.close(); } }); // now any future animations will be in another postDigest animationQueue.length = 0; var groupedAnimations = groupAnimations(animations); var toBeSortedAnimations = []; forEach(groupedAnimations, function(animationEntry) { var element = animationEntry.from ? animationEntry.from.element : animationEntry.element; var extraClasses = options.addClass; extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME; var cacheKey = $$animateCache.cacheKey(element[0], animationEntry.event, extraClasses, options.removeClass); toBeSortedAnimations.push({ element: element, domNode: getDomNode(element), fn: function triggerAnimationStart() { var startAnimationFn, closeFn = animationEntry.close; // in the event that we've cached the animation status for this element // and it's in fact an invalid animation (something that has duration = 0) // then we should skip all the heavy work from here on if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) { closeFn(); return; } // it's important that we apply the `ng-animate` CSS class and the // temporary classes before we do any driver invoking since these // CSS classes may be required for proper CSS detection. animationEntry.beforeStart(); // in the event that the element was removed before the digest runs or // during the RAF sequencing then we should not trigger the animation. var targetElement = animationEntry.anchors ? (animationEntry.from.element || animationEntry.to.element) : animationEntry.element; if (getRunner(targetElement)) { var operation = invokeFirstDriver(animationEntry); if (operation) { startAnimationFn = operation.start; } } if (!startAnimationFn) { closeFn(); } else { var animationRunner = startAnimationFn(); animationRunner.done(function(status) { closeFn(!status); }); updateAnimationRunners(animationEntry, animationRunner); } } }); }); // we need to sort each of the animations in order of parent to child // relationships. This ensures that the child classes are applied at the // right time. var finalAnimations = sortAnimations(toBeSortedAnimations); for (var i = 0; i < finalAnimations.length; i++) { var innerArray = finalAnimations[i]; for (var j = 0; j < innerArray.length; j++) { var entry = innerArray[j]; var element = entry.element; // the RAFScheduler code only uses functions finalAnimations[i][j] = entry.fn; // the first row of elements shouldn't have a prepare-class added to them // since the elements are at the top of the animation hierarchy and they // will be applied without a RAF having to pass... if (i === 0) { element.removeData(PREPARE_CLASSES_KEY); continue; } var prepareClassName = element.data(PREPARE_CLASSES_KEY); if (prepareClassName) { $$jqLite.addClass(element, prepareClassName); } } } $$rAFScheduler(finalAnimations); }); return runner; // TODO(matsko): change to reference nodes function getAnchorNodes(node) { var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']'; var items = node.hasAttribute(NG_ANIMATE_REF_ATTR) ? [node] : node.querySelectorAll(SELECTOR); var anchors = []; forEach(items, function(node) { var attr = node.getAttribute(NG_ANIMATE_REF_ATTR); if (attr && attr.length) { anchors.push(node); } }); return anchors; } function groupAnimations(animations) { var preparedAnimations = []; var refLookup = {}; forEach(animations, function(animation, index) { var element = animation.element; var node = getDomNode(element); var event = animation.event; var enterOrMove = ['enter', 'move'].indexOf(event) >= 0; var anchorNodes = animation.structural ? getAnchorNodes(node) : []; if (anchorNodes.length) { var direction = enterOrMove ? 'to' : 'from'; forEach(anchorNodes, function(anchor) { var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR); refLookup[key] = refLookup[key] || {}; refLookup[key][direction] = { animationID: index, element: jqLite(anchor) }; }); } else { preparedAnimations.push(animation); } }); var usedIndicesLookup = {}; var anchorGroups = {}; forEach(refLookup, function(operations, key) { var from = operations.from; var to = operations.to; if (!from || !to) { // only one of these is set therefore we can't have an // anchor animation since all three pieces are required var index = from ? from.animationID : to.animationID; var indexKey = index.toString(); if (!usedIndicesLookup[indexKey]) { usedIndicesLookup[indexKey] = true; preparedAnimations.push(animations[index]); } return; } var fromAnimation = animations[from.animationID]; var toAnimation = animations[to.animationID]; var lookupKey = from.animationID.toString(); if (!anchorGroups[lookupKey]) { var group = anchorGroups[lookupKey] = { structural: true, beforeStart: function() { fromAnimation.beforeStart(); toAnimation.beforeStart(); }, close: function() { fromAnimation.close(); toAnimation.close(); }, classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes), from: fromAnimation, to: toAnimation, anchors: [] // TODO(matsko): change to reference nodes }; // the anchor animations require that the from and to elements both have at least // one shared CSS class which effectively marries the two elements together to use // the same animation driver and to properly sequence the anchor animation. if (group.classes.length) { preparedAnimations.push(group); } else { preparedAnimations.push(fromAnimation); preparedAnimations.push(toAnimation); } } anchorGroups[lookupKey].anchors.push({ 'out': from.element, 'in': to.element }); }); return preparedAnimations; } function cssClassesIntersection(a,b) { a = a.split(' '); b = b.split(' '); var matches = []; for (var i = 0; i < a.length; i++) { var aa = a[i]; if (aa.substring(0,3) === 'ng-') continue; for (var j = 0; j < b.length; j++) { if (aa === b[j]) { matches.push(aa); break; } } } return matches.join(' '); } function invokeFirstDriver(animationDetails) { // we loop in reverse order since the more general drivers (like CSS and JS) // may attempt more elements, but custom drivers are more particular for (var i = drivers.length - 1; i >= 0; i--) { var driverName = drivers[i]; var factory = $injector.get(driverName); var driver = factory(animationDetails); if (driver) { return driver; } } } function beforeStart() { tempClasses = (tempClasses ? (tempClasses + ' ') : '') + NG_ANIMATE_CLASSNAME; $$jqLite.addClass(element, tempClasses); var prepareClassName = element.data(PREPARE_CLASSES_KEY); if (prepareClassName) { $$jqLite.removeClass(element, prepareClassName); prepareClassName = null; } } function updateAnimationRunners(animation, newRunner) { if (animation.from && animation.to) { update(animation.from.element); update(animation.to.element); } else { update(animation.element); } function update(element) { var runner = getRunner(element); if (runner) runner.setHost(newRunner); } } function handleDestroyedElement() { var runner = getRunner(element); if (runner && (event !== 'leave' || !options.$$domOperationFired)) { runner.end(); } } function close(rejected) { element.off('$destroy', handleDestroyedElement); removeRunner(element); applyAnimationClasses(element, options); applyAnimationStyles(element, options); options.domOperation(); if (tempClasses) { $$jqLite.removeClass(element, tempClasses); } runner.complete(!rejected); } }; }]; }];