docs: function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) {
          var node = extractElementNode(element);
          //transcluded directives may sometimes fire an animation using only comment nodes
          //best to catch this early on to prevent any animation operations from occurring
          if(!node) {
            fireDOMOperation();
            closeAnimation();
            return;
          }
  
          var currentClassName = node.className;
          var classes = currentClassName + ' ' + className;
          var animationLookup = (' ' + classes).replace(/\s+/g,'.');
          if (!parentElement) {
            parentElement = afterElement ? afterElement.parent() : element.parent();
          }
  
          var matches = lookup(animationLookup);
          var isClassBased = animationEvent == 'addClass' || animationEvent == 'removeClass';
          var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
  
          //skip the animation if animations are disabled, a parent is already being animated,
          //the element is not currently attached to the document body or then completely close
          //the animation if any matching animations are not found at all.
          //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found.
          if (animationsDisabled(element, parentElement) || matches.length === 0) {
            fireDOMOperation();
            closeAnimation();
            return;
          }
  
          var animations = [];
          //only add animations if the currently running animation is not structural
          //or if there is no animation running at all
          if(!ngAnimateState.running || !(isClassBased && ngAnimateState.structural)) {
            forEach(matches, function(animation) {
              //add the animation to the queue to if it is allowed to be cancelled
              if(!animation.allowCancel || animation.allowCancel(element, animationEvent, className)) {
                var beforeFn, afterFn = animation[animationEvent];
  
                //Special case for a leave animation since there is no point in performing an
                //animation on a element node that has already been removed from the DOM
                if(animationEvent == 'leave') {
                  beforeFn = afterFn;
                  afterFn = null; //this must be falsy so that the animation is skipped for leave
                } else {
                  beforeFn = animation['before' + animationEvent.charAt(0).toUpperCase() + animationEvent.substr(1)];
                }
                animations.push({
                  before : beforeFn,
                  after : afterFn
                });
              }
            });
          }
  
          //this would mean that an animation was not allowed so let the existing
          //animation do it's thing and close this one early
          if(animations.length === 0) {
            fireDOMOperation();
            fireDoneCallbackAsync();
            return;
          }
  
          //this value will be searched for class-based CSS className lookup. Therefore,
          //we prefix and suffix the current className value with spaces to avoid substring
          //lookups of className tokens
          var futureClassName = ' ' + currentClassName + ' ';
          if(ngAnimateState.running) {
            //if an animation is currently running on the element then lets take the steps
            //to cancel that animation and fire any required callbacks
            $timeout.cancel(ngAnimateState.closeAnimationTimeout);
            cleanup(element);
            cancelAnimations(ngAnimateState.animations);
  
            //if the class is removed during the reflow then it will revert the styles temporarily
            //back to the base class CSS styling causing a jump-like effect to occur. This check
            //here ensures that the domOperation is only performed after the reflow has commenced
            if(ngAnimateState.beforeComplete) {
              (ngAnimateState.done || noop)(true);
            } else if(isClassBased && !ngAnimateState.structural) {
              //class-based animations will compare element className values after cancelling the
              //previous animation to see if the element properties already contain the final CSS
              //class and if so then the animation will be skipped. Since the domOperation will
              //be performed only after the reflow is complete then our element's className value
              //will be invalid. Therefore the same string manipulation that would occur within the
              //DOM operation will be performed below so that the class comparison is valid...
              futureClassName = ngAnimateState.event == 'removeClass' ?
                futureClassName.replace(ngAnimateState.className, '') :
                futureClassName + ngAnimateState.className + ' ';
            }
          }
  
          //There is no point in perform a class-based animation if the element already contains
          //(on addClass) or doesn't contain (on removeClass) the className being animated.
          //The reason why this is being called after the previous animations are cancelled
          //is so that the CSS classes present on the element can be properly examined.
          var classNameToken = ' ' + className + ' ';
          if((animationEvent == 'addClass'    && futureClassName.indexOf(classNameToken) >= 0) ||
             (animationEvent == 'removeClass' && futureClassName.indexOf(classNameToken) == -1)) {
            fireDOMOperation();
            fireDoneCallbackAsync();
            return;
          }
  
          //the ng-animate class does nothing, but it's here to allow for
          //parent animations to find and cancel child animations when needed
          element.addClass(NG_ANIMATE_CLASS_NAME);
  
          element.data(NG_ANIMATE_STATE, {
            running:true,
            event:animationEvent,
            className:className,
            structural:!isClassBased,
            animations:animations,
            done:onBeforeAnimationsComplete
          });
  
          //first we run the before animations and when all of those are complete
          //then we perform the DOM operation and run the next set of animations
          invokeRegisteredAnimationFns(animations, 'before', onBeforeAnimationsComplete);
  
          function onBeforeAnimationsComplete(cancelled) {
            fireDOMOperation();
            if(cancelled === true) {
              closeAnimation();
              return;
            }
  
            //set the done function to the final done function
            //so that the DOM event won't be executed twice by accident
            //if the after animation is cancelled as well
            var data = element.data(NG_ANIMATE_STATE);
            if(data) {
              data.done = closeAnimation;
              element.data(NG_ANIMATE_STATE, data);
            }
            invokeRegisteredAnimationFns(animations, 'after', closeAnimation);
          }
  
          function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) {
            var endFnName = phase + 'End';
            forEach(animations, function(animation, index) {
              var animationPhaseCompleted = function() {
                progress(index, phase);
              };
  
              //there are no before functions for enter + move since the DOM
              //operations happen before the performAnimation method fires
              if(phase == 'before' && (animationEvent == 'enter' || animationEvent == 'move')) {
                animationPhaseCompleted();
                return;
              }
  
              if(animation[phase]) {
                animation[endFnName] = isClassBased ?
                  animation[phase](element, className, animationPhaseCompleted) :
                  animation[phase](element, animationPhaseCompleted);
              } else {
                animationPhaseCompleted();
              }
            });
  
            function progress(index, phase) {
              var phaseCompletionFlag = phase + 'Complete';
              var currentAnimation = animations[index];
              currentAnimation[phaseCompletionFlag] = true;
              (currentAnimation[endFnName] || noop)();
  
              for(var i=0;i<animations.length;i++) {
                if(!animations[i][phaseCompletionFlag]) return;
              }
  
              allAnimationFnsComplete();
            }
          }
  
          function fireDoneCallbackAsync() {
            doneCallback && $timeout(doneCallback, 0, false);
          }
  
          //it is less complicated to use a flag than managing and cancelling
          //timeouts containing multiple callbacks.
          function fireDOMOperation() {
            if(!fireDOMOperation.hasBeenRun) {
              fireDOMOperation.hasBeenRun = true;
              domOperation();
            }
          }
  
          function closeAnimation() {
            if(!closeAnimation.hasBeenRun) {
              closeAnimation.hasBeenRun = true;
              var data = element.data(NG_ANIMATE_STATE);
              if(data) {
                /* only structural animations wait for reflow before removing an
                   animation, but class-based animations don't. An example of this
                   failing would be when a parent HTML tag has a ng-class attribute
                   causing ALL directives below to skip animations during the digest */
                if(isClassBased) {
                  cleanup(element);
                } else {
                  data.closeAnimationTimeout = $timeout(function() {
                    cleanup(element);
                  }, 0, false);
                  element.data(NG_ANIMATE_STATE, data);
                }
              }
              fireDoneCallbackAsync();
            }
          }
        }