diff options
| author | Matias Niemelä | 2013-09-03 13:29:37 -0400 | 
|---|---|---|
| committer | Matias Niemelä | 2013-09-05 17:20:28 -0400 | 
| commit | 32ad2926116e5f52ed04d788220bd389f8d0a4ac (patch) | |
| tree | 395d7209d71c760c3cb493454a47f5770da2b153 | |
| parent | e12e584ca8db306dce8e0f4d3a212d77fbae966c (diff) | |
| download | angular.js-32ad2926116e5f52ed04d788220bd389f8d0a4ac.tar.bz2 | |
refactor($animate): use CSS3 transition/animation events instead of $timeouts to track ongoing animations
Closes #3629
Closes #3874
| -rw-r--r-- | src/ngAnimate/animate.js | 31 | ||||
| -rw-r--r-- | test/ngAnimate/animateSpec.js | 117 | 
2 files changed, 80 insertions, 68 deletions
| diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index ca136a50..d03ff643 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -549,13 +549,16 @@ angular.module('ngAnimate', ['ng'])        //one day all browsers will have these properties        var w3cAnimationProp = 'animation';        var w3cTransitionProp = 'transition'; +      var w3cAnimationEvent = 'animationend'; +      var w3cTransitionEvent = 'transitionend';        //but some still use vendor-prefixed styles        var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation';        var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; +      var vendorAnimationEvent = $sniffer.vendorPrefix.toLowerCase() + 'AnimationEnd'; +      var vendorTransitionEvent = $sniffer.vendorPrefix.toLowerCase() + 'TransitionEnd';        var durationKey = 'Duration', -          delayKey = 'Delay',            propertyKey = 'Property',            animationIterationCountKey = 'IterationCount',            ELEMENT_NODE = 1; @@ -584,17 +587,12 @@ angular.module('ngAnimate', ['ng'])          element.addClass(className);          //we want all the styles defined before and after -        var duration = 0; +        var transitionTime = 0, +            animationTime = 0;          forEach(element, function(element) {            if (element.nodeType == ELEMENT_NODE) {              var elementStyles = $window.getComputedStyle(element) || {}; -            var transitionDelay     = Math.max(parseMaxTime(elementStyles[w3cTransitionProp     + delayKey]), -                                               parseMaxTime(elementStyles[vendorTransitionProp  + delayKey])); - -            var animationDelay      = Math.max(parseMaxTime(elementStyles[w3cAnimationProp      + delayKey]), -                                               parseMaxTime(elementStyles[vendorAnimationProp   + delayKey])); -              var transitionDuration  = Math.max(parseMaxTime(elementStyles[w3cTransitionProp     + durationKey]),                                                 parseMaxTime(elementStyles[vendorTransitionProp  + durationKey])); @@ -607,16 +605,16 @@ angular.module('ngAnimate', ['ng'])                                             1);              } -            duration = Math.max(animationDelay  + animationDuration, -                                transitionDelay + transitionDuration, -                                duration); +            transitionTime = Math.max(transitionDuration, transitionTime); +            animationTime = Math.max(animationDuration, animationTime);            }          });          /* there is no point in performing a reflow if the animation             timeout is empty (this would cause a flicker bug normally             in the page */ -        if(duration > 0) { +        var totalTime = Math.max(transitionTime,animationTime); +        if(totalTime > 0) {            var node = element[0];            //temporarily disable the transition so that the enter styles @@ -635,12 +633,15 @@ angular.module('ngAnimate', ['ng'])            node.style[vendorTransitionProp + propertyKey] = '';            element.addClass(activeClassName); -          $timeout(done, duration * 1000, false); +          var css3AnimationEvents = [w3cAnimationEvent,  vendorAnimationEvent, +                                     w3cTransitionEvent, vendorTransitionEvent].join(' '); +          element.on(css3AnimationEvents, onAnimationProgress);            //this will automatically be called by $animate so            //there is no need to attach this internally to the            //timeout done method            return function onEnd(cancelled) { +            element.off(css3AnimationEvents, onAnimationProgress);              element.removeClass(className);              element.removeClass(activeClassName); @@ -657,6 +658,10 @@ angular.module('ngAnimate', ['ng'])            done();          } +        function onAnimationProgress(event) { +          event.originalEvent.elapsedTime >= totalTime && done(); +        } +          function parseMaxTime(str) {            var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : [];            forEach(values, function(value) { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 661cc91e..8b88776f 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -138,7 +138,7 @@ describe("ngAnimate", function() {          if($sniffer.transitions) {            expect(child.hasClass('ng-enter')).toBe(true);            expect(child.hasClass('ng-enter-active')).toBe(true); -          $timeout.flush(); +          child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          }          expect(element.contents().length).toBe(1); @@ -154,7 +154,7 @@ describe("ngAnimate", function() {          if($sniffer.transitions) {            expect(child.hasClass('ng-leave')).toBe(true);            expect(child.hasClass('ng-leave-active')).toBe(true); -          $timeout.flush(); +          child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          }          expect(element.contents().length).toBe(0); @@ -186,7 +186,7 @@ describe("ngAnimate", function() {          if($sniffer.transitions) {            expect(child.hasClass('ng-hide-remove')).toBe(true);            expect(child.hasClass('ng-hide-remove-active')).toBe(true); -          $timeout.flush(); +          child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          }          expect(child.hasClass('ng-hide-remove')).toBe(false);          expect(child.hasClass('ng-hide-remove-active')).toBe(false); @@ -202,7 +202,7 @@ describe("ngAnimate", function() {          if($sniffer.transitions) {            expect(child.hasClass('ng-hide-add')).toBe(true);            expect(child.hasClass('ng-hide-add-active')).toBe(true); -          $timeout.flush(); +          child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          }          expect(child).toBeHidden();        })); @@ -221,7 +221,7 @@ describe("ngAnimate", function() {          expect(child.attr('class')).toContain('ng-enter');          expect(child.attr('class')).toContain('ng-enter-active'); -        $timeout.flushNext(1000); +        child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          //move          element.append(after); @@ -230,26 +230,26 @@ describe("ngAnimate", function() {          expect(child.attr('class')).toContain('ng-move');          expect(child.attr('class')).toContain('ng-move-active'); -        $timeout.flushNext(1000); +        child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          //hide          $animate.addClass(child, 'ng-hide');          expect(child.attr('class')).toContain('ng-hide-add');          expect(child.attr('class')).toContain('ng-hide-add-active'); -        $timeout.flushNext(1000); +        child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          //show          $animate.removeClass(child, 'ng-hide');          expect(child.attr('class')).toContain('ng-hide-remove');          expect(child.attr('class')).toContain('ng-hide-remove-active'); -        $timeout.flushNext(1000); +        child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          //leave          $animate.leave(child);          $rootScope.$digest();          expect(child.attr('class')).toContain('ng-leave');          expect(child.attr('class')).toContain('ng-leave-active'); -        $timeout.flushNext(1000); +        child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });        }));        it("should not run if animations are disabled", @@ -292,11 +292,10 @@ describe("ngAnimate", function() {          if($sniffer.transitions) {            expect(element.children().length).toBe(1); //still animating -          $timeout.flushNext(1000); -          $timeout.flushNext(1000); +          child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          } -        $timeout.flushNext(2000); -        $timeout.flushNext(2000); +        $timeout.flush(2000); +        $timeout.flush(2000);          expect(child).toBeShown();          expect(element.children().length).toBe(0); @@ -310,9 +309,9 @@ describe("ngAnimate", function() {          child.addClass('custom-delay ng-hide');          $animate.removeClass(child, 'ng-hide');          if($sniffer.transitions) { -          $timeout.flushNext(1000); +          child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          } -        $timeout.flushNext(2000); +        $timeout.flush(2000);          $animate.addClass(child, 'ng-hide'); @@ -352,8 +351,8 @@ describe("ngAnimate", function() {          inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {            $animate.addClass(element, 'custom-delay custom-long-delay'); -          $timeout.flushNext(2000); -          $timeout.flushNext(20000); +          $timeout.flush(2000); +          $timeout.flush(20000);            expect(element.hasClass('custom-delay')).toBe(true);            expect(element.hasClass('custom-long-delay')).toBe(true);        })); @@ -374,10 +373,10 @@ describe("ngAnimate", function() {          $animate.removeClass(element, 'ng-hide');          if($sniffer.transitions) { -          $timeout.flushNext(1000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });          } -        $timeout.flushNext(2000); -        $timeout.flushNext(20000); +        $timeout.flush(2000); +        $timeout.flush(20000);          expect(element.hasClass('custom-delay')).toBe(true);          expect(element.hasClass('custom-delay-add')).toBe(false); @@ -417,7 +416,7 @@ describe("ngAnimate", function() {            $animate.removeClass(element, 'ng-hide');            if ($sniffer.animations) { -            $timeout.flushNext(4000); +            element.triggerHandler('animationend', { originalEvent: { elapsedTime: 4 } });            }            expect(element).toBeShown();          })); @@ -440,7 +439,7 @@ describe("ngAnimate", function() {            $animate.removeClass(element, 'ng-hide');            if ($sniffer.animations) { -            $timeout.flushNext(6000); +            element.triggerHandler('animationend', { originalEvent: { elapsedTime: 6 } });            }            expect(element).toBeShown();          })); @@ -463,12 +462,12 @@ describe("ngAnimate", function() {            $animate.removeClass(element, 'ng-hide');            if ($sniffer.animations) { -            $timeout.flushNext(2000); +            element.triggerHandler('animationend', { originalEvent: { elapsedTime: 2 } });            }            expect(element).toBeShown();          })); -        it("should consider the animation delay is provided", +        it("should not consider the animation delay is provided",            inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {            var style = 'animation-duration: 2s;' + @@ -488,7 +487,7 @@ describe("ngAnimate", function() {            $animate.removeClass(element, 'ng-hide');            if ($sniffer.transitions) { -            $timeout.flushNext(20000); +            element.triggerHandler('animationend', { originalEvent: { elapsedTime: 10 } });            }            expect(element).toBeShown();          })); @@ -534,7 +533,7 @@ describe("ngAnimate", function() {              if($sniffer.animations) { //cleanup some pending animations                expect(element.hasClass('ng-hide-add')).toBe(true);                expect(element.hasClass('ng-hide-add-active')).toBe(true); -              $timeout.flushNext(2000); +              element.triggerHandler('animationend', { originalEvent: { elapsedTime: 2 } });              }              expect(element.hasClass('ng-hide-remove-active')).toBe(false); @@ -567,7 +566,7 @@ describe("ngAnimate", function() {            $animate.removeClass(element, 'ng-hide');            if ($sniffer.transitions) { -            $timeout.flushNext(1000); +            element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });            }            expect(element).toBeShown();          })); @@ -588,7 +587,9 @@ describe("ngAnimate", function() {              $animate.removeClass(element, 'ng-hide');              if ($sniffer.transitions) {                expect(element).toBeHidden(); -              $timeout.flushNext(2000); +              element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } }); +              element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } }); +              element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 2 } });              }              expect(element).toBeShown();            })); @@ -610,7 +611,7 @@ describe("ngAnimate", function() {              element.addClass('ng-hide');              $animate.removeClass(element, 'ng-hide'); -            $timeout.flushNext(0); +            $timeout.flush(0);              expect(element).toBeShown();              $animate.enabled(true); @@ -619,12 +620,14 @@ describe("ngAnimate", function() {              $animate.removeClass(element, 'ng-hide');              if ($sniffer.transitions) { -              $timeout.flushNext(3000); +              element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 0 } }); +              element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } }); +              element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });              }              expect(element).toBeShown();          })); -        it("should select the highest duration and delay", +        it("should animate for the highest duration",            inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {              var style = 'transition:1s linear all 2s;' +                           vendorPrefix + 'transition:1s linear all 2s;' + @@ -641,7 +644,7 @@ describe("ngAnimate", function() {              $animate.removeClass(element, 'ng-hide');              if ($sniffer.transitions) { -              $timeout.flushNext(11000); +              element.triggerHandler('animationend', { originalEvent: { elapsedTime: 10 } });              }              expect(element).toBeShown();          })); @@ -661,7 +664,7 @@ describe("ngAnimate", function() {              if($sniffer.transitions) {                expect(element.hasClass('ng-hide-remove')).toBe(true);                expect(element.hasClass('ng-hide-remove-active')).toBe(true); -              $timeout.flushNext(1000); +              element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });              }              expect(element.hasClass('ng-hide-remove')).toBe(false);              expect(element.hasClass('ng-hide-remove-active')).toBe(false); @@ -678,37 +681,37 @@ describe("ngAnimate", function() {      describe('animation evaluation', function () {        it('should re-evaluate the CSS classes for an animation each time', -        inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { +        inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout, $compile) { -        ss.addRule('.ng-enter', 'transition:22s linear all;' + -                 vendorPrefix + 'transition:22s linear all'); -        ss.addRule('.ng-enter', 'transition:22s linear all;' + +        ss.addRule('.abc', 'transition:22s linear all;' +                   vendorPrefix + 'transition:22s linear all'); +        ss.addRule('.xyz', 'transition:11s linear all;' + +                 vendorPrefix + 'transition:11s linear all'); -        var parent = jqLite('<div><span></span></div>'); +        var parent = $compile('<div><span ng-class="klass"></span></div>')($rootScope);          var element = parent.find('span');          $rootElement.append(parent);          angular.element(document.body).append($rootElement); -        element[0].className = 'abc'; +        $rootScope.klass = 'abc';          $animate.enter(element, parent);          $rootScope.$digest();          if ($sniffer.transitions) {            expect(element.hasClass('abc ng-enter')).toBe(true);            expect(element.hasClass('abc ng-enter ng-enter-active')).toBe(true); -          $timeout.flushNext(22000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 22 } });          }          expect(element.hasClass('abc')).toBe(true); -        element[0].className = 'xyz'; +        $rootScope.klass = 'xyz';          $animate.enter(element, parent);          $rootScope.$digest();          if ($sniffer.transitions) {            expect(element.hasClass('xyz')).toBe(true);            expect(element.hasClass('xyz ng-enter ng-enter-active')).toBe(true); -          $timeout.flushNext(22000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 11 } });          }          expect(element.hasClass('xyz')).toBe(true);        })); @@ -735,7 +738,7 @@ describe("ngAnimate", function() {            expect(element.hasClass('one two ng-enter ng-enter-active')).toBe(true);            expect(element.hasClass('one-active')).toBe(false);            expect(element.hasClass('two-active')).toBe(false); -          $timeout.flushNext(9000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 3 } });          }          expect(element.hasClass('one two')).toBe(true); @@ -883,6 +886,9 @@ describe("ngAnimate", function() {            flag = true;          }); +        if($sniffer.transitions) { +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } }); +        }          $timeout.flush();          expect(flag).toBe(true);        })); @@ -1019,7 +1025,7 @@ describe("ngAnimate", function() {            expect(element.hasClass('klass')).toBe(false);            expect(element.hasClass('klass-add')).toBe(true);            expect(element.hasClass('klass-add-active')).toBe(true); -          $timeout.flushNext(3000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 3 } });          }          //this cancels out the older animation @@ -1033,6 +1039,7 @@ describe("ngAnimate", function() {            expect(element.hasClass('klass-add-active')).toBe(false);            expect(element.hasClass('klass-remove')).toBe(true); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 3 } });          }          $timeout.flush(); @@ -1090,7 +1097,7 @@ describe("ngAnimate", function() {          if($sniffer.transitions) {            expect(element.hasClass('klass-add')).toBe(true);            expect(element.hasClass('klass-add-active')).toBe(true); -          $timeout.flushNext(11000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 11 } });            expect(element.hasClass('klass-add')).toBe(false);            expect(element.hasClass('klass-add-active')).toBe(false);          } @@ -1104,7 +1111,7 @@ describe("ngAnimate", function() {          if($sniffer.transitions) {            expect(element.hasClass('klass-remove')).toBe(true);            expect(element.hasClass('klass-remove-active')).toBe(true); -          $timeout.flushNext(11000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 11 } });            expect(element.hasClass('klass-remove')).toBe(false);            expect(element.hasClass('klass-remove-active')).toBe(false);          } @@ -1139,7 +1146,7 @@ describe("ngAnimate", function() {            expect(element.hasClass('one-add-active')).toBe(true);            expect(element.hasClass('two-add-active')).toBe(true); -          $timeout.flushNext(7000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 7 } });            expect(element.hasClass('one-add')).toBe(false);            expect(element.hasClass('one-add-active')).toBe(false); @@ -1183,7 +1190,7 @@ describe("ngAnimate", function() {            expect(element.hasClass('one-remove-active')).toBe(true);            expect(element.hasClass('two-remove-active')).toBe(true); -          $timeout.flushNext(9000); +          element.triggerHandler('transitionend', { originalEvent: { elapsedTime: 9 } });            expect(element.hasClass('one-remove')).toBe(false);            expect(element.hasClass('one-remove-active')).toBe(false); @@ -1233,7 +1240,7 @@ describe("ngAnimate", function() {      if($sniffer.transitions) {        expect(child.hasClass('ng-enter')).toBe(true);        expect(child.hasClass('ng-enter-active')).toBe(true); -      $timeout.flushNext(1000); +      child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });      }      expect(child.hasClass('ng-enter')).toBe(false); @@ -1255,7 +1262,7 @@ describe("ngAnimate", function() {      if($sniffer.transitions) {        expect(child.hasClass('ng-enter')).toBe(true);        expect(child.hasClass('ng-enter-active')).toBe(true); -      $timeout.flushNext(9000); +      child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 8 } });      }      expect(child.hasClass('ng-enter')).toBe(false);      expect(child.hasClass('ng-enter-active')).toBe(false); @@ -1304,10 +1311,10 @@ describe("ngAnimate", function() {        $animate.enter(child, element);        $rootScope.$digest(); -      $timeout.flushNext(10); +      $timeout.flush(10);        if($sniffer.transitions) { -        $timeout.flushNext(1000); +        child.triggerHandler('transitionend', { originalEvent: { elapsedTime: 1 } });        }        expect(child.hasClass('i-was-animated')).toBe(true); @@ -1354,7 +1361,7 @@ describe("ngAnimate", function() {        expect(child.hasClass('usurper')).toBe(true);        expect(child.hasClass('this-is-mine-now')).toBe(true); -      $timeout.flushNext(55); +      $timeout.flush(55);      });    }); @@ -1435,7 +1442,7 @@ describe("ngAnimate", function() {  //        expect(element).toBeShown(); //still showing  //        $timeout.flush();  //        expect(element).toBeShown(); -//        $timeout.flushNext(5555); +//        $timeout.flush(5555);  //      }  //      $timeout.flush();  //      expect(element).toBeHidden(); @@ -1448,7 +1455,7 @@ describe("ngAnimate", function() {  //        expect(element).toBeHidden();  //        $timeout.flush();  //        expect(element).toBeHidden(); -//        $timeout.flushNext(5580); +//        $timeout.flush(5580);  //      }  //      $timeout.flush();  //      expect(element).toBeShown(); | 
