diff options
| author | Matias Niemelä | 2013-06-18 13:59:57 -0400 |
|---|---|---|
| committer | Misko Hevery | 2013-07-26 23:49:54 -0700 |
| commit | 81923f1e41560327f7de6e8fddfda0d2612658f3 (patch) | |
| tree | bbf8151bddf4d026f8f5fa3196b84a45ecd9c858 | |
| parent | 11521a4cde689c2bd6aaa227b1f45cb3fb53725b (diff) | |
| download | angular.js-81923f1e41560327f7de6e8fddfda0d2612658f3.tar.bz2 | |
feat(ngAnimate): complete rewrite of animations
- ngAnimate directive is gone and was replaced with class based animations/transitions
- support for triggering animations on css class additions and removals
- done callback was added to all animation apis
- $animation and $animator where merged into a single $animate service with api:
- $animate.enter(element, parent, after, done);
- $animate.leave(element, done);
- $animate.move(element, parent, after, done);
- $animate.addClass(element, className, done);
- $animate.removeClass(element, className, done);
BREAKING CHANGE: too many things changed, we'll write up a separate doc with migration instructions
40 files changed, 2961 insertions, 2191 deletions
diff --git a/Gruntfile.js b/Gruntfile.js index 264fe874..6d0395fb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -160,6 +160,10 @@ module.exports = function(grunt) { dest: 'build/angular-resource.js', src: util.wrap(['src/ngResource/resource.js'], 'module') }, + animate: { + dest: 'build/angular-animate.js', + src: util.wrap(['src/ngAnimate/animate.js'], 'module') + }, route: { dest: 'build/angular-route.js', src: util.wrap([ @@ -178,6 +182,7 @@ module.exports = function(grunt) { min: { angular: 'build/angular.js', + animate: 'build/angular-animate.js', cookies: 'build/angular-cookies.js', loader: 'build/angular-loader.js', mobile: 'build/angular-mobile.js', diff --git a/angularFiles.js b/angularFiles.js index dc98bcc1..c06f8adc 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -10,8 +10,7 @@ angularFiles = { 'src/auto/injector.js', 'src/ng/anchorScroll.js', - 'src/ng/animation.js', - 'src/ng/animator.js', + 'src/ng/animate.js', 'src/ng/browser.js', 'src/ng/cacheFactory.js', 'src/ng/compile.js', @@ -66,6 +65,7 @@ angularFiles = { ], 'angularSrcModules': [ + 'src/ngAnimate/animate.js', 'src/ngCookies/cookies.js', 'src/ngResource/resource.js', 'src/ngRoute/routeUtils.js', @@ -107,6 +107,7 @@ angularFiles = { 'test/*.js', 'test/auto/*.js', 'test/ng/**/*.js', + 'test/ngAnimate/*.js', 'test/ngCookies/*.js', 'test/ngResource/*.js', 'test/ngRoute/**/*.js', diff --git a/css/angular.css b/css/angular.css index 0cd9d7dc..6833234f 100644 --- a/css/angular.css +++ b/css/angular.css @@ -8,3 +8,7 @@ ng\:form { display: block; } + +.ng-hide { + display: none; +} diff --git a/docs/component-spec/annotationsSpec.js b/docs/component-spec/annotationsSpec.js index 321118ad..a17c906c 100644 --- a/docs/component-spec/annotationsSpec.js +++ b/docs/component-spec/annotationsSpec.js @@ -67,41 +67,33 @@ describe('Docs Annotations', function() { var $scope, parent, element, url, window; beforeEach(function() { - module(function($provide, $animationProvider) { + module(function($provide, $animateProvider) { $provide.value('$window', window = angular.mock.createMockWindow()); - $animationProvider.register('foldout-enter', function($window) { + $animateProvider.register('.foldout', function($window) { return { - start : function(element, done) { + enter : function(element, done) { $window.setTimeout(done, 1000); - } - } - }); - $animationProvider.register('foldout-hide', function($window) { - return { - start : function(element, done) { + }, + show : function(element, done) { $window.setTimeout(done, 500); - } - } - }); - $animationProvider.register('foldout-show', function($window) { - return { - start : function(element, done) { + }, + hide : function(element, done) { $window.setTimeout(done, 200); } } }); }); - inject(function($rootScope, $compile, $templateCache, $rootElement, $animator) { - $animator.enabled(true); + inject(function($rootScope, $compile, $templateCache, $rootElement, $animate) { + $animate.enabled(true); url = '/page.html'; $scope = $rootScope.$new(); parent = angular.element('<div class="parent"></div>'); - element = angular.element('<div data-url="' + url + '" foldout></div>'); //we're injecting the element to the $rootElement since the changes in - //$animator only detect and perform animations if the root element has + //$animate only detect and perform animations if the root element has //animations enabled. If the element is not apart of the DOM //then animations are skipped. + element = angular.element('<div data-url="' + url + '" class="foldout" foldout></div>'); parent.append(element); $rootElement.append(parent); body.append($rootElement); @@ -142,16 +134,19 @@ describe('Docs Annotations', function() { $httpBackend.flush(); window.setTimeout.expect(1).process(); window.setTimeout.expect(1000).process(); + window.setTimeout.expect(0).process(); //hide element.triggerHandler('click'); window.setTimeout.expect(1).process(); - window.setTimeout.expect(500).process(); + window.setTimeout.expect(200).process(); + window.setTimeout.expect(0).process(); //show element.triggerHandler('click'); window.setTimeout.expect(1).process(); - window.setTimeout.expect(200).process(); + window.setTimeout.expect(500).process(); + window.setTimeout.expect(0).process(); })); }); @@ -160,7 +155,7 @@ describe('Docs Annotations', function() { var window, $scope, ctrl; beforeEach(function() { - module(function($provide, $animationProvider) { + module(function($provide, $animateProvider) { $provide.value('$window', window = angular.mock.createMockWindow()); }); inject(function($rootScope, $controller, $location, $cookies, sections) { diff --git a/docs/components/angular-bootstrap/bootstrap-prettify.js b/docs/components/angular-bootstrap/bootstrap-prettify.js index ad5340e5..fa40c6e7 100644 --- a/docs/components/angular-bootstrap/bootstrap-prettify.js +++ b/docs/components/angular-bootstrap/bootstrap-prettify.js @@ -183,8 +183,8 @@ directive.ngEvalJavascript = ['getEmbeddedTemplate', function(getEmbeddedTemplat }]; -directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', '$sniffer', - function($templateCache, $browser, docsRootScope, $location, $sniffer) { +directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', '$sniffer', '$animate', + function($templateCache, $browser, docsRootScope, $location, $sniffer, $animate) { return { terminal: true, link: function(scope, element, attrs) { @@ -193,6 +193,7 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', deregisterEmbedRootScope; modules.push(['$provide', function($provide) { + $provide.value('$animate', $animate); $provide.value('$templateCache', $templateCache); $provide.value('$anchorScroll', angular.noop); $provide.value('$browser', $browser); diff --git a/docs/components/angular-bootstrap/bootstrap.js b/docs/components/angular-bootstrap/bootstrap.js index 170e8805..20a5741d 100644 --- a/docs/components/angular-bootstrap/bootstrap.js +++ b/docs/components/angular-bootstrap/bootstrap.js @@ -335,12 +335,11 @@ directive.tabPane = function() { }; }; -directive.foldout = ['$http', '$animator','$window', function($http, $animator, $window) { +directive.foldout = ['$http', '$animate','$window', function($http, $animate, $window) { return { restrict: 'A', priority : 500, link: function(scope, element, attrs) { - var animator = $animator(scope, { ngAnimate: "'foldout'" }); var container, loading, url = attrs.url; if(/\/build\//.test($window.location.href)) { url = '/build/docs' + url; @@ -353,7 +352,7 @@ directive.foldout = ['$http', '$animator','$window', function($http, $animator, loading = true; var par = element.parent(); container = angular.element('<div class="foldout">loading...</div>'); - animator.enter(container, null, par); + $animate.enter(container, null, par); $http.get(url, { cache : true }).success(function(html) { loading = false; @@ -367,12 +366,12 @@ directive.foldout = ['$http', '$animator','$window', function($http, $animator, //avoid showing the element if the user has already closed it if(container.css('display') == 'block') { container.css('display','none'); - animator.show(container); + $animate.show(container); } }); } else { - container.css('display') == 'none' ? animator.show(container) : animator.hide(container); + container.hasClass('ng-hide') ? $animate.show(container) : $animate.hide(container); } }); }); diff --git a/docs/src/example.js b/docs/src/example.js index cdbc24a7..9471b3fa 100644 --- a/docs/src/example.js +++ b/docs/src/example.js @@ -134,7 +134,7 @@ exports.Example.prototype.toHtmlTabs = function() { exports.Example.prototype.toHtmlEmbed = function() { var out = []; - out.push('<div class="well doc-example-live animator-container"'); + out.push('<div class="well doc-example-live animate-container"'); if(this.animations) { out.push(" ng-class=\"{'animations-off':animationsOff == true}\""); } diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index 394510c5..eb26bbf2 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -494,6 +494,19 @@ Doc.prototype = { html_usage_parameters: function(dom) { var self = this; var params = this.param ? this.param : []; + if(this.animations) { + dom.h('Animations', this.animations, function(animations){ + dom.html('<ul>'); + var animations = animations.split("\n"); + animations.forEach(function(ani) { + dom.html('<li>'); + dom.text(ani); + dom.html('</li>'); + }); + dom.html('</ul>'); + }); + dom.html('<a href="api/ngAnimate.$animate">Click here</a> to learn more about the steps involved in the animation.'); + } if(params.length > 0) { dom.html('<h2 id="parameters">Parameters</h2>'); dom.html('<table class="variables-matrix table table-bordered table-striped">'); @@ -538,18 +551,6 @@ Doc.prototype = { dom.html('</tbody>'); dom.html('</table>'); } - if(this.animations) { - dom.h('Animations', this.animations, function(animations){ - dom.html('<ul>'); - var animations = animations.split("\n"); - animations.forEach(function(ani) { - dom.html('<li>'); - dom.text(ani); - dom.html('</li>'); - }); - dom.html('</ul>'); - }); - } }, html_usage_returns: function(dom) { @@ -665,48 +666,6 @@ Doc.prototype = { dom.text('</' + element + '>'); }); } - if(self.animations) { - var animations = [], matches = self.animations.split("\n"); - matches.forEach(function(ani) { - var name = ani.match(/^\s*(.+?)\s*-/)[1]; - animations.push(name); - }); - - dom.html('with <span id="animations">animations</span>'); - var comment; - if(animations.length == 1) { - comment = 'The ' + animations[0] + ' animation is supported'; - } - else { - var rhs = animations[animations.length-1]; - var lhs = ''; - for(var i=0;i<animations.length-1;i++) { - if(i>0) { - lhs += ', '; - } - lhs += animations[i]; - } - comment = 'The ' + lhs + ' and ' + rhs + ' animations are supported'; - } - var element = self.element || 'ANY'; - dom.code(function() { - dom.text('//' + comment + "\n"); - dom.text('<' + element + ' '); - dom.text(dashCase(self.shortName)); - renderParams('\n ', '="', '"', true); - dom.text(' ng-animate="{'); - animations.forEach(function(ani, index) { - if (index) { - dom.text(', '); - } - dom.text(ani + ': \'' + ani + '-animation\''); - }); - dom.text('}">\n ...\n'); - dom.text('</' + element + '>'); - }); - - dom.html('<a href="api/ng.$animator#Methods">Click here</a> to learn more about the steps involved in the animation.'); - } } self.html_usage_directiveInfo(dom); self.html_usage_parameters(dom); diff --git a/docs/src/templates/css/animations.css b/docs/src/templates/css/animations.css index 2d54bbfb..7324a8a1 100644 --- a/docs/src/templates/css/animations.css +++ b/docs/src/templates/css/animations.css @@ -1,4 +1,4 @@ -.reveal { +.reveal.ng-enter { -webkit-transition:1s linear all; -moz-transition:1s linear all; -o-transition:1s linear all; @@ -6,7 +6,7 @@ opacity:0; } -.reveal.reveal-active { +.reveal.ng-enter.ng-enter-active { opacity:1; } @@ -15,48 +15,45 @@ overflow:hidden; } -.slide-reveal { +.slide-reveal > .ng-enter { -webkit-transition:0.5s linear all; -moz-transition:0.5s linear all; -o-transition:0.5s linear all; transition:0.5s linear all; - opacity:0.5; + opacity:0.5; position:relative; opacity:0; top:10px; } -.slide-reveal.slide-reveal-active { +.slide-reveal > .ng-enter.ng-enter-active { top:0; opacity:1; } -.expand-enter { +.expand.ng-enter, +.expand.ng-leave { -webkit-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; -moz-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; -o-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; - +} +.expand.ng-enter { opacity:0; line-height:0; height:0!important; } -.expand-enter.expand-enter-active { +.expand.ng-enter.expand.ng-enter-active { opacity:1; line-height:20px; height:20px!important; } -.expand-leave { - -webkit-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; - -moz-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; - -o-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; - transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; - +.expand.ng-leave { opacity:1; height:20px; } -.expand-leave.expand-leave-active { +.expand.ng-leave.expand.ng-leave-active { opacity:0; height:0; } @@ -73,32 +70,36 @@ padding:1em; } -.animator-container.animations-off * { +.animate-container.animations-off * { -webkit-transition: none; -moz-transition: none; -o-transition: color 0 ease-in; /* opera is special :) */ transition: none; } -.foldout-show, .foldout-enter, .foldout-hide { +.foldout.ng-enter, +.foldout.ng-hide-add, +.foldout.ng-hide-remove { -webkit-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; -moz-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; -o-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all; } -.foldout-show, .foldout-enter { +.foldout.ng-hide-remove, +.foldout.ng-enter { opacity:0; } -.foldout-show.foldout-show-active, .foldout-hide.foldout-hide-active { +.foldout.ng-hide-remove.ng-hide-remove-active, +.foldout.ng-enter.ng-enter-active { opacity:1; } -.foldout-hide { +.foldout.ng-hide-add { opacity:1; } -.foldout-hide.foldout-hide-active { +.foldout.ng-hide-add.ng-hide-active { opacity:0; } diff --git a/docs/src/templates/index.html b/docs/src/templates/index.html index 82a5c87e..3f3e83c8 100644 --- a/docs/src/templates/index.html +++ b/docs/src/templates/index.html @@ -43,6 +43,7 @@ addTag('script', {src: path('angular-cookies.js') }, sync); addTag('script', {src: path('angular-sanitize.js') }, sync); addTag('script', {src: path('angular-mobile.js') }, sync); + addTag('script', {src: path('angular-animate.js') }, sync); addTag('script', {src: 'components/angular-bootstrap.js' }, sync); addTag('script', {src: 'components/angular-bootstrap-prettify.js' }, sync); addTag('script', {src: 'components/google-code-prettify.js' }, sync); @@ -201,7 +202,7 @@ </header> <div id="docs-fold-overlay" ng-show="docs_fold" ng-click="fold(null)"></div> - <div id="docs-fold" ng-show="docs_fold" ng-animate="'fold'"> + <div class="foldout" id="docs-fold" ng-show="docs_fold"> <div id="docs-fold-close" ng-click="fold(null)"> <span class="icon-remove-sign"></span> </div> @@ -283,21 +284,21 @@ <li class="nav-header section" ng-show="module.directives"> <a href="{{URL.directive}}" class="guide">directive</a> </li> - <li ng-repeat="page in module.directives track by page.url" ng-class="navClass(page)" ng-animate="'expand'" class="api-list-item"> + <li ng-repeat="page in module.directives track by page.url" ng-class="navClass(page)" class="expand api-list-item"> <a href="{{page.url}}" tabindex="2">{{page.shortName}}</a> </li> <li class="nav-header section" ng-show="module.filters"> <a href="{{URL.filter}}" class="guide">filter</a> </li> - <li ng-repeat="page in module.filters track by page.url" ng-class="navClass(page)" ng-animate="'expand'" class="api-list-item"> + <li ng-repeat="page in module.filters track by page.url" ng-class="navClass(page)" class="expand api-list-item"> <a href="{{page.url}}" tabindex="2">{{page.shortName}}</a> </li> <li class="nav-header section" ng-show="module.services"> <a href="{{URL.service}}" class="guide">service</a> </li> - <li ng-repeat="service in module.services track by service.instance.url" ng-animate="'expand'" ng-class="navClass(service.instance, service.provider)" class="api-list-item"> + <li ng-repeat="service in module.services track by service.instance.url" ng-class="navClass(service.instance, service.provider)" class="api-list-item expand"> <a ng-show="service.provider" class="pull-right" href="{{service.provider.url}}" tabindex="2"><i class="icon-cog"></i></a> <a href="{{service.instance.url}}" tabindex="2">{{service.name}}</a> </li> @@ -305,7 +306,7 @@ <li class="nav-header section" ng-show="module.types"> <a href="{{URL.type}}" class="guide">Types</a> </li> - <li ng-repeat="page in module.types track by page.url" ng-class="navClass(page)" ng-animate="'expand'" class="api-list-item"> + <li ng-repeat="page in module.types track by page.url" ng-class="navClass(page)" class="expand api-list-item"> <a href="{{page.url}}" tabindex="2">{{page.shortName}}</a> </li> @@ -334,7 +335,7 @@ <div id="loading" ng-show="loading">Loading...</div> - <div ng-hide="loading" ng-include src="currentPage.partialUrl" onload="afterPartialLoaded()" autoscroll class="content" ng-animate="{enter: 'slide-reveal'}" ></div> + <div ng-hide="loading" ng-include src="currentPage.partialUrl" onload="afterPartialLoaded()" autoscroll class="content slide-reveal"></div> <div id="disqus" class="disqus"> <h2>Discussion</h2> diff --git a/docs/src/templates/js/docs.js b/docs/src/templates/js/docs.js index 7cac6a9a..05b09571 100644 --- a/docs/src/templates/js/docs.js +++ b/docs/src/templates/js/docs.js @@ -803,7 +803,7 @@ docsApp.controller.DocsController = function($scope, $location, $window, $cookie }; -angular.module('docsApp', ['ngResource', 'ngRoute', 'ngCookies', 'ngSanitize', 'bootstrap', 'bootstrapPrettify', 'docsData']). +angular.module('docsApp', ['ngResource', 'ngRoute', 'ngCookies', 'ngSanitize', 'ngAnimate', 'bootstrap', 'bootstrapPrettify', 'docsData']). config(function($locationProvider) { $locationProvider.html5Mode(true).hashPrefix('!'); }). diff --git a/karma-docs.conf.js b/karma-docs.conf.js index 65f51fdd..7d21570d 100644 --- a/karma-docs.conf.js +++ b/karma-docs.conf.js @@ -15,6 +15,7 @@ module.exports = function(config) { 'build/angular-mobile.js', 'build/angular-sanitize.js', 'build/angular-route.js', + 'build/angular-animate.js', 'build/docs/components/lunr.js', 'build/docs/components/google-code-prettify.js', diff --git a/src/Angular.js b/src/Angular.js index 4e050a0c..68768d34 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1053,13 +1053,13 @@ function bootstrap(element, modules) { }]); modules.unshift('ng'); var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animator', - function(scope, element, compile, injector, animator) { + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', + function(scope, element, compile, injector, animate) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); - animator.enabled(true); + animate.enabled(true); }] ); return injector; diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 7745cce9..b225fc85 100755 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -106,8 +106,7 @@ function publishExternalAPI(angular){ directive(ngEventDirectives); $provide.provider({ $anchorScroll: $AnchorScrollProvider, - $animation: $AnimationProvider, - $animator: $AnimatorProvider, + $animate: $AnimateProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, diff --git a/src/loader.js b/src/loader.js index 712ff0f7..b28ccee2 100644 --- a/src/loader.js +++ b/src/loader.js @@ -173,24 +173,30 @@ function setupModuleLoader(window) { * @param {Function} animationFactory Factory function for creating new instance of an animation. * @description * - * Defines an animation hook that can be later used with {@link ng.directive:ngAnimate ngAnimate} - * alongside {@link ng.directive:ngAnimate#Description common ng directives} as well as custom directives. + * **NOTE**: animations are take effect only if the **ngAnimate** module is loaded. + * + * + * Defines an animation hook that can be later used with {@link ngAnimate.$animate $animate} service and + * directives that use this service. + * * <pre> - * module.animation('animation-name', function($inject1, $inject2) { + * module.animation('.animation-name', function($inject1, $inject2) { * return { - * //this gets called in preparation to setup an animation - * setup : function(element) { ... }, - * - * //this gets called once the animation is run - * start : function(element, done, memo) { ... } + * eventName : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction(element) { + * //code to cancel the animation + * } + * } * } * }) * </pre> * - * See {@link ng.$animationProvider#register $animationProvider.register()} and - * {@link ng.directive:ngAnimate ngAnimate} for more information. + * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * {@link ngAnimate ngAnimate module} for more information. */ - animation: invokeLater('$animationProvider', 'register'), + animation: invokeLater('$animateProvider', 'register'), /** * @ngdoc method diff --git a/src/ng/animate.js b/src/ng/animate.js new file mode 100644 index 00000000..7e515594 --- /dev/null +++ b/src/ng/animate.js @@ -0,0 +1,112 @@ +'use strict'; + +/** + * @ngdoc object + * @name ng.$animateProvider + * + * @description + * Default implementation of $animate that doesn't perform any animations, instead just synchronously performs DOM + * updates and calls done() callbacks. + * + * In order to enable animations the ngAnimate module has to be loaded. + * + * To see the functional implementation check out src/ngAnimate/animate.js + */ +var $AnimateProvider = ['$provide', function($provide) { + + this.$$selectors = []; + + + /** + * @ngdoc function + * @name ng.$animateProvider#register + * @methodOf ng.$animateProvider + * + * @description + * Registers a new injectable animation factory function. The factory function produces the animation object which + * contains callback functions for each event that is expected to be animated. + * + * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` must be called once the + * element animation is complete. If a function is returned then the animation service will use this function to + * cancel the animation whenever a cancel event is triggered. + * + * + *<pre> + * return { + * eventFn : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction() { + * //code to cancel the animation + * } + * } + * } + *</pre> + * + * @param {string} name The name of the animation. + * @param {function} factory The factory function that will be executed to return the animation object. + */ + this.register = function(name, factory) { + var classes = name.substr(1).split('.'); + name += '-animation'; + this.$$selectors.push({ + selectors : classes, + name : name + }); + $provide.factory(name, factory); + }; + + this.$get = function() { + return { + enter : function(element, parent, after, done) { + var afterNode = after && after[after.length - 1]; + var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; + // IE does not like undefined so we have to pass null. + var afterNextSibling = (afterNode && afterNode.nextSibling) || null; + forEach(element, function(node) { + parentNode.insertBefore(node, afterNextSibling); + }); + (done || noop)(); + }, + + leave : function(element, done) { + element.remove(); + (done || noop)(); + }, + + move : function(element, parent, after, done) { + // Do not remove element before insert. Removing will cause data associated with the + // element to be dropped. Insert will implicitly do the remove. + this.enter(element, parent, after, done); + }, + + show : function(element, done) { + element.removeClass('ng-hide'); + (done || noop)(); + }, + + hide : function(element, done) { + element.addClass('ng-hide'); + (done || noop)(); + }, + + addClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + element.addClass(className); + (done || noop)(); + }, + + removeClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + element.removeClass(className); + (done || noop)(); + }, + + enabled : noop + }; + }; +}]; diff --git a/src/ng/animation.js b/src/ng/animation.js deleted file mode 100644 index faed84ca..00000000 --- a/src/ng/animation.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @ngdoc object - * @name ng.$animationProvider - * @description - * - * The $AnimationProvider provider allows developers to register and access custom JavaScript animations directly inside - * of a module. - * - */ -$AnimationProvider.$inject = ['$provide']; -function $AnimationProvider($provide) { - var suffix = 'Animation'; - - /** - * @ngdoc function - * @name ng.$animation#register - * @methodOf ng.$animationProvider - * - * @description - * Registers a new injectable animation factory function. The factory function produces the animation object which - * has these two properties: - * - * * `setup`: `function(Element):*` A function which receives the starting state of the element. The purpose - * of this function is to get the element ready for animation. Optionally the function returns an memento which - * is passed to the `start` function. - * * `start`: `function(Element, doneFunction, *)` The element to animate, the `doneFunction` to be called on - * element animation completion, and an optional memento from the `setup` function. - * - * @param {string} name The name of the animation. - * @param {function} factory The factory function that will be executed to return the animation object. - * - */ - this.register = function(name, factory) { - $provide.factory(camelCase(name) + suffix, factory); - }; - - this.$get = ['$injector', function($injector) { - /** - * @ngdoc function - * @name ng.$animation - * @function - * - * @description - * The $animation service is used to retrieve any defined animation functions. When executed, the $animation service - * will return a object that contains the setup and start functions that were defined for the animation. - * - * @param {String} name Name of the animation function to retrieve. Animation functions are registered and stored - * inside of the AngularJS DI so a call to $animate('custom') is the same as injecting `customAnimation` - * via dependency injection. - * @return {Object} the animation object which contains the `setup` and `start` functions that perform the animation. - */ - return function $animation(name) { - if (name) { - var animationName = camelCase(name) + suffix; - if ($injector.has(animationName)) { - return $injector.get(animationName); - } - } - }; - }]; -} diff --git a/src/ng/animator.js b/src/ng/animator.js deleted file mode 100644 index a9ea5743..00000000 --- a/src/ng/animator.js +++ /dev/null @@ -1,446 +0,0 @@ -'use strict'; - -// NOTE: this is a pseudo directive. - -/** - * @ngdoc directive - * @name ng.directive:ngAnimate - * - * @description - * The `ngAnimate` directive works as an attribute that is attached alongside pre-existing directives. - * It effects how the directive will perform DOM manipulation. This allows for complex animations to take place - * without burdening the directive which uses the animation with animation details. The built in directives - * `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView` already accept `ngAnimate` directive. - * Custom directives can take advantage of animation through {@link ng.$animator $animator service}. - * - * Below is a more detailed breakdown of the supported callback events provided by pre-exisitng ng directives: - * - * | Directive | Supported Animations | - * |---------------------------------------------------------- |----------------------------------------------------| - * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | - * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | - * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | - * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | - * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | - * | {@link ng.directive:ngShow#animations ngShow & ngHide} | show and hide | - * - * You can find out more information about animations upon visiting each directive page. - * - * Below is an example of a directive that makes use of the ngAnimate attribute: - * - * <pre> - * <!-- you can also use data-ng-animate, ng:animate or x-ng-animate as well --> - * <ANY ng-directive ng-animate="{event1: 'animation-name', event2: 'animation-name-2'}"></ANY> - * - * <!-- you can also use a short hand --> - * //!annotate="animation" ngAnimate|This *expands* to `{ enter: 'animation-enter', leave: 'animation-leave', ...}`</strong> - * <ANY ng-directive ng-animate=" 'animation' "></ANY> - * - * <!-- keep in mind that ng-animate can take expressions --> - * //!annotate="computeCurrentAnimation\(\)" Scope Function|This will be called each time the scope changes... - * <ANY ng-directive ng-animate=" computeCurrentAnimation() "></ANY> - * </pre> - * - * The `event1` and `event2` attributes refer to the animation events specific to the directive that has been assigned. - * - * Keep in mind that if an animation is running, no child element of such animation can also be animated. - * - * <h2>CSS-defined Animations</h2> - * By default, ngAnimate attaches two CSS classes per animation event to the DOM element to achieve the animation. - * It is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions as - * well as CSS animations. - * - * The following code below demonstrates how to perform animations using **CSS transitions** with ngAnimate: - * - * <pre> - * <style type="text/css"> - * /* - * The animate-enter CSS class is the event name that you - * have provided within the ngAnimate attribute. - * */ - * .animate-enter { - * -webkit-transition: 1s linear all; /* Safari/Chrome */ - * -moz-transition: 1s linear all; /* Firefox */ - * -o-transition: 1s linear all; /* Opera */ - * transition: 1s linear all; /* IE10+ and Future Browsers */ - * - * /* The animation preparation code */ - * opacity: 0; - * } - * - * /* - * Keep in mind that you want to combine both CSS - * classes together to avoid any CSS-specificity - * conflicts - * */ - * .animate-enter.animate-enter-active { - * /* The animation code itself */ - * opacity: 1; - * } - * </style> - * - * <div ng-directive ng-animate="{enter: 'animate-enter'}"></div> - * </pre> - * - * The following code below demonstrates how to perform animations using **CSS animations** with ngAnimate: - * - * <pre> - * <style type="text/css"> - * .animate-enter { - * -webkit-animation: enter_sequence 1s linear; /* Safari/Chrome */ - * -moz-animation: enter_sequence 1s linear; /* Firefox */ - * -o-animation: enter_sequence 1s linear; /* Opera */ - * animation: enter_sequence 1s linear; /* IE10+ and Future Browsers */ - * } - * @-webkit-keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } - * @-moz-keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } - * @-o-keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } - * @keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } - * </style> - * - * <div ng-directive ng-animate="{enter: 'animate-enter'}"></div> - * </pre> - * - * ngAnimate will first examine any CSS animation code and then fallback to using CSS transitions. - * - * Upon DOM mutation, the event class is added first, then the browser is allowed to reflow the content and then, - * the active class is added to trigger the animation. The ngAnimate directive will automatically extract the duration - * of the animation to determine when the animation ends. Once the animation is over then both CSS classes will be - * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end - * immediately resulting in a DOM element that is at it's final state. This final state is when the DOM element - * has no CSS transition/animation classes surrounding it. - * - * <h2>JavaScript-defined Animations</h2> - * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations to browsers that do not - * yet support them, then you can make use of JavaScript animations defined inside of your AngularJS module. - * - * <pre> - * var ngModule = angular.module('YourApp', []); - * ngModule.animation('animate-enter', function() { - * return { - * setup : function(element) { - * //prepare the element for animation - * element.css({ 'opacity': 0 }); - * var memo = "..."; //this value is passed to the start function - * return memo; - * }, - * start : function(element, done, memo) { - * //start the animation - * element.animate({ - * 'opacity' : 1 - * }, function() { - * //call when the animation is complete - * done() - * }); - * } - * } - * }); - * </pre> - * - * As you can see, the JavaScript code follows a similar template to the CSS3 animations. Once defined, the animation - * can be used in the same way with the ngAnimate attribute. Keep in mind that, when using JavaScript-enabled - * animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're not using - * CSS animations) to animated the element, but it will not attempt to find any CSS3 transition or animation duration/delay values. - * It will instead close off the animation once the provided done function is executed. So it's important that you - * make sure your animations remember to fire off the done function once the animations are complete. - * - * @param {expression} ngAnimate Used to configure the DOM manipulation animations. - * - */ - -var $AnimatorProvider = function() { - var NG_ANIMATE_CONTROLLER = '$ngAnimateController'; - var rootAnimateController = {running:true}; - - this.$get = ['$animation', '$window', '$sniffer', '$rootElement', '$rootScope', - function($animation, $window, $sniffer, $rootElement, $rootScope) { - $rootElement.data(NG_ANIMATE_CONTROLLER, rootAnimateController); - - /** - * @ngdoc function - * @name ng.$animator - * @function - * - * @description - * The $animator.create service provides the DOM manipulation API which is decorated with animations. - * - * @param {Scope} scope the scope for the ng-animate. - * @param {Attributes} attr the attributes object which contains the ngAnimate key / value pair. (The attributes are - * passed into the linking function of the directive using the `$animator`.) - * @return {object} the animator object which contains the enter, leave, move, show, hide and animate methods. - */ - var AnimatorService = function(scope, attrs) { - var animator = {}; - - /** - * @ngdoc function - * @name ng.animator#enter - * @methodOf ng.$animator - * @function - * - * @description - * Injects the element object into the DOM (inside of the parent element) and then runs the enter animation. - * - * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation - * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation - * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation - */ - animator.enter = animateActionFactory('enter', insert, noop); - - /** - * @ngdoc function - * @name ng.animator#leave - * @methodOf ng.$animator - * @function - * - * @description - * Runs the leave animation operation and, upon completion, removes the element from the DOM. - * - * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation - * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the leave animation - */ - animator.leave = animateActionFactory('leave', noop, remove); - - /** - * @ngdoc function - * @name ng.animator#move - * @methodOf ng.$animator - * @function - * - * @description - * Fires the move DOM operation. Just before the animation starts, the animator will either append it into the parent container or - * add the element directly after the after element if present. Then the move animation will be run. - * - * @param {jQuery/jqLite element} element the element that will be the focus of the move animation - * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation - * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation - */ - animator.move = animateActionFactory('move', move, noop); - - /** - * @ngdoc function - * @name ng.animator#show - * @methodOf ng.$animator - * @function - * - * @description - * Reveals the element by setting the CSS property `display` to `block` and then starts the show animation directly after. - * - * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden - */ - animator.show = animateActionFactory('show', show, noop); - - /** - * @ngdoc function - * @name ng.animator#hide - * @methodOf ng.$animator - * - * @description - * Starts the hide animation first and sets the CSS `display` property to `none` upon completion. - * - * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden - */ - animator.hide = animateActionFactory('hide', noop, hide); - - /** - * @ngdoc function - * @name ng.animator#animate - * @methodOf ng.$animator - * - * @description - * Triggers a custom animation event to be executed on the given element - * - * @param {string} event the name of the custom event - * @param {jQuery/jqLite element} element the element that will be animated - */ - animator.animate = function(event, element) { - animateActionFactory(event, noop, noop)(element); - } - return animator; - - function animateActionFactory(type, beforeFn, afterFn) { - return function(element, parent, after) { - var ngAnimateValue = scope.$eval(attrs.ngAnimate); - var className = ngAnimateValue - ? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type - : ''; - var animationPolyfill = $animation(className); - var polyfillSetup = animationPolyfill && animationPolyfill.setup; - var polyfillStart = animationPolyfill && animationPolyfill.start; - var polyfillCancel = animationPolyfill && animationPolyfill.cancel; - - if (!className) { - beforeFn(element, parent, after); - afterFn(element, parent, after); - } else { - var activeClassName = className + '-active'; - - if (!parent) { - parent = after ? after.parent() : element.parent(); - } - var disabledAnimation = { running : true }; - if ((!$sniffer.transitions && !polyfillSetup && !polyfillStart) || - (parent.inheritedData(NG_ANIMATE_CONTROLLER) || disabledAnimation).running) { - beforeFn(element, parent, after); - afterFn(element, parent, after); - return; - } - - var animationData = element.data(NG_ANIMATE_CONTROLLER) || {}; - if(animationData.running) { - (polyfillCancel || noop)(element); - animationData.done(); - } - - element.data(NG_ANIMATE_CONTROLLER, {running:true, done:done}); - element.addClass(className); - beforeFn(element, parent, after); - if (element.length == 0) return done(); - - var memento = (polyfillSetup || noop)(element); - - // $window.setTimeout(beginAnimation, 0); this was causing the element not to animate - // keep at 1 for animation dom rerender - $window.setTimeout(beginAnimation, 1); - } - - function parseMaxTime(str) { - var total = 0, values = isString(str) ? str.split(/\s*,\s*/) : []; - forEach(values, function(value) { - total = Math.max(parseFloat(value) || 0, total); - }); - return total; - } - - function beginAnimation() { - element.addClass(activeClassName); - if (polyfillStart) { - polyfillStart(element, done, memento); - } else if (isFunction($window.getComputedStyle)) { - //one day all browsers will have these properties - var w3cAnimationProp = 'animation'; - var w3cTransitionProp = 'transition'; - - //but some still use vendor-prefixed styles - var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; - var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; - - var durationKey = 'Duration', - delayKey = 'Delay', - animationIterationCountKey = 'IterationCount', - duration = 0; - - //we want all the styles defined before and after - var ELEMENT_NODE = 1; - 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])); - - var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), - parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); - - if(animationDuration > 0) { - animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, - parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, - 1); - } - - duration = Math.max(animationDelay + animationDuration, - transitionDelay + transitionDuration, - duration); - } - }); - $window.setTimeout(done, duration * 1000); - } else { - done(); - } - } - - function done() { - if(!done.run) { - done.run = true; - afterFn(element, parent, after); - element.removeClass(className); - element.removeClass(activeClassName); - element.removeData(NG_ANIMATE_CONTROLLER); - } - } - }; - } - - function show(element) { - element.css('display', ''); - } - - function hide(element) { - element.css('display', 'none'); - } - - function insert(element, parent, after) { - var afterNode = after && after[after.length - 1]; - var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; - var afterNextSibling = afterNode && afterNode.nextSibling; - forEach(element, function(node) { - if (afterNextSibling) { - parentNode.insertBefore(node, afterNextSibling); - } else { - parentNode.appendChild(node); - } - }); - } - - function remove(element) { - element.remove(); - } - - function move(element, parent, after) { - // Do not remove element before insert. Removing will cause data associated with the - // element to be dropped. Insert will implicitly do the remove. - insert(element, parent, after); - } - }; - - /** - * @ngdoc function - * @name ng.animator#enabled - * @methodOf ng.$animator - * @function - * - * @param {Boolean=} If provided then set the animation on or off. - * @return {Boolean} Current animation state. - * - * @description - * Globally enables/disables animations. - * - */ - AnimatorService.enabled = function(value) { - if (arguments.length) { - rootAnimateController.running = !value; - } - return !rootAnimateController.running; - }; - - return AnimatorService; - }]; -}; diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js index 75e35a1e..a5b2acb6 100644 --- a/src/ng/directive/ngClass.js +++ b/src/ng/directive/ngClass.js @@ -2,59 +2,72 @@ function classDirective(name, selector) { name = 'ngClass' + name; - return ngDirective(function(scope, element, attr) { - var oldVal = undefined; - - scope.$watch(attr[name], ngClassWatchAction, true); - - attr.$observe('class', function(value) { - var ngClass = scope.$eval(attr[name]); - ngClassWatchAction(ngClass, ngClass); - }); + return ['$animate', function($animate) { + return { + restrict: 'AC', + link: function(scope, element, attr) { + var oldVal = undefined; + + scope.$watch(attr[name], ngClassWatchAction, true); + + attr.$observe('class', function(value) { + var ngClass = scope.$eval(attr[name]); + ngClassWatchAction(ngClass, ngClass); + }); + + + if (name !== 'ngClass') { + scope.$watch('$index', function($index, old$index) { + var mod = $index & 1; + if (mod !== old$index & 1) { + if (mod === selector) { + addClass(scope.$eval(attr[name])); + } else { + removeClass(scope.$eval(attr[name])); + } + } + }); + } - if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - var mod = $index & 1; - if (mod !== old$index & 1) { - if (mod === selector) { - addClass(scope.$eval(attr[name])); - } else { - removeClass(scope.$eval(attr[name])); + function ngClassWatchAction(newVal) { + if (selector === true || scope.$index % 2 === selector) { + if (oldVal && !equals(newVal,oldVal)) { + removeClass(oldVal); + } + addClass(newVal); } + oldVal = copy(newVal); } - }); - } - function ngClassWatchAction(newVal) { - if (selector === true || scope.$index % 2 === selector) { - if (oldVal && !equals(newVal,oldVal)) { - removeClass(oldVal); + function removeClass(classVal) { + $animate.removeClass(element, flattenClasses(classVal)); } - addClass(newVal); - } - oldVal = copy(newVal); - } - function removeClass(classVal) { - if (isObject(classVal) && !isArray(classVal)) { - classVal = map(classVal, function(v, k) { if (v) return k }); - } - element.removeClass(isArray(classVal) ? classVal.join(' ') : classVal); - } + function addClass(classVal) { + $animate.addClass(element, flattenClasses(classVal)); + } + function flattenClasses(classVal) { + if(isArray(classVal)) { + return classVal.join(' '); + } else if (isObject(classVal)) { + var classes = [], i = 0; + forEach(classVal, function(v, k) { + if (v) { + classes.push(k); + } + }); + return classes.join(' '); + } - function addClass(classVal) { - if (isObject(classVal) && !isArray(classVal)) { - classVal = map(classVal, function(v, k) { if (v) return k }); + return classVal; + }; } - if (classVal) { - element.addClass(isArray(classVal) ? classVal.join(' ') : classVal); - } - } - }); + }; + }]; } /** @@ -70,6 +83,10 @@ function classDirective(name, selector) { * When the expression changes, the previously added classes are removed and only then the * new classes are added. * + * @animations + * add - happens just before the class is applied to the element + * remove - happens just before the class is removed from the element + * * @element ANY * @param {expression} ngClass {@link guide/expression Expression} to eval. The result * of the evaluation can be a string representing space delimited class @@ -78,7 +95,7 @@ function classDirective(name, selector) { * element. * * @example - <example> + <example animations="true"> <file name="index.html"> <input type="button" value="set" ng-click="myVar='my-class'"> <input type="button" value="clear" ng-click="myVar=''"> @@ -86,8 +103,23 @@ function classDirective(name, selector) { <span ng-class="myVar">Sample Text</span> </file> <file name="style.css"> - .my-class { + .my-class-add, + .my-class-remove { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + } + + .my-class, + .my-class-add.my-class-add-active { color: red; + font-size:3em; + } + + .my-class-remove.my-class-remove-active { + font-size:1.0em; + color:black; } </file> <file name="scenario.js"> diff --git a/src/ng/directive/ngIf.js b/src/ng/directive/ngIf.js index c8166ee5..9d99d859 100755 --- a/src/ng/directive/ngIf.js +++ b/src/ng/directive/ngIf.js @@ -30,7 +30,7 @@ * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element * the added class will be lost because the original compiled state is used to regenerate the element. * - * Additionally, you can provide animations via the ngAnimate attribute to animate the **enter** + * Additionally, you can provide animations via the ngAnimate module to animate the **enter** * and **leave** effects. * * @animations @@ -47,36 +47,32 @@ <file name="index.html"> Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/> Show when checked: - <span ng-if="checked" ng-animate="'example'"> + <span ng-if="checked" class="example-if"> I'm removed when the checkbox is unchecked. </span> </file> <file name="animations.css"> - .example-leave, .example-enter { + .example-if.ng-enter, + .example-if.ng-leave { -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } - .example-enter { + .example-if.ng-enter, + .example-if.ng-leave.ng-leave-active { opacity:0; } - .example-enter.example-enter-active { - opacity:1; - } - .example-leave { + .example-if.ng-enter.ng-enter-active, + .example-if.ng-leave { opacity:1; } - .example-leave.example-leave-active { - opacity:0; - } </file> </example> */ -var ngIfDirective = ['$animator', function($animator) { +var ngIfDirective = ['$animate', function($animate) { return { transclude: 'element', priority: 1000, @@ -84,11 +80,10 @@ var ngIfDirective = ['$animator', function($animator) { restrict: 'A', compile: function (element, attr, transclude) { return function ($scope, $element, $attr) { - var animate = $animator($scope, $attr); var childElement, childScope; $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { if (childElement) { - animate.leave(childElement); + $animate.leave(childElement); childElement = undefined; } if (childScope) { @@ -99,7 +94,7 @@ var ngIfDirective = ['$animator', function($animator) { childScope = $scope.$new(); transclude(childScope, function (clone) { childElement = clone; - animate.enter(clone, $element.parent(), $element); + $animate.enter(clone, $element.parent(), $element); }); } }); diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js index 72b5af08..d5ed1fc5 100644 --- a/src/ng/directive/ngInclude.js +++ b/src/ng/directive/ngInclude.js @@ -23,9 +23,6 @@ * (e.g. ngInclude won't work for cross-domain requests on all browsers and for `file://` * access on some browsers) * - * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter** - * and **leave** effects. - * * @animations * enter - happens just after the ngInclude contents change and a new DOM element is created and injected into the ngInclude container * leave - happens just after the ngInclude contents change and just before the former contents are removed from the DOM @@ -143,8 +140,8 @@ * @description * Emitted every time the ngInclude content is reloaded. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animator', '$sce', - function($http, $templateCache, $anchorScroll, $compile, $animator, $sce) { +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animate', '$sce', + function($http, $templateCache, $anchorScroll, $compile, $animate, $sce) { return { restrict: 'ECA', terminal: true, @@ -154,7 +151,6 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' autoScrollExp = attr.autoscroll; return function(scope, element, attr) { - var animate = $animator(scope, attr); var changeCounter = 0, childScope; @@ -163,7 +159,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' childScope.$destroy(); childScope = null; } - animate.leave(element.contents(), element); + $animate.leave(element.contents()); }; scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { @@ -175,11 +171,11 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' if (childScope) childScope.$destroy(); childScope = scope.$new(); - animate.leave(element.contents(), element); + $animate.leave(element.contents()); var contents = jqLite('<div/>').html(response).contents(); - animate.enter(contents, element); + $animate.enter(contents, element); $compile(contents)(childScope); if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index e0b2cb38..8f12b7c2 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -20,9 +20,6 @@ * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | * - * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**, - * **leave** and **move** effects. - * * * # Special repeat start and end points * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending @@ -131,46 +128,40 @@ I have {{friends.length}} friends. They are: <input type="search" ng-model="q" placeholder="filter friends..." /> <ul> - <li ng-repeat="friend in friends | filter:q" - ng-animate="{enter: 'example-repeat-enter', - leave: 'example-repeat-leave', - move: 'example-repeat-move'}"> + <li class="animate-repeat" ng-repeat="friend in friends | filter:q"> [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. </li> </ul> </div> </file> <file name="animations.css"> - .example-repeat-enter, - .example-repeat-leave, - .example-repeat-move { + .animate-repeat { -webkit-transition:all linear 0.5s; -moz-transition:all linear 0.5s; - -ms-transition:all linear 0.5s; -o-transition:all linear 0.5s; transition:all linear 0.5s; } - .example-repeat-enter { + .animate-repeat.ng-enter { line-height:0; opacity:0; } - .example-repeat-enter.example-repeat-enter-active { + .animate-repeat.ng-enter.ng-enter-active { line-height:20px; opacity:1; } - .example-repeat-leave { + .animate-repeat.ng-leave { opacity:1; line-height:20px; } - .example-repeat-leave.example-repeat-leave-active { + .animate-repeat.ng-leave.ng-leave-active { opacity:0; line-height:0; } - .example-repeat-move { } - .example-repeat-move.example-repeat-move-active { } + .animate-repeat.ng-move { } + .animate-repeat.ng-move.ng-move-active { } </file> <file name="scenario.js"> it('should render initial data set', function() { @@ -195,7 +186,7 @@ </file> </example> */ -var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { +var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); return { @@ -204,7 +195,6 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { terminal: true, compile: function(element, attr, linker) { return function($scope, $element, $attr){ - var animate = $animator($scope, $attr); var expression = $attr.ngRepeat; var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/), trackByExp, trackByExpGetter, trackByIdFn, trackByIdArrayFn, trackByIdObjFn, lhs, rhs, valueIdentifier, keyIdentifier, @@ -316,7 +306,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { for (key in lastBlockMap) { if (lastBlockMap.hasOwnProperty(key)) { block = lastBlockMap[key]; - animate.leave(block.elements); + $animate.leave(block.elements); forEach(block.elements, function(element) { element[NG_REMOVED] = true}); block.scope.$destroy(); } @@ -342,7 +332,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { // do nothing } else { // existing item which got moved - animate.move(block.elements, null, jqLite(previousNode)); + $animate.move(block.elements, null, jqLite(previousNode)); } previousNode = block.endNode; } else { @@ -360,7 +350,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (!block.startNode) { linker(childScope, function(clone) { - animate.enter(clone, null, jqLite(previousNode)); + $animate.enter(clone, null, jqLite(previousNode)); previousNode = clone; block.scope = childScope; block.startNode = clone[0]; diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js index 7ef7008c..bdbcf463 100644 --- a/src/ng/directive/ngShowHide.js +++ b/src/ng/directive/ngShowHide.js @@ -12,8 +12,6 @@ * With ngHide this is the reverse whereas true values cause the element itself to become * hidden. * - * Additionally, you can also provide animations via the ngAnimate attribute to animate the **show** - * and **hide** effects. * * @animations * show - happens after the ngShow expression evaluates to a truthy value and the contents are set to visible @@ -29,36 +27,37 @@ Click me: <input type="checkbox" ng-model="checked"><br/> <div> Show: - <span class="check-element" - ng-show="checked" - ng-animate="{show: 'example-show', hide: 'example-hide'}"> + <span class="check-element example-show-hide" ng-show="checked"> <span class="icon-thumbs-up"></span> I show up when your checkbox is checked. </span> </div> <div> Hide: - <span class="check-element" - ng-hide="checked" - ng-animate="{show: 'example-show', hide: 'example-hide'}"> + <span class="check-element example-show-hide" ng-hide="checked"> <span class="icon-thumbs-down"></span> I hide when your checkbox is checked. </span> </div> </file> <file name="animations.css"> - .example-show, .example-hide { + .example-show-hide { -webkit-transition:all linear 0.5s; -moz-transition:all linear 0.5s; -ms-transition:all linear 0.5s; -o-transition:all linear 0.5s; transition:all linear 0.5s; + display:block; + } + .example-show-hide.ng-hide { + display:none; } - .example-show { + .example-show-hide.ng-hide-remove { + display:block; line-height:0; opacity:0; padding:0 10px; } - .example-show-active.example-show-active { + .example-show-hide.ng-hide-remove.ng-hide-remove-active { line-height:20px; opacity:1; padding:10px; @@ -66,14 +65,14 @@ background:white; } - .example-hide { + .example-show-hide.ng-hide-add { line-height:20px; opacity:1; padding:10px; border:1px solid black; background:white; } - .example-hide-active.example-hide-active { + .example-show-hide.ng-hide-add.ng-hide-add-active { line-height:0; opacity:0; padding:0 10px; @@ -98,12 +97,10 @@ </file> </example> */ -//TODO(misko): refactor to remove element from the DOM -var ngShowDirective = ['$animator', function($animator) { +var ngShowDirective = ['$animate', function($animate) { return function(scope, element, attr) { - var animate = $animator(scope, attr); scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - animate[toBoolean(value) ? 'show' : 'hide'](element); + $animate[toBoolean(value) ? 'show' : 'hide'](element); }); }; }]; @@ -121,9 +118,6 @@ var ngShowDirective = ['$animator', function($animator) { * With ngHide this is the reverse whereas true values cause the element itself to become * hidden. * - * Additionally, you can also provide animations via the ngAnimate attribute to animate the **show** - * and **hide** effects. - * * @animations * show - happens after the ngHide expression evaluates to a non truthy value and the contents are set to visible * hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden @@ -138,36 +132,36 @@ var ngShowDirective = ['$animator', function($animator) { Click me: <input type="checkbox" ng-model="checked"><br/> <div> Show: - <span class="check-element" - ng-show="checked" - ng-animate="{show: 'example-show', hide: 'example-hide'}"> + <span class="check-element example-show-hide" ng-show="checked"> <span class="icon-thumbs-up"></span> I show up when your checkbox is checked. </span> </div> <div> Hide: - <span class="check-element" - ng-hide="checked" - ng-animate="{show: 'example-show', hide: 'example-hide'}"> + <span class="check-element example-show-hide" ng-hide="checked"> <span class="icon-thumbs-down"></span> I hide when your checkbox is checked. </span> </div> </file> <file name="animations.css"> - .example-show, .example-hide { + .example-show-hide { -webkit-transition:all linear 0.5s; -moz-transition:all linear 0.5s; - -ms-transition:all linear 0.5s; -o-transition:all linear 0.5s; transition:all linear 0.5s; + display:block; + } + .example-show-hide.ng-hide { + display:none; } - .example-show { + .example-show-hide.ng-hide-remove { + display:block; line-height:0; opacity:0; padding:0 10px; } - .example-show.example-show-active { + .example-show-hide.ng-hide-remove.ng-hide-remove-active { line-height:20px; opacity:1; padding:10px; @@ -175,14 +169,14 @@ var ngShowDirective = ['$animator', function($animator) { background:white; } - .example-hide { + .example-show-hide.ng-hide-add { line-height:20px; opacity:1; padding:10px; border:1px solid black; background:white; } - .example-hide.example-hide-active { + .example-show-hide.ng-hide-add.ng-hide-add-active { line-height:0; opacity:0; padding:0 10px; @@ -207,12 +201,10 @@ var ngShowDirective = ['$animator', function($animator) { </file> </example> */ -//TODO(misko): refactor to remove element from the DOM -var ngHideDirective = ['$animator', function($animator) { +var ngHideDirective = ['$animate', function($animate) { return function(scope, element, attr) { - var animate = $animator(scope, attr); scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - animate[toBoolean(value) ? 'hide' : 'show'](element); + $animate[toBoolean(value) ? 'hide' : 'show'](element); }); }; }]; diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js index f36e651c..38a123a2 100644 --- a/src/ng/directive/ngSwitch.js +++ b/src/ng/directive/ngSwitch.js @@ -19,9 +19,6 @@ * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default * attribute is displayed. * - * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter** - * and **leave** effects. - * * @animations * enter - happens after the ngSwtich contents change and the matched child element is placed inside the container * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM @@ -55,9 +52,8 @@ <tt>selection={{selection}}</tt> <hr/> <div - class="example-animate-container" - ng-switch on="selection" - ng-animate="{enter: 'example-enter', leave: 'example-leave'}"> + class="example-animate-container animate-switch" + ng-switch on="selection"> <div ng-switch-when="settings">Settings Div</div> <div ng-switch-when="home">Home Span</div> <div ng-switch-default>default</div> @@ -71,10 +67,9 @@ } </file> <file name="animations.css"> - .example-leave, .example-enter { + .animate-switch > * { -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; @@ -90,17 +85,17 @@ padding:10px; } - .example-enter { + .animate-switch > .ng-enter { top:-50px; } - .example-enter.example-enter-active { + .animate-switch > .ng-enter.ng-enter-active { top:0; } - .example-leave { + .animate-switch > .ng-leave { top:0; } - .example-leave.example-leave-active { + .animate-switch > .ng-leave.ng-leave-active { top:50px; } </file> @@ -119,7 +114,7 @@ </file> </example> */ -var ngSwitchDirective = ['$animator', function($animator) { +var ngSwitchDirective = ['$animate', function($animate) { return { restrict: 'EA', require: 'ngSwitch', @@ -129,7 +124,6 @@ var ngSwitchDirective = ['$animator', function($animator) { this.cases = {}; }], link: function(scope, element, attr, ngSwitchController) { - var animate = $animator(scope, attr); var watchExpr = attr.ngSwitch || attr.on, selectedTranscludes, selectedElements, @@ -138,7 +132,7 @@ var ngSwitchDirective = ['$animator', function($animator) { scope.$watch(watchExpr, function ngSwitchWatchAction(value) { for (var i= 0, ii=selectedScopes.length; i<ii; i++) { selectedScopes[i].$destroy(); - animate.leave(selectedElements[i]); + $animate.leave(selectedElements[i]); } selectedElements = []; @@ -153,7 +147,7 @@ var ngSwitchDirective = ['$animator', function($animator) { var anchor = selectedTransclude.element; selectedElements.push(caseElement); - animate.enter(caseElement, anchor.parent(), anchor); + $animate.enter(caseElement, anchor.parent(), anchor); }); }); } diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js new file mode 100644 index 00000000..ab7a19b0 --- /dev/null +++ b/src/ngAnimate/animate.js @@ -0,0 +1,714 @@ +/** + * @ngdoc overview + * @name ngAnimate + * @description + * + * ngAnimate + * ========= + * + * The ngAnimate module is an optional module that comes packed with AngularJS that can be included within an AngularJS + * application to provide support for CSS and JavaScript animation hooks. + * + * To make use of animations with AngularJS, the `angular-animate.js` JavaScript file must be included into your application + * and the `ngAnimate` module must be included as a dependency. + * + * <pre> + * angular.module('App', ['ngAnimate']); + * </pre> + * + * Then, to see animations in action, all that is required is to define the appropriate CSS classes + * or to register a JavaScript animation via the $animation service. The directives that support animation automatically are: + * `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView`. Custom directives can take advantage of animation + * by using the `$animate` service. + * + * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: + * + * | Directive | Supported Animations | + * |========================================================== |====================================================| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngShow#animations ngShow & ngHide} | show and hide | + * | {@link ng.directive:ngShow#animations ngClass} | add and remove | + * + * You can find out more information about animations upon visiting each directive page. + * + * Below is an example of how to apply animations to a directive that supports animation hooks: + * + * <pre> + * <style type="text/css"> + * .slide.ng-enter > div, + * .slide.ng-leave > div { + * -webkit-transition:0.5s linear all; + * -moz-transition:0.5s linear all; + * -o-transition:0.5s linear all; + * transition:0.5s linear all; + * } + * + * .slide > .ng-enter { } /* starting animations for enter */ + * .slide > .ng-enter-active { } /* terminal animations for enter */ + * .slide > .ng-leave { } /* starting animations for leave */ + * .slide > .ng-leave-active { } /* terminal animations for leave */ + * </style> + * + * <!-- + * the animate service will automatically add .ng-enter and .ng-leave to the element + * to trigger the CSS animations + * --> + * <ANY class="slide" ng-include="..."></ANY> + * </pre> + * + * Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's + * animation has completed. + * + * <h2>CSS-defined Animations</h2> + * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes + * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported + * and can be used to play along with this naming structure. + * + * The following code below demonstrates how to perform animations using **CSS transitions** with Angular: + * + * <pre> + * <style type="text/css"> + * /* + * The animate class is apart of the element and the ng-enter class + * is attached to the element once the enter animation event is triggered + * */ + * .reveal-animation.ng-enter { + * -webkit-transition: 1s linear all; /* Safari/Chrome */ + * -moz-transition: 1s linear all; /* Firefox */ + * -o-transition: 1s linear all; /* Opera */ + * transition: 1s linear all; /* IE10+ and Future Browsers */ + * + * /* The animation preparation code */ + * opacity: 0; + * } + * + * /* + * Keep in mind that you want to combine both CSS + * classes together to avoid any CSS-specificity + * conflicts + * */ + * .reveal-animation.ng-enter.ng-enter-active { + * /* The animation code itself */ + * opacity: 1; + * } + * </style> + * + * <div class="view-container"> + * <div ng-view class="reveal-animation"></div> + * </div> + * </pre> + * + * The following code below demonstrates how to perform animations using **CSS animations** with Angular: + * + * <pre> + * <style type="text/css"> + * .reveal-animation.ng-enter { + * -webkit-animation: enter_sequence 1s linear; /* Safari/Chrome */ + * -moz-animation: enter_sequence 1s linear; /* Firefox */ + * -o-animation: enter_sequence 1s linear; /* Opera */ + * animation: enter_sequence 1s linear; /* IE10+ and Future Browsers */ + * } + * @-webkit-keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * @-moz-keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * @-o-keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * @keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * </style> + * + * <div class="view-container"> + * <div ng-view class="reveal-animation"></div> + * </div> + * </pre> + * + * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing. + * + * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add + * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically + * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be + * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end + * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element + * has no CSS transition/animation classes applied to it. + * + * <h2>JavaScript-defined Animations</h2> + * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not + * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module. + * + * <pre> + * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application. + * var ngModule = angular.module('YourApp', []); + * ngModule.animation('.my-crazy-animation', function() { + * return { + * enter: function(element, done) { + * //run the animation + * //!annotate Cancel Animation|This function (if provided) will perform the cancellation of the animation when another is triggered + * return function(element, done) { + * //cancel the animation + * } + * } + * leave: function(element, done) { }, + * move: function(element, done) { }, + * show: function(element, done) { }, + * hide: function(element, done) { }, + * addClass: function(element, className, done) { }, + * removeClass: function(element, className, done) { }, + * } + * }); + * </pre> + * + * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run + * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits + * the element's CSS class attribute value and then run the matching animation event function (if found). + * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function + * be executed. It should be also noted that only simple or compound class selectors are allowed. + * + * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned. + * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run, + * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation + * or transition code that is defined via a stylesheet). + * + */ + +angular.module('ngAnimate', ['ng']) + + /** + * @ngdoc object + * @name ngAnimate.$animateProvider + * @description + * + * The $AnimationProvider provider allows developers to register and access custom JavaScript animations directly inside + * of a module. When an animation is triggered, the $animate service will query the $animation function to find any + * animations that match the provided name value. + * + * Please visit the {@link ngAnimate ngAnimate} module overview page learn more about how to use animations in your application. + * + */ + .config(['$provide', '$animateProvider', function($provide, $animateProvider) { + var selectors = $animateProvider.$$selectors; + + var NG_ANIMATE_STATE = '$$ngAnimateState'; + var rootAnimateState = {running:true}; + + $provide.decorator('$animate', ['$delegate', '$injector', '$window', '$sniffer', '$rootElement', + function($delegate, $injector, $window, $sniffer, $rootElement) { + + var noop = angular.noop; + var forEach = angular.forEach; + + $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); + + function lookup(name) { + if (name) { + var classes = name.substr(1).split('.'), + classMap = {}; + + for (var i = 0, ii = classes.length; i < ii; i++) { + classMap[classes[i]] = true; + } + + var matches = []; + for (var i = 0, ii = selectors.length; i < ii; i++) { + var selectorFactory = selectors[i]; + var found = true; + for(var j = 0, jj = selectorFactory.selectors.length; j < jj; j++) { + var klass = selectorFactory.selectors[j]; + if(klass.length > 0) { + found = found && classMap[klass]; + } + } + if(found) { + matches.push($injector.get(selectorFactory.name)); + } + } + return matches; + } + }; + + /** + * @ngdoc object + * @name ngAnimate.$animate + * @requires $window, $sniffer, $rootElement + * @function + * + * @description + * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) + * as well as during addClass and removeClass operations. When any of these operations are run, the $animate service + * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object) + * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run. + * + * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives + * will work out of the box without any extra configuration. + * + * Please visit the {@link ngAnimate ngAnimate} module overview page learn more about how to use animations in your application. + * + */ + return { + /** + * @ngdoc function + * @name ngAnimate.$animate#enter + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Appends the element to the parent element that resides in the document and then runs the enter animation. Once + * the animation is started, the following CSS classes will be present on the element for the duration of the animation: + * <pre> + * .ng-enter + * .ng-enter-active + * </pre> + * + * Once the animation is complete then the done callback, if provided, will be also fired. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation + * @param {function()=} done callback function that will be called once the animation is complete + */ + enter : function(element, parent, after, done) { + $delegate.enter(element, parent, after); + performAnimation('enter', 'ng-enter', element, parent, after, done); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#leave + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once + * the animation is started, the following CSS classes will be added for the duration of the animation: + * <pre> + * .ng-leave + * .ng-leave-active + * </pre> + * + * Once the animation is complete then the done callback, if provided, will be also fired. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation + * @param {function()=} done callback function that will be called once the animation is complete + */ + leave : function(element, done) { + performAnimation('leave', 'ng-leave', element, null, null, function() { + $delegate.leave(element, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#move + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parent container or + * add the element directly after the after element if present. Then the move animation will be run. Once + * the animation is started, the following CSS classes will be added for the duration of the animation: + * <pre> + * .ng-move + * .ng-move-active + * </pre> + * + * Once the animation is complete then the done callback, if provided, will be also fired. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the move animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation + * @param {function()=} done callback function that will be called once the animation is complete + */ + move : function(element, parent, after, done) { + $delegate.move(element, parent, after); + performAnimation('move', 'ng-move', element, null, null, done); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#show + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Reveals the element by removing the `ng-hide` class thus performing an animation in the process. During + * this animation the CSS classes present on the element will be: + * + * <pre> + * .ng-hide //already on the element if hidden + * .ng-hide-remove + * .ng-hide-remove-active + * </pre> + * + * Once the animation is complete then all three CSS classes will be removed from the element. + * The done callback, if provided, will be also fired once the animation is complete. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + * @param {function()=} done callback function that will be called once the animation is complete + */ + show : function(element, done) { + performAnimation('show', 'ng-hide-remove', element, null, null, function() { + $delegate.show(element, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#hide + * @methodOf ngAnimate.$animate + * + * @description + * Sets the element to hidden by adding the `ng-hide` class it. However, before the class is applied + * the following CSS classes will be added temporarily to trigger any animation code: + * + * <pre> + * .ng-hide-add + * .ng-hide-add-active + * </pre> + * + * Once the animation is complete then both CSS classes will be removed and `ng-hide` will be added to the element. + * The done callback, if provided, will be also fired once the animation is complete. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + * @param {function()=} done callback function that will be called once the animation is complete + */ + hide : function(element, done) { + performAnimation('hide', 'ng-hide-add', element, null, null, function() { + $delegate.hide(element, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#addClass + * @methodOf ngAnimate.$animate + * + * @description + * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. + * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide + * the animate service the setup and active CSS classes in order to trigger the animation. + * + * For example, upon execution of: + * + * <pre> + * $animate.addClass(element, 'super'); + * </pre> + * + * The generated CSS class values present on element will look like: + * <pre> + * .super-add + * .super-add-active + * </pre> + * + * And upon completion, the generated animation CSS classes will be removed from the element, but the className + * value will be attached to the element. In this case, based on the previous example, the resulting CSS class for the element + * will look like so: + * + * <pre> + * .super + * </pre> + * + * Once this is complete, then the done callback, if provided, will be fired. + * + * @param {jQuery/jqLite element} element the element that will be animated + * @param {string} className the CSS class that will be animated and then attached to the element + * @param {function()=} done callback function that will be called once the animation is complete + */ + addClass : function(element, className, done) { + performAnimation('addClass', className, element, null, null, function() { + $delegate.addClass(element, className, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#removeClass + * @methodOf ngAnimate.$animate + * + * @description + * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value + * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in + * order to provide the animate service the setup and active CSS classes in order to trigger the animation. + * + * For example, upon the execution of: + * + * <pre> + * $animate.removeClass(element, 'super'); + * </pre> + * + * The CSS class values present on element during the animation will look like: + * + * <pre> + * .super //this was here from before + * .super-remove + * .super-remove-active + * </pre> + * + * And upon completion, the generated animation CSS classes will be removed from the element as well as the + * className value that was provided (in this case {@type super} will be removed). Once that is complete, then, if provided, + * the done callback will be fired. + * + * @param {jQuery/jqLite element} element the element that will be animated + * @param {string} className the CSS class that will be animated and then removed from the element + * @param {function()=} done callback function that will be called once the animation is complete + */ + removeClass : function(element, className, done) { + performAnimation('removeClass', className, element, null, null, function() { + $delegate.removeClass(element, className, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#enabled + * @methodOf ngAnimate.$animate + * @function + * + * @param {boolean=} If provided then set the animation on or off. + * @return {boolean} Current animation state. + * + * @description + * Globally enables/disables animations. + * + */ + enabled : function(value) { + if (arguments.length) { + rootAnimateState.running = !value; + } + return !rootAnimateState.running; + } + }; + + /* + all animations call this shared animation triggering function internally. + The event variable refers to the JavaScript animation event that will be triggered + and the className value is the name of the animation that will be applied within the + CSS code. Element, parent and after are provided DOM elements for the animation + and the onComplete callback will be fired once the animation is fully complete. + */ + function performAnimation(event, className, element, parent, after, onComplete) { + if(nothingToAnimate(className, element)) { + (onComplete || noop)(); + } else { + var classes = ((element.attr('class') || '') + ' ' + className), + animationLookup = (' ' + classes).replace(/\s+/g,'.'), + animations = []; + forEach(lookup(animationLookup), function(animation, index) { + animations.push({ + start : animation[event], + done : false + }); + }); + + if (!parent) { + parent = after ? after.parent() : element.parent(); + } + var disabledAnimation = { running : true }; + + //skip the animation if animations are disabled, a parent is already being animated + //or the element is not currently attached to the document body. + if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running) { + //avoid calling done() since there is no need to remove any + //data or className values since this happens earlier than that + (onComplete || noop)(); + return; + } + + var animationData = element.data(NG_ANIMATE_STATE) || {}; + + //if an animation is currently running on the element then lets take the steps + //to cancel that animation and fire any required callbacks + if(animationData.running) { + cancelAnimations(animationData.animations); + animationData.done(); + } + + element.data(NG_ANIMATE_STATE, { + running:true, + animations:animations, + done:done + }); + + if(event == 'addClass') { + className = suffixClasses(className, '-add'); + } else if(event == 'removeClass') { + className = suffixClasses(className, '-remove'); + } + + element.addClass(className); + + forEach(animations, function(animation, index) { + var fn = function() { + progress(index); + }; + + if(animation.start) { + if(event == 'addClass' || event == 'removeClass') { + animation.cancel = animation.start(element, className, fn); + } else { + animation.cancel = animation.start(element, fn); + } + } else { + fn(); + } + }); + } + + function nothingToAnimate(className, element) { + return !(className && className.length > 0 && element.length > 0); + } + + function cancelAnimations(animations) { + forEach(animations, function(animation) { + (animation.cancel || noop)(element); + }); + } + + function suffixClasses(classes, suffix) { + var className = ''; + classes = angular.isArray(classes) ? classes : classes.split(/\s+/); + forEach(classes, function(klass, i) { + if(klass && klass.length > 0) { + className += (i > 0 ? ' ' : '') + klass + suffix; + } + }); + return className; + } + + function progress(index) { + animations[index].done = true; + for(var i=0;i<animations.length;i++) { + if(!animations[i].done) return; + } + done(); + }; + + function done() { + if(!done.hasBeenRun) { + done.hasBeenRun = true; + element.removeClass(className); + element.removeData(NG_ANIMATE_STATE); + (onComplete || noop)(); + } + } + } + }]); + }]) + + .animation('', ['$window','$sniffer', function($window, $sniffer) { + return { + enter : function(element, done) { + return animate(element, 'ng-enter', done); + }, + leave : function(element, done) { + return animate(element, 'ng-leave', done); + }, + move : function(element, done) { + return animate(element, 'ng-move', done); + }, + show : function(element, done) { + return animate(element, 'ng-hide-remove', done); + }, + hide : function(element, done) { + return animate(element, 'ng-hide-add', done); + }, + addClass : function(element, className, done) { + return animate(element, className, done); + }, + removeClass : function(element, className, done) { + return animate(element, className, done); + } + }; + + function animate(element, className, done) { + if (!($sniffer.transitions || $sniffer.animations)) { + done(); + } else { + var activeClassName = ''; + $window.setTimeout(startAnimation, 1); + + //this acts as the cancellation function in case + //a new animation is triggered while another animation + //is still going on (otherwise the active className + //would still hang around until the timer is complete). + return onComplete; + } + + function parseMaxTime(str) { + var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; + } + + function startAnimation() { + var duration = 0; + forEach(className.split(' '), function(klass, i) { + activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; + }); + + element.addClass(activeClassName); + + //one day all browsers will have these properties + var w3cAnimationProp = 'animation'; + var w3cTransitionProp = 'transition'; + + //but some still use vendor-prefixed styles + var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; + var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + + var durationKey = 'Duration', + delayKey = 'Delay', + animationIterationCountKey = 'IterationCount'; + + //we want all the styles defined before and after + var ELEMENT_NODE = 1; + 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])); + + var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), + parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); + + if(animationDuration > 0) { + animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, + parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, + 1); + } + + duration = Math.max(animationDelay + animationDuration, + transitionDelay + transitionDuration, + duration); + } + }); + + $window.setTimeout(onComplete, duration * 1000); + } + + function onComplete() { + element.removeClass(activeClassName); + done(); + }; + }; + }]); diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index e05e7a28..bfb601fd 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -627,6 +627,43 @@ angular.mock.$LogProvider = function() { angular.mock.TzDate.prototype = Date.prototype; })(); +angular.mock.animate = angular.module('mock.animate', ['ng']) + + .config(['$provide', function($provide) { + + $provide.decorator('$animate', function($delegate) { + var animate = { + queue : [], + enabled : $delegate.enabled, + process : function(name) { + var tick = animate.queue.shift(); + expect(tick.method).toBe(name); + tick.fn(); + return tick; + } + }; + + forEach(['enter','leave','move','show','hide','addClass','removeClass'], function(method) { + animate[method] = function() { + var params = arguments; + animate.queue.push({ + method : method, + params : params, + element : angular.isElement(params[0]) && params[0], + parent : angular.isElement(params[1]) && params[1], + after : angular.isElement(params[2]) && params[2], + fn : function() { + $delegate[method].apply($delegate, params); + } + }); + }; + }); + + return animate; + }); + + }]); + /** * @ngdoc function * @name angular.mock.createMockWindow diff --git a/src/ngRoute/directive/ngView.js b/src/ngRoute/directive/ngView.js index 935ba05d..3074df49 100644 --- a/src/ngRoute/directive/ngView.js +++ b/src/ngRoute/directive/ngView.js @@ -14,9 +14,6 @@ ngRouteModule.directive('ngView', ngViewFactory); * Every time the current route changes, the included view changes with it according to the * configuration of the `$route` service. * - * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter** - * and **leave** effects. - * * @animations * enter - happens just after the ngView contents are changed (when the new view DOM element is inserted into the DOM) * leave - happens just after the current ngView contents change and just before the former contents are removed from the DOM @@ -35,8 +32,8 @@ ngRouteModule.directive('ngView', ngViewFactory); <div ng-view - class="example-animate-container" - ng-animate="{enter: 'example-enter', leave: 'example-leave'}"></div> + class="example-$animate-container" + ng-$animate="{enter: 'example-enter', leave: 'example-leave'}"></div> <hr /> <pre>$location.path() = {{main.$location.path()}}</pre> @@ -71,12 +68,12 @@ ngRouteModule.directive('ngView', ngViewFactory); transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; } - .example-animate-container { + .example-$animate-container { position:relative; height:100px; } - .example-animate-container > * { + .example-$animate-container > * { display:block; width:100%; border-left:1px solid black; @@ -162,15 +159,14 @@ ngRouteModule.directive('ngView', ngViewFactory); * @description * Emitted every time the ngView content is reloaded. */ -ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animator']; -function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animator) { +ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animate']; +function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animate) { return { restrict: 'ECA', terminal: true, link: function(scope, element, attr) { var lastScope, - onloadExp = attr.onload || '', - animate = $animator(scope, attr); + onloadExp = attr.onload || ''; scope.$on('$routeChangeSuccess', update); update(); @@ -184,7 +180,7 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller, } function clearContent() { - animate.leave(element.contents(), element); + $animate.leave(element.contents()); destroyLastScope(); } @@ -195,7 +191,7 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller, if (template) { clearContent(); var enterElements = jqLite('<div></div>').html(template).contents(); - animate.enter(enterElements, element); + $animate.enter(enterElements, element); var link = $compile(enterElements), current = $route.current, diff --git a/test/matchers.js b/test/matchers.js index 67efd3e7..ef89e3ee 100644 --- a/test/matchers.js +++ b/test/matchers.js @@ -30,11 +30,23 @@ beforeEach(function() { return -1; } + function isNgElementHidden(element) { + return angular.element(element).hasClass('ng-hide'); + }; + this.addMatchers({ toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'), toBeValid: cssMatcher('ng-valid', 'ng-invalid'), toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'), toBePristine: cssMatcher('ng-pristine', 'ng-dirty'), + toBeShown: function() { + this.message = valueFn("Expected element to not have 'ng-hide' class"); + return !isNgElementHidden(this.actual); + }, + toBeHidden: function() { + this.message = valueFn("Expected element to have 'ng-hide' class"); + return isNgElementHidden(this.actual); + }, toEqual: function(expected) { if (this.actual && this.actual.$$log) { diff --git a/test/ng/animateSpec.js b/test/ng/animateSpec.js new file mode 100644 index 00000000..7f440bb5 --- /dev/null +++ b/test/ng/animateSpec.js @@ -0,0 +1,53 @@ +describe("$animate", function() { + + describe("without animation", function() { + beforeEach(inject(function($compile, _$rootElement_, $rootScope) { + element = $compile('<div></div>')($rootScope); + $rootElement = _$rootElement_; + })); + + it("should add element at the start of enter animation", inject(function($animate, $compile, $rootScope) { + var child = $compile('<div></div>')($rootScope); + expect(element.contents().length).toBe(0); + $animate.enter(child, element); + expect(element.contents().length).toBe(1); + })); + + it("should remove the element at the end of leave animation", inject(function($animate, $compile, $rootScope) { + var child = $compile('<div></div>')($rootScope); + element.append(child); + expect(element.contents().length).toBe(1); + $animate.leave(child); + expect(element.contents().length).toBe(0); + })); + + it("should reorder the move animation", inject(function($animate, $compile, $rootScope) { + var child1 = $compile('<div>1</div>')($rootScope); + var child2 = $compile('<div>2</div>')($rootScope); + element.append(child1); + element.append(child2); + expect(element.text()).toBe('12'); + $animate.move(child1, element, child2); + expect(element.text()).toBe('21'); + })); + + it("should animate the show animation event", inject(function($animate) { + element.addClass('ng-hide'); + $animate.show(element); + expect(element).toBeShown(); + })); + + it("should animate the hide animation event", inject(function($animate) { + expect(element).toBeShown(); + $animate.hide(element); + expect(element).toBeHidden(); + })); + + it("should still perform DOM operations even if animations are disabled", inject(function($animate) { + $animate.enabled(false); + expect(element).toBeShown(); + $animate.hide(element); + expect(element).toBeHidden(); + })); + }); +}); diff --git a/test/ng/animationSpec.js b/test/ng/animationSpec.js deleted file mode 100644 index 86592643..00000000 --- a/test/ng/animationSpec.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -describe('$animation', function() { - - it('should allow animation registration', function() { - var noopCustom = function(){}; - module(function($animationProvider) { - $animationProvider.register('noop-custom', valueFn(noopCustom)); - }); - inject(function($animation) { - expect($animation('noop-custom')).toBe(noopCustom); - }); - }); - -}); diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js deleted file mode 100644 index 8fb9f05f..00000000 --- a/test/ng/animatorSpec.js +++ /dev/null @@ -1,773 +0,0 @@ -'use strict'; - -describe("$animator", function() { - - var body, element, $rootElement; - - function html(html) { - body.append($rootElement); - $rootElement.html(html); - element = $rootElement.children().eq(0); - return element; - } - - beforeEach(function() { - // we need to run animation on attached elements; - body = jqLite(document.body); - }); - - afterEach(function(){ - dealoc(body); - }); - - describe("enable / disable", function() { - - beforeEach(function() { - module(function($animationProvider, $provide) { - $provide.value('$window', angular.mock.createMockWindow()); - }); - }); - - it("should disable and enable the animations", function() { - var initialState = null; - var animator; - - angular.bootstrap(body, [function() { - return function($animator) { - animator = $animator; - initialState = $animator.enabled(); - } - }]); - - expect(initialState).toBe(false); - - expect(animator.enabled()).toBe(true); - - expect(animator.enabled(0)).toBe(false); - expect(animator.enabled()).toBe(false); - - expect(animator.enabled(1)).toBe(true); - expect(animator.enabled()).toBe(true); - }); - - }); - - describe("without animation", function() { - var window, animator; - - beforeEach(function() { - module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - }) - inject(function($animator, $compile, $rootScope, _$rootElement_) { - animator = $animator($rootScope, {}); - element = $compile('<div></div>')($rootScope); - $rootElement = _$rootElement_; - }) - }); - - it("should add element at the start of enter animation", inject(function($animator, $compile, $rootScope) { - var child = $compile('<div></div>')($rootScope); - expect(element.contents().length).toBe(0); - animator.enter(child, element); - expect(element.contents().length).toBe(1); - })); - - it("should remove the element at the end of leave animation", inject(function($animator, $compile, $rootScope) { - var child = $compile('<div></div>')($rootScope); - element.append(child); - expect(element.contents().length).toBe(1); - animator.leave(child, element); - expect(element.contents().length).toBe(0); - })); - - it("should reorder the move animation", inject(function($animator, $compile, $rootScope) { - var child1 = $compile('<div>1</div>')($rootScope); - var child2 = $compile('<div>2</div>')($rootScope); - element.append(child1); - element.append(child2); - expect(element.text()).toBe('12'); - animator.move(child1, element, child2); - expect(element.text()).toBe('21'); - })); - - it("should animate the show animation event", inject(function() { - element.css('display','none'); - expect(element.css('display')).toBe('none'); - animator.show(element); - expect(element[0].style.display).toBe(''); - })); - - it("should animate the hide animation event", inject(function() { - element.css('display','block'); - expect(element.css('display')).toBe('block'); - animator.hide(element); - expect(element.css('display')).toBe('none'); - })); - - it("should still perform DOM operations even if animations are disabled", inject(function($animator) { - $animator.enabled(false); - element.css('display','block'); - expect(element.css('display')).toBe('block'); - animator.hide(element); - expect(element.css('display')).toBe('none'); - })); - }); - - describe("with polyfill", function() { - - var child, after, window, animator; - - beforeEach(function() { - module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - $animationProvider.register('custom', function() { - return { - start: function(element, done) { - done(); - } - } - }); - $animationProvider.register('custom-delay', function() { - return { - start: function(element, done) { - window.setTimeout(done, 2000); - }, - cancel : function(element) { - element.addClass('animation-cancelled'); - } - } - }); - $animationProvider.register('setup-memo', function() { - return { - setup: function(element) { - return "memento"; - }, - start: function(element, done, memento) { - element.text(memento); - done(); - } - } - }); - }) - inject(function($animator, $compile, $rootScope, $rootElement) { - element = $compile('<div></div>')($rootScope); - child = $compile('<div></div>')($rootScope); - after = $compile('<div></div>')($rootScope); - $rootElement.append(element); - }); - }) - - it("should animate the enter animation event", inject(function($animator, $rootScope) { - $animator.enabled(true); - animator = $animator($rootScope, { - ngAnimate : '{enter: \'custom\'}' - }); - - expect(element.contents().length).toBe(0); - animator.enter(child, element); - window.setTimeout.expect(1).process(); - })); - - it("should animate the leave animation event", inject(function($animator, $rootScope) { - $animator.enabled(true); - animator = $animator($rootScope, { - ngAnimate : '{leave: \'custom\'}' - }); - - element.append(child); - expect(element.contents().length).toBe(1); - animator.leave(child, element); - window.setTimeout.expect(1).process(); - expect(element.contents().length).toBe(0); - })); - - it("should animate the move animation event", inject(function($animator, $compile, $rootScope) { - $animator.enabled(true); - animator = $animator($rootScope, { - ngAnimate : '{move: \'custom\'}' - }); - $rootScope.$digest(); - var child1 = $compile('<div>1</div>')($rootScope); - var child2 = $compile('<div>2</div>')($rootScope); - element.append(child1); - element.append(child2); - expect(element.text()).toBe('12'); - animator.move(child1, element, child2); - expect(element.text()).toBe('21'); - window.setTimeout.expect(1).process(); - })); - - it("should animate the show animation event", inject(function($animator, $rootScope) { - $animator.enabled(true); - animator = $animator($rootScope, { - ngAnimate : '{show: \'custom\'}' - }); - $rootScope.$digest(); - element.css('display','none'); - expect(element.css('display')).toBe('none'); - animator.show(element); - expect(element[0].style.display).toBe(''); - window.setTimeout.expect(1).process(); - expect(element[0].style.display).toBe(''); - })); - - it("should animate the hide animation event", inject(function($animator, $rootScope) { - $animator.enabled(true); - animator = $animator($rootScope, { - ngAnimate : '{hide: \'custom\'}' - }); - $rootScope.$digest(); - element.css('display','block'); - expect(element.css('display')).toBe('block'); - animator.hide(element); - expect(element.css('display')).toBe('block'); - window.setTimeout.expect(1).process(); - expect(element.css('display')).toBe('none'); - })); - - it("should assign the ngAnimate string to all events if a string is given", - inject(function($animator, $sniffer, $rootScope) { - $animator.enabled(true); - if (!$sniffer.transitions) return; - animator = $animator($rootScope, { - ngAnimate : '"custom"' - }); - - $rootScope.$digest(); - - //enter - animator.enter(child, element); - expect(child.attr('class')).toContain('custom-enter'); - window.setTimeout.expect(1).process(); - expect(child.attr('class')).toContain('custom-enter-active'); - window.setTimeout.expect(0).process(); - - //leave - element.append(after); - animator.move(child, element, after); - expect(child.attr('class')).toContain('custom-move'); - window.setTimeout.expect(1).process(); - expect(child.attr('class')).toContain('custom-move-active'); - window.setTimeout.expect(0).process(); - - //hide - animator.hide(child); - expect(child.attr('class')).toContain('custom-hide'); - window.setTimeout.expect(1).process(); - expect(child.attr('class')).toContain('custom-hide-active'); - window.setTimeout.expect(0).process(); - - //show - animator.show(child); - expect(child.attr('class')).toContain('custom-show'); - window.setTimeout.expect(1).process(); - expect(child.attr('class')).toContain('custom-show-active'); - window.setTimeout.expect(0).process(); - - //leave - animator.leave(child); - expect(child.attr('class')).toContain('custom-leave'); - window.setTimeout.expect(1).process(); - expect(child.attr('class')).toContain('custom-leave-active'); - window.setTimeout.expect(0).process(); - })); - - it("should run polyfillSetup and return the memento", inject(function($animator, $rootScope) { - $animator.enabled(true); - animator = $animator($rootScope, { - ngAnimate : '{show: \'setup-memo\'}' - }); - $rootScope.$digest(); - expect(element.text()).toEqual(''); - animator.show(element); - window.setTimeout.expect(1).process(); - expect(element.text()).toBe('memento'); - })); - - it("should not run if animations are disabled", inject(function($animator, $rootScope) { - $animator.enabled(false); - - animator = $animator($rootScope, { - ngAnimate : '{show: \'setup-memo\'}' - }); - $rootScope.$digest(); - - element.text('123'); - expect(element.text()).toBe('123'); - animator.show(element); - expect(element.text()).toBe('123'); - - $animator.enabled(true); - - animator.show(element); - window.setTimeout.expect(1).process(); - expect(element.text()).toBe('memento'); - })); - - it("should only call done() once and right away if another animation takes place in between", - inject(function($animator, $rootScope) { - $animator.enabled(true); - - animator = $animator($rootScope, { - ngAnimate : '{hide: \'custom-delay\', leave: \'custom-delay\'}' - }); - - element.append(child); - - child.css('display','block'); - animator.hide(child); - window.setTimeout.expect(1).process(); - expect(child.css('display')).toBe('block'); - - animator.leave(child); - expect(child.css('display')).toBe('none'); //hides instantly - - //lets change this to prove that done doesn't fire anymore for the previous hide() operation - child.css('display','block'); - - window.setTimeout.expect(2000).process(); - expect(child.css('display')).toBe('block'); //doesn't run the done() method to hide it - - expect(element.children().length).toBe(1); //still animating - - window.setTimeout.expect(1).process(); - window.setTimeout.expect(2000).process(); - expect(element.children().length).toBe(0); - })); - - it("should call the cancel callback when another animation is called on the same element", - inject(function($animator, $rootScope) { - $animator.enabled(true); - - animator = $animator($rootScope, { - ngAnimate : '{hide: \'custom-delay\', show: \'custom-delay\'}' - }); - - child.css('display','none'); - element.data('foo', 'bar'); - animator.show(element); - window.setTimeout.expect(1).process(); - - animator.hide(element); - - expect(element.hasClass('animation-cancelled')).toBe(true); - expect(element.data('foo')).toEqual('bar'); - })); - - it("should NOT clobber all data on an element when animation is finished", - inject(function($animator, $rootScope) { - $animator.enabled(true); - - animator = $animator($rootScope, { - ngAnimate : '{hide: \'custom-delay\', show: \'custom-delay\'}' - }); - - child.css('display','none'); - element.data('foo', 'bar'); - - animator.show(element); - window.setTimeout.expect(1).process(); - - animator.hide(element); - - expect(element.data('foo')).toEqual('bar'); - })); - - - it("should properly animate custom animation events", inject(function($animator, $rootScope) { - $animator.enabled(true); - animator = $animator($rootScope, { - ngAnimate : '{custom: \'setup-memo\'}' - }); - - element.text('123'); - animator.animate('custom',element); - window.setTimeout.expect(1).process(); - expect(element.text()).toBe('memento'); - })); - }); - - describe("with CSS3", function() { - var window, animator, prefix, vendorPrefix; - - beforeEach(function() { - module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer, _$rootElement_, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - $rootElement = _$rootElement_; - $animator.enabled(true); - }; - }) - }); - - it("should properly animate custom animations for specific animation events", - inject(function($animator, $rootScope, $compile, $sniffer) { - - $animator.enabled(true); - var element = $compile(html('<div></div>'))($rootScope); - - animator = $animator($rootScope, { - ngAnimate : '{custom: \'special\'}' - }); - - animator.animate('custom',element); - if($sniffer.transitions) { - expect(element.hasClass('special')).toBe(true); - window.setTimeout.expect(1).process(); - expect(element.hasClass('special-active')).toBe(true); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - })); - - it("should not animate custom animations if not specifically defined", - inject(function($animator, $rootScope, $compile) { - - $animator.enabled(true); - var element = $compile(html('<div></div>'))($rootScope); - - animator = $animator($rootScope, { - ngAnimate : '{custom: \'special\'}' - }); - - expect(window.setTimeout.queue.length).toBe(0); - animator.animate('custom1',element); - expect(element.hasClass('special')).toBe(false); - expect(window.setTimeout.queue.length).toBe(0); - })); - - it("should properly animate custom animations for general animation events", - inject(function($animator, $rootScope, $compile, $sniffer) { - - $animator.enabled(true); - var element = $compile(html('<div></div>'))($rootScope); - - animator = $animator($rootScope, { - ngAnimate : "'special'" - }); - - animator.animate('custom',element); - if($sniffer.transitions) { - expect(element.hasClass('special-custom')).toBe(true); - window.setTimeout.expect(1).process(); - expect(element.hasClass('special-custom-active')).toBe(true); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - })); - - describe("Animations", function() { - it("should properly detect and make use of CSS Animations", - inject(function($animator, $rootScope, $compile, $sniffer) { - var style = 'animation: some_animation 4s linear 0s 1 alternate;' + - vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate;'; - element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - animator.show(element); - if ($sniffer.animations) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(4000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should properly detect and make use of CSS Animations with multiple iterations", - inject(function($animator, $rootScope, $compile, $sniffer) { - var style = 'animation-duration: 2s;' + - 'animation-iteration-count: 3;' + - vendorPrefix + 'animation-duration: 2s;' + - vendorPrefix + 'animation-iteration-count: 3;'; - element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - animator.show(element); - if ($sniffer.animations) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(6000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should fallback to the animation duration if an infinite iteration is provided", - inject(function($animator, $rootScope, $compile, $sniffer) { - var style = 'animation-duration: 2s;' + - 'animation-iteration-count: infinite;' + - vendorPrefix + 'animation-duration: 2s;' + - vendorPrefix + 'animation-iteration-count: infinite;'; - element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - animator.show(element); - if ($sniffer.animations) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(2000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should consider the animation delay is provided", - inject(function($animator, $rootScope, $compile, $sniffer) { - var style = 'animation-duration: 2s;' + - 'animation-delay: 10s;' + - 'animation-iteration-count: 5;' + - vendorPrefix + 'animation-duration: 2s;' + - vendorPrefix + 'animation-delay: 10s;' + - vendorPrefix + 'animation-iteration-count: 5;'; - element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - animator.show(element); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(20000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should skip animations if disabled and run when enabled", - inject(function($animator, $rootScope, $compile, $sniffer) { - $animator.enabled(false); - var style = 'animation: some_animation 2s linear 0s 1 alternate;' + - vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;' - - element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - element.css('display','none'); - expect(element.css('display')).toBe('none'); - animator.show(element); - expect(element[0].style.display).toBe(''); - })); - - it("should finish the previous animation when a new animation is started", - inject(function($animator, $rootScope, $compile, $sniffer) { - var style = 'animation: some_animation 2s linear 0s 1 alternate;' + - vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;' - - element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'show\', hide: \'hide\'}' - }); - - animator.show(element); - if($sniffer.animations) { - window.setTimeout.expect(1).process(); - expect(element.hasClass('show')).toBe(true); - expect(element.hasClass('show-active')).toBe(true); - } - else { //animation is skipped - expect(window.setTimeout.queue.length).toBe(0); - } - - animator.hide(element); - if(!$sniffer.animations) { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element.hasClass('show')).toBe(false); - expect(element.hasClass('show-active')).toBe(false); - })); - }); - - describe("Transitions", function() { - it("should skip transitions if disabled and run when enabled", - inject(function($animator, $rootScope, $compile, $sniffer) { - $animator.enabled(false); - element = $compile(html('<div style="' + vendorPrefix + 'transition: 1s linear all">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - animator.show(element); - expect(element[0].style.display).toBe(''); - - $animator.enabled(true); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - animator.show(element); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(1000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should skip animations if disabled and run when enabled picking the longest specified duration", - inject(function($animator, $rootScope, $compile, $sniffer) { - $animator.enabled(true); - element = $compile(html('<div style="' + vendorPrefix + 'transition-duration: 1s, 2000ms, 1s; ' + vendorPrefix + 'transition-property: height, left, opacity">foo</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - element.css('display','none'); - animator.show(element); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(2000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should skip animations if disabled and run when enabled picking the longest specified duration/delay combination", - inject(function($animator, $rootScope, $compile, $sniffer) { - $animator.enabled(false); - element = $compile(html('<div style="' + vendorPrefix + - 'transition-duration: 1s, 0s, 1s; ' + vendorPrefix + - 'transition-delay: 2s, 1000ms, 2s; ' + vendorPrefix + - 'transition-property: height, left, opacity">foo</div>'))($rootScope); - - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - animator.show(element); - expect(element[0].style.display).toBe(''); - - $animator.enabled(true); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - animator.show(element); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(3000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should select the highest duration and delay", - inject(function($animator, $rootScope, $compile, $sniffer) { - var styles = 'transition:1s linear all 2s;' + - vendorPrefix + 'transition:1s linear all 2s;' + - 'animation:my_ani 10s 1s;' + - vendorPrefix + 'animation:my_ani 10s 1s;'; - - element = $compile(html('<div style="' + styles + '">foo</div>'))($rootScope); - - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); - - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - animator.show(element); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(11000).process(); - } - else { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element[0].style.display).toBe(''); - })); - - it("should finish the previous transition when a new animation is started", - inject(function($animator, $rootScope, $compile, $sniffer) { - var style = 'transition: 1s linear all;' + - vendorPrefix + 'transition: 1s linear all;' - - element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'show\', hide: \'hide\'}' - }); - - animator.show(element); - if($sniffer.transitions) { - window.setTimeout.expect(1).process(); - expect(element.hasClass('show')).toBe(true); - expect(element.hasClass('show-active')).toBe(true); - } - else { //animation is skipped - expect(window.setTimeout.queue.length).toBe(0); - } - - animator.hide(element); - if(!$sniffer.transitions) { - expect(window.setTimeout.queue.length).toBe(0); - } - expect(element.hasClass('show')).toBe(false); - expect(element.hasClass('show-active')).toBe(false); - })); - }); - }); - - describe('anmation evaluation', function () { - it('should re-evaluate the animation expression on each animation', inject(function($animator, $rootScope) { - var parent = jqLite('<div><span></span></div>'); - var element = parent.find('span'); - - $rootScope.animationFn = function () { throw new Error('too early'); }; - var animate = $animator($rootScope, { ngAnimate: 'animationFn()' }); - var log = ''; - - $rootScope.animationFn = function () { log = 'abc' }; - animate.enter(element, parent); - expect(log).toEqual('abc'); - - $rootScope.animationFn = function () { log = 'xyz' }; - animate.enter(element, parent); - expect(log).toEqual('xyz'); - })); - }); - - it("should throw an error when an invalid ng-animate syntax is provided", inject(function($animator, $rootScope) { - expect(function() { - var animate = $animator($rootScope, { ngAnimate: ':' }); - animate.enter(); - }).toThrow("[$parse:syntax] Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:]."); - })); -}); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 1f5aae95..74090932 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3096,8 +3096,8 @@ describe('$compile', function() { '</div>')($rootScope); $rootScope.$digest(); var spans = element.find('span'); - expect(spans.eq(0).css('display')).toBe('none'); - expect(spans.eq(1).css('display')).toBe('none'); + expect(spans.eq(0)).toBeHidden(); + expect(spans.eq(1)).toBeHidden(); })); @@ -3216,10 +3216,10 @@ describe('$compile', function() { '</div>')($rootScope); $rootScope.$digest(); var spans = element.find('span'); - expect(spans.eq(0).css('display')).toBe('none'); - expect(spans.eq(1).css('display')).toBe('none'); - expect(spans.eq(2).css('display')).toBe('none'); - expect(spans.eq(3).css('display')).toBe('none'); + expect(spans.eq(0)).toBeHidden(); + expect(spans.eq(1)).toBeHidden(); + expect(spans.eq(2)).toBeHidden(); + expect(spans.eq(3)).toBeHidden(); })); }); }); diff --git a/test/ng/directive/ngIfSpec.js b/test/ng/directive/ngIfSpec.js index 0cca57d5..8f2cb793 100755 --- a/test/ng/directive/ngIfSpec.js +++ b/test/ng/directive/ngIfSpec.js @@ -75,8 +75,7 @@ describe('ngIf', function () { }); -describe('ngIf ngAnimate', function () { - var vendorPrefix, window; +describe('ngIf animations', function () { var body, element, $rootElement; function html(html) { @@ -85,6 +84,8 @@ describe('ngIf ngAnimate', function () { return element; } + beforeEach(module('mock.animate')); + beforeEach(module(function() { // we need to run animation on attached elements; return function(_$rootElement_) { @@ -99,97 +100,52 @@ describe('ngIf ngAnimate', function () { dealoc(element); }); - beforeEach(module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; - $animator.enabled(true); + beforeEach(module(function($animateProvider, $provide) { + return function($animate) { + $animate.enabled(true); }; })); - it('should fire off the enter animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer) { + it('should fire off the enter animation', + inject(function($compile, $rootScope, $animate) { + var item; var $scope = $rootScope.$new(); - var style = vendorPrefix + 'transition: 1s linear all'; element = $compile(html( '<div>' + - '<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' + + '<div ng-if="value"><div>Hi</div></div>' + '</div>' ))($scope); $rootScope.$digest(); $scope.$apply('value = true'); + item = $animate.process('enter').element; + expect(item.text()).toBe('Hi'); expect(element.children().length).toBe(1); - var first = element.children()[0]; - - if ($sniffer.transitions) { - expect(first.className).toContain('custom-enter'); - window.setTimeout.expect(1).process(); - expect(first.className).toContain('custom-enter-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(first.className).not.toContain('custom-enter'); - expect(first.className).not.toContain('custom-enter-active'); })); - it('should fire off the leave animation + add and remove the css classes', - inject(function ($compile, $rootScope, $sniffer) { + it('should fire off the leave animation', + inject(function ($compile, $rootScope, $animate) { + var item; var $scope = $rootScope.$new(); - var style = vendorPrefix + 'transition: 1s linear all'; element = $compile(html( '<div>' + - '<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' + + '<div ng-if="value"><div>Hi</div></div>' + '</div>' ))($scope); $scope.$apply('value = true'); - expect(element.children().length).toBe(1); - var first = element.children()[0]; - - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('enter').element; + expect(item.text()).toBe('Hi'); $scope.$apply('value = false'); - expect(element.children().length).toBe($sniffer.transitions ? 1 : 0); + expect(element.children().length).toBe(1); - if ($sniffer.transitions) { - expect(first.className).toContain('custom-leave'); - window.setTimeout.expect(1).process(); - expect(first.className).toContain('custom-leave-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('leave').element; + expect(item.text()).toBe('Hi'); expect(element.children().length).toBe(0); })); - it('should catch and use the correct duration for animation', - inject(function ($compile, $rootScope, $sniffer) { - var $scope = $rootScope.$new(); - var style = vendorPrefix + 'transition: 0.5s linear all'; - element = $compile(html( - '<div>' + - '<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' + - '</div>' - ))($scope); - $scope.$apply('value = true'); - - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(500).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - })); - }); diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js index 6cb78755..286ee5af 100644 --- a/test/ng/directive/ngIncludeSpec.js +++ b/test/ng/directive/ngIncludeSpec.js @@ -341,8 +341,7 @@ describe('ngInclude', function() { }); }); -describe('ngInclude ngAnimate', function() { - var vendorPrefix, window; +describe('ngInclude animations', function() { var body, element, $rootElement; function html(html) { @@ -351,11 +350,6 @@ describe('ngInclude ngAnimate', function() { return element; } - function applyCSS(element, cssProp, cssValue) { - element.css(cssProp, cssValue); - element.css(vendorPrefix + cssProp, cssValue); - } - beforeEach(module(function() { // we need to run animation on attached elements; return function(_$rootElement_) { @@ -370,107 +364,51 @@ describe('ngInclude ngAnimate', function() { dealoc(element); }); - beforeEach(module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; - $animator.enabled(true); - }; - })); + beforeEach(module('mock.animate')); afterEach(function(){ dealoc(element); }); - it('should fire off the enter animation + add and remove the css classes', - inject(function($compile, $rootScope, $templateCache, $sniffer) { + it('should fire off the enter animation', + inject(function($compile, $rootScope, $templateCache, $animate) { + var item; $templateCache.put('enter', [200, '<div>data</div>', {}]); $rootScope.tpl = 'enter'; element = $compile(html( '<div ' + - 'ng-include="tpl" ' + - 'ng-animate="{enter: \'custom-enter\'}">' + + 'ng-include="tpl">' + '</div>' ))($rootScope); $rootScope.$digest(); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '1s linear all'); - - if ($sniffer.transitions) { - expect(child.attr('class')).toContain('custom-enter'); - window.setTimeout.expect(1).process(); - - expect(child.attr('class')).toContain('custom-enter-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(child.attr('class')).not.toContain('custom-enter'); - expect(child.attr('class')).not.toContain('custom-enter-active'); + item = $animate.process('leave').element; + item = $animate.process('enter').element; + expect(item.text()).toBe('data'); })); - it('should fire off the leave animation + add and remove the css classes', - inject(function($compile, $rootScope, $templateCache, $sniffer) { + it('should fire off the leave animation', + inject(function($compile, $rootScope, $templateCache, $animate) { + var item; $templateCache.put('enter', [200, '<div>data</div>', {}]); $rootScope.tpl = 'enter'; element = $compile(html( '<div ' + - 'ng-include="tpl" ' + - 'ng-animate="{leave: \'custom-leave\'}">' + + 'ng-include="tpl">' + '</div>' ))($rootScope); $rootScope.$digest(); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '1s linear all'); + item = $animate.process('leave').element; + item = $animate.process('enter').element; + expect(item.text()).toBe('data'); $rootScope.tpl = ''; $rootScope.$digest(); - if ($sniffer.transitions) { - expect(child.attr('class')).toContain('custom-leave'); - window.setTimeout.expect(1).process(); - - expect(child.attr('class')).toContain('custom-leave-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(child.attr('class')).not.toContain('custom-leave'); - expect(child.attr('class')).not.toContain('custom-leave-active'); - })); - - it('should catch and use the correct duration for animation', - inject(function($compile, $rootScope, $templateCache, $sniffer) { - $templateCache.put('enter', [200, '<div>data</div>', {}]); - $rootScope.tpl = 'enter'; - element = $compile(html( - '<div ' + - 'ng-include="tpl" ' + - 'ng-animate="{enter: \'custom-enter\'}">' + - '</div>' - ))($rootScope); - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '0.5s linear all'); - - $rootScope.tpl = 'enter'; - $rootScope.$digest(); - - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(500).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('leave').element; + expect(item.text()).toBe('data'); })); }); diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 26562f4e..a85fd5ab 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -822,10 +822,30 @@ describe('ngRepeat', function() { expect(newLis[2]).toEqual(lis[1]); }); }); + + it('should grow multi-node repeater', inject(function($compile, $rootScope) { + $rootScope.show = false; + $rootScope.books = [ + {title:'T1', description: 'D1'}, + {title:'T2', description: 'D2'} + ]; + element = $compile( + '<div>' + + '<dt ng-repeat-start="book in books">{{book.title}}:</dt>' + + '<dd ng-repeat-end>{{book.description}};</dd>' + + '</div>')($rootScope); + + $rootScope.$digest(); + expect(element.text()).toEqual('T1:D1;T2:D2;'); + $rootScope.books.push({title:'T3', description: 'D3'}); + $rootScope.$digest(); + expect(element.text()).toEqual('T1:D1;T2:D2;T3:D3;'); + })); + + }); -describe('ngRepeat ngAnimate', function() { - var vendorPrefix, window; +describe('ngRepeat animations', function() { var body, element, $rootElement; function html(html) { @@ -834,10 +854,7 @@ describe('ngRepeat ngAnimate', function() { return element; } - function applyCSS(element, cssProp, cssValue) { - element.css(cssProp, cssValue); - element.css(vendorPrefix + cssProp, cssValue); - } + beforeEach(module('mock.animate')); beforeEach(module(function() { // we need to run animation on attached elements; @@ -853,21 +870,14 @@ describe('ngRepeat ngAnimate', function() { dealoc(element); }); - beforeEach(module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; - $animator.enabled(true); - }; - })); + it('should fire off the enter animation', + inject(function($compile, $rootScope, $animate) { - it('should fire off the enter animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer) { + var item; element = $compile(html( '<div><div ' + - 'ng-repeat="item in items" ' + - 'ng-animate="{enter: \'custom-enter\'}">' + + 'ng-repeat="item in items">' + '{{ item }}' + '</div></div>' ))($rootScope); @@ -877,40 +887,24 @@ describe('ngRepeat ngAnimate', function() { $rootScope.items = ['1','2','3']; $rootScope.$digest(); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var kids = element.children(); - for(var i=0;i<kids.length;i++) { - kids[i] = jqLite(kids[i]); - applyCSS(kids[i], 'transition', '1s linear all'); - } - - if ($sniffer.transitions) { - angular.forEach(kids, function(kid) { - expect(kid.attr('class')).toContain('custom-enter'); - window.setTimeout.expect(1).process(); - }); + item = $animate.process('enter').element; + expect(item.text()).toBe('1'); - angular.forEach(kids, function(kid) { - expect(kid.attr('class')).toContain('custom-enter-active'); - window.setTimeout.expect(1000).process(); - }); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('enter').element; + expect(item.text()).toBe('2'); - angular.forEach(kids, function(kid) { - expect(kid.attr('class')).not.toContain('custom-enter'); - expect(kid.attr('class')).not.toContain('custom-enter-active'); - }); + item = $animate.process('enter').element; + expect(item.text()).toBe('3'); })); - it('should fire off the leave animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer) { + it('should fire off the leave animation', + inject(function($compile, $rootScope, $animate) { + + var item; element = $compile(html( '<div><div ' + - 'ng-repeat="item in items" ' + - 'ng-animate="{leave: \'custom-leave\'}">' + + 'ng-repeat="item in items">' + '{{ item }}' + '</div></div>' ))($rootScope); @@ -918,36 +912,30 @@ describe('ngRepeat ngAnimate', function() { $rootScope.items = ['1','2','3']; $rootScope.$digest(); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var kids = element.children(); - for(var i=0;i<kids.length;i++) { - kids[i] = jqLite(kids[i]); - applyCSS(kids[i], 'transition', '1s linear all'); - } + item = $animate.process('enter').element; + expect(item.text()).toBe('1'); + + item = $animate.process('enter').element; + expect(item.text()).toBe('2'); + + item = $animate.process('enter').element; + expect(item.text()).toBe('3'); $rootScope.items = ['1','3']; $rootScope.$digest(); - //the last element gets pushed down when it animates - var kid = jqLite(element.children()[1]); - if ($sniffer.transitions) { - expect(kid.attr('class')).toContain('custom-leave'); - window.setTimeout.expect(1).process(); - expect(kid.attr('class')).toContain('custom-leave-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(kid.attr('class')).not.toContain('custom-leave'); - expect(kid.attr('class')).not.toContain('custom-leave-active'); + item = $animate.process('leave').element; + expect(item.text()).toBe('2'); })); - it('should fire off the move animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer) { + it('should fire off the move animation', + inject(function($compile, $rootScope, $animate) { + + var item; + element = $compile(html( '<div>' + - '<div ng-repeat="item in items" ng-animate="{move: \'custom-move\'}">' + + '<div ng-repeat="item in items">' + '{{ item }}' + '</div>' + '</div>' @@ -956,97 +944,23 @@ describe('ngRepeat ngAnimate', function() { $rootScope.items = ['1','2','3']; $rootScope.$digest(); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var kids = element.children(); - for(var i=0;i<kids.length;i++) { - kids[i] = jqLite(kids[i]); - applyCSS(kids[i], 'transition', '1s linear all'); - } + item = $animate.process('enter').element; + expect(item.text()).toBe('1'); - $rootScope.items = ['2','3','1']; - $rootScope.$digest(); - - //the last element gets pushed down when it animates - kids = element.children(); - var first = jqLite(kids[0]); - var left = jqLite(kids[1]); - var right = jqLite(kids[2]); - - if ($sniffer.transitions) { - expect(first.attr('class')).toContain('custom-move'); - window.setTimeout.expect(1).process(); - expect(left.attr('class')).toContain('custom-move'); - window.setTimeout.expect(1).process(); - - expect(first.attr('class')).toContain('custom-move-active'); - window.setTimeout.expect(1000).process(); - expect(left.attr('class')).toContain('custom-move-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(first.attr('class')).not.toContain('custom-move'); - expect(first.attr('class')).not.toContain('custom-move-active'); - expect(left.attr('class')).not.toContain('custom-move'); - expect(left.attr('class')).not.toContain('custom-move-active'); - expect(right.attr('class')).not.toContain('custom-move'); - expect(right.attr('class')).not.toContain('custom-move-active'); - })); + item = $animate.process('enter').element; + expect(item.text()).toBe('2'); - it('should catch and use the correct duration for animation', - inject(function($compile, $rootScope, $sniffer) { + item = $animate.process('enter').element; + expect(item.text()).toBe('3'); - element = $compile(html( - '<div><div ' + - 'ng-repeat="item in items" ' + - 'ng-animate="{enter: \'custom-enter\'}">' + - '{{ item }}' + - '</div></div>' - ))($rootScope); - - $rootScope.$digest(); // re-enable the animations; - - $rootScope.items = ['a','b']; + $rootScope.items = ['2','3','1']; $rootScope.$digest(); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var kids = element.children(); - var first = jqLite(kids[0]); - var second = jqLite(kids[1]); - var cssProp = 'transition'; - var cssValue = '0.5s linear all'; - applyCSS(first, cssProp, cssValue); - applyCSS(second, cssProp, cssValue); - - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(1).process(); - window.setTimeout.expect(500).process(); - window.setTimeout.expect(500).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - })); + item = $animate.process('move').element; + expect(item.text()).toBe('2'); - it('should grow multi-node repeater', inject(function($compile, $rootScope) { - $rootScope.show = false; - $rootScope.books = [ - {title:'T1', description: 'D1'}, - {title:'T2', description: 'D2'} - ]; - element = $compile( - '<div>' + - '<dt ng-repeat-start="book in books">{{book.title}}:</dt>' + - '<dd ng-repeat-end>{{book.description}};</dd>' + - '</div>')($rootScope); - - $rootScope.$digest(); - expect(element.text()).toEqual('T1:D1;T2:D2;'); - $rootScope.books.push({title:'T3', description: 'D3'}); - $rootScope.$digest(); - expect(element.text()).toEqual('T1:D1;T2:D2;T3:D3;'); + item = $animate.process('move').element; + expect(item.text()).toBe('1'); })); - }); diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js index 9b95440d..f8193a12 100644 --- a/test/ng/directive/ngShowHideSpec.js +++ b/test/ng/directive/ngShowHideSpec.js @@ -13,20 +13,20 @@ describe('ngShow / ngHide', function() { element = jqLite('<div ng-show="exp"></div>'); element = $compile(element)($rootScope); $rootScope.$digest(); - expect(isCssVisible(element)).toEqual(false); + expect(element).toBeHidden(); $rootScope.exp = true; $rootScope.$digest(); - expect(isCssVisible(element)).toEqual(true); + expect(element).toBeShown(); })); it('should make hidden element visible', inject(function($rootScope, $compile) { - element = jqLite('<div style="display: none" ng-show="exp"></div>'); + element = jqLite('<div class="ng-hide" ng-show="exp"></div>'); element = $compile(element)($rootScope); - expect(isCssVisible(element)).toBe(false); + expect(element).toBeHidden(); $rootScope.exp = true; $rootScope.$digest(); - expect(isCssVisible(element)).toBe(true); + expect(element).toBeShown(); })); }); @@ -34,17 +34,15 @@ describe('ngShow / ngHide', function() { it('should hide an element', inject(function($rootScope, $compile) { element = jqLite('<div ng-hide="exp"></div>'); element = $compile(element)($rootScope); - expect(isCssVisible(element)).toBe(true); + expect(element).toBeShown(); $rootScope.exp = true; $rootScope.$digest(); - expect(isCssVisible(element)).toBe(false); + expect(element).toBeHidden(); })); }); }); -describe('ngShow / ngHide - ngAnimate', function() { - var window; - var vendorPrefix; +describe('ngShow / ngHide animations', function() { var body, element, $rootElement; function html(html) { @@ -65,152 +63,57 @@ describe('ngShow / ngHide - ngAnimate', function() { body.removeAttr('ng-animation-running'); }); - beforeEach(module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer, _$rootElement_, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + beforeEach(module('mock.animate')); + + beforeEach(module(function($animateProvider, $provide) { + return function(_$rootElement_) { $rootElement = _$rootElement_; - $animator.enabled(true); }; })); describe('ngShow', function() { - it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) { + it('should fire off the $animate.show and $animate.hide animation', inject(function($compile, $rootScope, $animate) { + var item; var $scope = $rootScope.$new(); $scope.on = true; element = $compile(html( - '<div ' + - 'style="'+vendorPrefix+'transition: 1s linear all"' + - 'ng-show="on" ' + - 'ng-animate="{show: \'custom-show\', hide: \'custom-hide\', animateFirst: true}">' + - '</div>' + '<div ng-show="on">data</div>' ))($scope); $scope.$digest(); - if ($sniffer.transitions) { - expect(element.attr('class')).toContain('custom-show'); - window.setTimeout.expect(1).process(); - - expect(element.attr('class')).toContain('custom-show-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(element.attr('class')).not.toContain('custom-show-active'); - expect(element.attr('class')).not.toContain('custom-show'); + item = $animate.process('show').element; + expect(item.text()).toBe('data'); + expect(item).toBeShown(); $scope.on = false; $scope.$digest(); - if ($sniffer.transitions) { - expect(element.attr('class')).toContain('custom-hide'); - window.setTimeout.expect(1).process(); - expect(element.attr('class')).toContain('custom-hide-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(element.attr('class')).not.toContain('custom-hide-active'); - expect(element.attr('class')).not.toContain('custom-hide'); - })); - it('should skip animation if parent animation running', function() { - var fired = false; - inject(function($animator, $compile, $rootScope, $sniffer) { - $animator.enabled(true); - $rootScope.$digest(); - $rootScope.val = true; - var element = $compile(html('<div ng-show="val" ng-animate="\'animation\'">123</div>'))($rootScope); - $rootElement.controller('ngAnimate').running = true; - element.css('display','none'); - expect(element.css('display')).toBe('none'); - - $rootScope.$digest(); - expect(element[0].style.display).toBe(''); - expect(fired).toBe(false); - - $rootElement.controller('ngAnimate').running = false; - $rootScope.val = false; - $rootScope.$digest(); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(0).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - expect(element[0].style.display).toBe('none'); - }); - }); + item = $animate.process('hide').element; + expect(item.text()).toBe('data'); + expect(item).toBeHidden(); + })); }); describe('ngHide', function() { - it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) { + it('should fire off the $animate.show and $animate.hide animation', inject(function($compile, $rootScope, $animate) { + var item; var $scope = $rootScope.$new(); $scope.off = true; element = $compile(html( - '<div ' + - 'style="'+vendorPrefix+'transition: 1s linear all"' + - 'ng-hide="off" ' + - 'ng-animate="{show: \'custom-show\', hide: \'custom-hide\', animateFirst: true}">' + - '</div>' + '<div ng-hide="off">datum</div>' ))($scope); $scope.$digest(); - if ($sniffer.transitions) { - expect(element.attr('class')).toContain('custom-hide'); - window.setTimeout.expect(1).process(); - - expect(element.attr('class')).toContain('custom-hide-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(element.attr('class')).not.toContain('custom-hide-active'); - expect(element.attr('class')).not.toContain('custom-hide'); + item = $animate.process('hide').element; + expect(item.text()).toBe('datum'); + expect(item).toBeHidden(); $scope.off = false; $scope.$digest(); - if ($sniffer.transitions) { - expect(element.attr('class')).toContain('custom-show'); - window.setTimeout.expect(1).process(); - expect(element.attr('class')).toContain('custom-show-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(element.attr('class')).not.toContain('custom-show-active'); - expect(element.attr('class')).not.toContain('custom-show'); + item = $animate.process('show').element; + expect(item.text()).toBe('datum'); + expect(item).toBeShown(); })); - - it('should disable animation when parent animation is running', function() { - var fired = false; - module(function($animationProvider) { - $animationProvider.register('destructive-animation', function() { - return { - setup : function() {}, - start : function(element, done) { - fired = true; - } - }; - }); - }); - inject(function($compile, $rootScope) { - $rootScope.val = false; - var element = $compile(html('<div ng-hide="val" ng-animate="{ hide:\'destructive-animation\' }">123</div>'))($rootScope); - $rootElement.controller('ngAnimate').running = true; - element.css('display','block'); - expect(element.css('display')).toBe('block'); - - $rootScope.val = true; - $rootScope.$digest(); - - expect(element.css('display')).toBe('none'); - expect(fired).toBe(false); - }); - }); }); }); diff --git a/test/ng/directive/ngSwitchSpec.js b/test/ng/directive/ngSwitchSpec.js index ab231ec2..8750b187 100644 --- a/test/ng/directive/ngSwitchSpec.js +++ b/test/ng/directive/ngSwitchSpec.js @@ -214,8 +214,7 @@ describe('ngSwitch', function() { })); }); -describe('ngSwitch ngAnimate', function() { - var vendorPrefix, window; +describe('ngSwitch animations', function() { var body, element, $rootElement; function html(html) { @@ -224,6 +223,8 @@ describe('ngSwitch ngAnimate', function() { return element; } + beforeEach(module('mock.animate')); + beforeEach(module(function() { // we need to run animation on attached elements; return function(_$rootElement_) { @@ -238,23 +239,15 @@ describe('ngSwitch ngAnimate', function() { dealoc(element); }); - beforeEach(module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; - $animator.enabled(true); - }; - })); - - it('should fire off the enter animation + set and remove the classes', - inject(function($compile, $rootScope, $sniffer) { + it('should fire off the enter animation', + inject(function($compile, $rootScope, $animate) { + var item; var $scope = $rootScope.$new(); - var style = vendorPrefix + 'transition: 1s linear all'; element = $compile(html( - '<div ng-switch on="val" ng-animate="{enter: \'cool-enter\', leave: \'cool-leave\'}">' + - '<div ng-switch-when="one" style="' + style + '">one</div>' + - '<div ng-switch-when="two" style="' + style + '">two</div>' + - '<div ng-switch-when="three" style="' + style + '">three</div>' + + '<div ng-switch on="val">' + + '<div ng-switch-when="one">one</div>' + + '<div ng-switch-when="two">two</div>' + + '<div ng-switch-when="three">three</div>' + '</div>' ))($scope); @@ -262,33 +255,20 @@ describe('ngSwitch ngAnimate', function() { $scope.val = 'one'; $scope.$digest(); - expect(element.children().length).toBe(1); - var first = element.children()[0]; - - if ($sniffer.transitions) { - expect(first.className).toContain('cool-enter'); - window.setTimeout.expect(1).process(); - - expect(first.className).toContain('cool-enter-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(first.className).not.toContain('cool-enter'); - expect(first.className).not.toContain('cool-enter-active'); + item = $animate.process('enter').element; + expect(item.text()).toBe('one'); })); - it('should fire off the leave animation + set and remove the classes', - inject(function($compile, $rootScope, $sniffer) { + it('should fire off the leave animation', + inject(function($compile, $rootScope, $animate) { + var item; var $scope = $rootScope.$new(); - var style = vendorPrefix + 'transition: 1s linear all'; element = $compile(html( - '<div ng-switch on="val" ng-animate="{enter: \'cool-enter\', leave: \'cool-leave\'}">' + - '<div ng-switch-when="one" style="' + style + '">one</div>' + - '<div ng-switch-when="two" style="' + style + '">two</div>' + - '<div ng-switch-when="three" style="' + style + '">three</div>' + + '<div ng-switch on="val">' + + '<div ng-switch-when="one">one</div>' + + '<div ng-switch-when="two">two</div>' + + '<div ng-switch-when="three">three</div>' + '</div>' ))($scope); @@ -296,59 +276,17 @@ describe('ngSwitch ngAnimate', function() { $scope.val = 'two'; $scope.$digest(); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('enter').element; + expect(item.text()).toBe('two'); $scope.val = 'three'; $scope.$digest(); - expect(element.children().length).toBe($sniffer.transitions ? 2 : 1); - var first = element.children()[0]; - - - if ($sniffer.transitions) { - expect(first.className).toContain('cool-leave'); - window.setTimeout.expect(1).process(); - window.setTimeout.expect(1).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('leave').element; + expect(item.text()).toBe('two'); - - if ($sniffer.transitions) { - expect(first.className).toContain('cool-leave-active'); - window.setTimeout.expect(1000).process(); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(first.className).not.toContain('cool-leave'); - expect(first.className).not.toContain('cool-leave-active'); - })); - - it('should catch and use the correct duration for animation', - inject(function($compile, $rootScope, $sniffer) { - element = $compile(html( - '<div ng-switch on="val" ng-animate="{enter: \'cool-enter\', leave: \'cool-leave\'}">' + - '<div ng-switch-when="one" style="' + vendorPrefix + 'transition: 0.5s linear all">one</div>' + - '</div>' - ))($rootScope); - - $rootScope.$digest(); // re-enable the animations; - $rootScope.val = 'one'; - $rootScope.$digest(); - - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(500).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('enter').element; + expect(item.text()).toBe('three'); })); }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js new file mode 100644 index 00000000..52bf5e0b --- /dev/null +++ b/test/ngAnimate/animateSpec.js @@ -0,0 +1,1524 @@ +'use strict'; + +describe("ngAnimate", function() { + + beforeEach(module('ngAnimate')); + + describe("$animate", function() { + + var body, element, $rootElement; + + function html(html) { + body.append($rootElement); + $rootElement.html(html); + element = $rootElement.children().eq(0); + return element; + } + + beforeEach(module(function() { + // we need to run animation on attached elements; + body = jqLite(document.body); + return function($animate) { + $animate.enabled(true); + }; + })); + + afterEach(function(){ + dealoc(body); + }); + + describe("enable / disable", function() { + + beforeEach(function() { + module(function($animateProvider, $provide) { + $provide.value('$window', angular.mock.createMockWindow()); + }); + }); + + it("should disable and enable the animations", function() { + var $animate, initialState = null; + + angular.bootstrap(body, ['ngAnimate',function() { + return function(_$animate_) { + $animate = _$animate_; + initialState = $animate.enabled(); + } + }]); + + expect(initialState).toBe(false); + + expect($animate.enabled()).toBe(true); + + expect($animate.enabled(0)).toBe(false); + expect($animate.enabled()).toBe(false); + + expect($animate.enabled(1)).toBe(true); + expect($animate.enabled()).toBe(true); + }); + + }); + + describe("with polyfill", function() { + + var child, after, window; + + beforeEach(function() { + module(function($animateProvider, $provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + $animateProvider.register('.custom', function() { + return { + start: function(element, done) { + done(); + } + } + }); + $animateProvider.register('.custom-delay', function() { + function animate(element, done) { + done = arguments.length == 3 ? arguments[2] : done; + window.setTimeout(done, 2000); + return function() { + element.addClass('animation-cancelled'); + } + } + return { + show : animate, + hide : animate, + leave : animate, + addClass : animate, + removeClass : animate + } + }); + $animateProvider.register('.custom-long-delay', function() { + function animate(element, done) { + done = arguments.length == 3 ? arguments[2] : done; + window.setTimeout(done, 20000); + return function() { + element.addClass('animation-cancelled'); + } + } + return { + show : animate, + hide : animate, + leave : animate, + addClass : animate, + removeClass : animate + } + }); + $animateProvider.register('.setup-memo', function() { + return { + show: function(element, done) { + element.text('memento'); + done(); + } + } + }); + return function($animate, $compile, $rootScope, $rootElement) { + element = $compile('<div></div>')($rootScope); + child = $compile('<div></div>')($rootScope); + after = $compile('<div></div>')($rootScope); + $rootElement.append(element); + }; + }); + }) + + it("should animate the enter animation event", inject(function($animate, $rootScope, $sniffer) { + expect(element.contents().length).toBe(0); + $animate.enter(child, element); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + expect(element.contents().length).toBe(1); + })); + + it("should animate the leave animation event", inject(function($animate, $rootScope, $sniffer) { + element.append(child); + expect(element.contents().length).toBe(1); + $animate.leave(child); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + expect(element.contents().length).toBe(0); + })); + + it("should animate the move animation event", inject(function($animate, $compile, $rootScope, $sniffer) { + $rootScope.$digest(); + var child1 = $compile('<div>1</div>')($rootScope); + var child2 = $compile('<div>2</div>')($rootScope); + element.append(child1); + element.append(child2); + expect(element.text()).toBe('12'); + $animate.move(child1, element, child2); + expect(element.text()).toBe('21'); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + })); + + it("should animate the show animation event", inject(function($animate, $rootScope, $sniffer) { + $rootScope.$digest(); + element.addClass('ng-hide'); + expect(element).toBeHidden(); + $animate.show(element); + if($sniffer.transitions) { + expect(element.hasClass('ng-hide-remove')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('ng-hide-remove-active')).toBe(true); + window.setTimeout.expect(0).process(); + } + expect(element).toBeShown(); + })); + + it("should animate the hide animation event", inject(function($animate, $rootScope, $sniffer) { + $rootScope.$digest(); + expect(element).toBeShown(); + $animate.hide(element); + if($sniffer.transitions) { + expect(element.hasClass('ng-hide-add')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('ng-hide-add-active')).toBe(true); + window.setTimeout.expect(0).process(); + } + expect(element).toBeHidden(); + })); + + it("should assign the ngAnimate string to all events if a string is given", + inject(function($animate, $sniffer, $rootScope) { + + if (!$sniffer.transitions) return; + + $rootScope.$digest(); + + //enter + $animate.enter(child, element); + expect(child.attr('class')).toContain('ng-enter'); + window.setTimeout.expect(1).process(); + expect(child.attr('class')).toContain('ng-enter-active'); + window.setTimeout.expect(0).process(); + + //leave + element.append(after); + $animate.move(child, element, after); + expect(child.attr('class')).toContain('ng-move'); + window.setTimeout.expect(1).process(); + expect(child.attr('class')).toContain('ng-move-active'); + window.setTimeout.expect(0).process(); + + //hide + $animate.hide(child); + expect(child.attr('class')).toContain('ng-hide-add'); + window.setTimeout.expect(1).process(); + expect(child.attr('class')).toContain('ng-hide-add-active'); + window.setTimeout.expect(0).process(); + + //show + $animate.show(child); + expect(child.attr('class')).toContain('ng-hide-remove'); + window.setTimeout.expect(1).process(); + expect(child.attr('class')).toContain('ng-hide-remove-active'); + window.setTimeout.expect(0).process(); + + //leave + $animate.leave(child); + expect(child.attr('class')).toContain('ng-leave'); + window.setTimeout.expect(1).process(); + expect(child.attr('class')).toContain('ng-leave-active'); + window.setTimeout.expect(0).process(); + })); + + it("should not run if animations are disabled", inject(function($animate, $rootScope, $sniffer) { + $animate.enabled(false); + + $rootScope.$digest(); + + element.addClass('setup-memo'); + + element.text('123'); + expect(element.text()).toBe('123'); + $animate.show(element); + expect(element.text()).toBe('123'); + + $animate.enabled(true); + + $animate.show(element); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + expect(element.text()).toBe('memento'); + })); + + it("should only call done() once and right away if another animation takes place in between", + inject(function($animate, $rootScope, $sniffer) { + + element.append(child); + child.addClass('custom-delay'); + + expect(element).toBeShown(); + $animate.hide(child); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + expect(child).toBeShown(); + + $animate.leave(child); + expect(child).toBeHidden(); //hides instantly + + //lets change this to prove that done doesn't fire anymore for the previous hide() operation + child.css('display','block'); + child.removeClass('ng-hide'); + + window.setTimeout.expect(2000).process(); + if($sniffer.transitions) { + window.setTimeout.expect(0).process(); + } + expect(child).toBeShown(); + + expect(element.children().length).toBe(1); //still animating + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + window.setTimeout.expect(2000).process(); + if($sniffer.transitions) { + window.setTimeout.expect(0).process(); + } + expect(element.children().length).toBe(0); + })); + + it("should call the cancel callback when another animation is called on the same element", + inject(function($animate, $rootScope, $sniffer) { + + element.append(child); + + child.addClass('custom-delay ng-hide'); + $animate.show(child); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + + $animate.hide(child); + + expect(child.hasClass('animation-cancelled')).toBe(true); + })); + + + it("should NOT clobber all data on an element when animation is finished", + inject(function($animate, $rootScope, $sniffer) { + + child.css('display','none'); + element.data('foo', 'bar'); + + $animate.show(element); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + + $animate.hide(element); + + expect(element.data('foo')).toEqual('bar'); + })); + + + it("should allow multiple JS animations which run in parallel", + inject(function($animate, $rootScope, $compile, $sniffer) { + + $animate.addClass(element, 'custom-delay custom-long-delay'); + if($sniffer.transitions) { + expect(element[0].className).toContain('custom-delay-add custom-long-delay-add'); + window.setTimeout.expect(1).process(); + expect(element[0].className).toContain('custom-delay-add-active custom-long-delay-add-active'); + } + window.setTimeout.expect(2000).process(); + window.setTimeout.expect(20000).process(); + if($sniffer.transitions) { + window.setTimeout.expect(0).process(); //css animation + } + + expect(element.hasClass('custom-delay')).toBe(true); + expect(element.hasClass('custom-delay-add')).toBe(false); + expect(element.hasClass('custom-delay-add-active')).toBe(false); + + expect(element.hasClass('custom-long-delay')).toBe(true); + expect(element.hasClass('custom-long-delay-add')).toBe(false); + expect(element.hasClass('custom-long-delay-add-active')).toBe(false); + })); + + it("should allow both multiple JS and CSS animations which run in parallel", + inject(function($animate, $rootScope, $compile, $sniffer, _$rootElement_) { + $rootElement = _$rootElement_; + + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'transition: 1s linear all;' + + vendorPrefix + 'transition: 1s linear all;' + + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + element.addClass('custom-delay custom-long-delay'); + $rootScope.$digest(); + + $animate.show(element); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + window.setTimeout.expect(2000).process(); //1st JavaScript Animation + window.setTimeout.expect(20000).process(); //2nd JavaScript Animation + if($sniffer.transitions) { + window.setTimeout.expect(1000).process(); //CSS animation + } + + expect(element.hasClass('custom-delay')).toBe(true); + expect(element.hasClass('custom-delay-add')).toBe(false); + expect(element.hasClass('custom-delay-add-active')).toBe(false); + + expect(element.hasClass('custom-long-delay')).toBe(true); + expect(element.hasClass('custom-long-delay-add')).toBe(false); + expect(element.hasClass('custom-long-delay-add-active')).toBe(false); + })); + }); + + describe("with CSS3", function() { + var window, prefix, vendorPrefix; + + beforeEach(function() { + module(function($animateProvider, $provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + return function($sniffer, _$rootElement_, $animate) { + vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + $rootElement = _$rootElement_; + }; + }) + }); + + describe("Animations", function() { + it("should properly detect and make use of CSS Animations", + inject(function($animate, $rootScope, $compile, $sniffer) { + var style = 'animation: some_animation 4s linear 0s 1 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate;'; + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + + $animate.show(element); + if ($sniffer.animations) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(4000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should properly detect and make use of CSS Animations with multiple iterations", + inject(function($animate, $rootScope, $compile, $sniffer) { + var style = 'animation-duration: 2s;' + + 'animation-iteration-count: 3;' + + vendorPrefix + 'animation-duration: 2s;' + + vendorPrefix + 'animation-iteration-count: 3;'; + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + + $animate.show(element); + if ($sniffer.animations) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(6000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should fallback to the animation duration if an infinite iteration is provided", + inject(function($animate, $rootScope, $compile, $sniffer) { + var style = 'animation-duration: 2s;' + + 'animation-iteration-count: infinite;' + + vendorPrefix + 'animation-duration: 2s;' + + vendorPrefix + 'animation-iteration-count: infinite;'; + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + + $animate.show(element); + if ($sniffer.animations) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(2000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should consider the animation delay is provided", + inject(function($animate, $rootScope, $compile, $sniffer) { + var style = 'animation-duration: 2s;' + + 'animation-delay: 10s;' + + 'animation-iteration-count: 5;' + + vendorPrefix + 'animation-duration: 2s;' + + vendorPrefix + 'animation-delay: 10s;' + + vendorPrefix + 'animation-iteration-count: 5;'; + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + + $animate.show(element); + if ($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(20000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should skip animations if disabled and run when enabled", + inject(function($animate, $rootScope, $compile, $sniffer) { + $animate.enabled(false); + var style = 'animation: some_animation 2s linear 0s 1 alternate;' + + vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;' + + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + element.addClass('ng-hide'); + expect(element).toBeHidden(); + $animate.show(element); + expect(element).toBeShown(); + })); + + it("should finish the previous animation when a new animation is started", + inject(function($animate, $rootScope, $compile, $sniffer) { + var style = 'animation: some_animation 2s linear 0s 1 alternate;' + + vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;' + + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + element.addClass('custom'); + + $animate.show(element); + if($sniffer.animations) { + window.setTimeout.expect(1).process(); + expect(element.hasClass('ng-hide-remove')).toBe(true); + expect(element.hasClass('ng-hide-remove-active')).toBe(true); + } + else { //animation is skipped + expect(window.setTimeout.queue.length).toBe(0); + } + + $animate.hide(element); + expect(element.hasClass('ng-hide-remove')).toBe(false); //added right away + + if($sniffer.animations) { //cleanup some pending animations + window.setTimeout.expect(2000).process(); + window.setTimeout.expect(1).process(); + expect(element.hasClass('ng-hide-add')).toBe(true); + expect(element.hasClass('ng-hide-add-active')).toBe(true); + } + + expect(element.hasClass('ng-hide-remove-active')).toBe(false); + })); + }); + + describe("Transitions", function() { + it("should skip transitions if disabled and run when enabled", + inject(function($animate, $rootScope, $compile, $sniffer) { + $animate.enabled(false); + element = $compile(html('<div style="' + vendorPrefix + 'transition: 1s linear all">1</div>'))($rootScope); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + $animate.show(element); + expect(element).toBeShown(); + + $animate.enabled(true); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + + $animate.show(element); + if ($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(1000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should skip animations if disabled and run when enabled picking the longest specified duration", + inject(function($animate, $rootScope, $compile, $sniffer) { + element = $compile(html('<div style="' + vendorPrefix + 'transition-duration: 1s, 2000ms, 1s; ' + vendorPrefix + 'transition-property: height, left, opacity">foo</div>'))($rootScope); + element.addClass('ng-hide'); + $animate.show(element); + if ($sniffer.transitions) { + expect(element).toBeHidden(); + window.setTimeout.expect(1).process(); + window.setTimeout.expect(2000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should skip animations if disabled and run when enabled picking the longest specified duration/delay combination", + inject(function($animate, $rootScope, $compile, $sniffer) { + $animate.enabled(false); + element = $compile(html('<div style="' + vendorPrefix + + 'transition-duration: 1s, 0s, 1s; ' + vendorPrefix + + 'transition-delay: 2s, 1000ms, 2s; ' + vendorPrefix + + 'transition-property: height, left, opacity">foo</div>'))($rootScope); + + element.addClass('ng-hide'); + $animate.show(element); + expect(element).toBeShown(); + + $animate.enabled(true); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + + $animate.show(element); + if ($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(3000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should select the highest duration and delay", + inject(function($animate, $rootScope, $compile, $sniffer) { + var styles = 'transition:1s linear all 2s;' + + vendorPrefix + 'transition:1s linear all 2s;' + + 'animation:my_ani 10s 1s;' + + vendorPrefix + 'animation:my_ani 10s 1s;'; + + element = $compile(html('<div style="' + styles + '">foo</div>'))($rootScope); + + element.addClass('ng-hide'); + expect(element).toBeHidden(); + + $animate.show(element); + if ($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(11000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element).toBeShown(); + })); + + it("should finish the previous transition when a new animation is started", + inject(function($animate, $rootScope, $compile, $sniffer) { + var style = 'transition: 1s linear all;' + + vendorPrefix + 'transition: 1s linear all;' + + element = $compile(html('<div style="' + style + '">1</div>'))($rootScope); + + element.addClass('ng-hide'); + $animate.show(element); + if($sniffer.transitions) { + expect(element.hasClass('ng-hide-remove')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('ng-hide-remove-active')).toBe(true); + window.setTimeout.expect(1000).process(); + } + else { //animation is skipped + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element.hasClass('ng-hide-remove')).toBe(false); + expect(element.hasClass('ng-hide-remove-active')).toBe(false); + expect(element).toBeShown(); + + $animate.hide(element); + if($sniffer.transitions) { + expect(element.hasClass('ng-hide-add')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('ng-hide-add-active')).toBe(true); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + })); + }); + }); + + describe('animation evaluation', function () { + beforeEach(module(function($provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + })); + + it('should re-evaluate the CSS classes for an animation each time', + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + var element = parent.find('span'); + $rootElement.append(parent); + angular.element(document.body).append($rootElement); + + element[0].className = 'abc'; + $animate.enter(element, parent); + if ($sniffer.transitions) { + expect(element.hasClass('abc ng-enter')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('abc ng-enter ng-enter-active')).toBe(true); + window.setTimeout.expect(0).process(); + } + expect(element.hasClass('abc')).toBe(true); + + element[0].className = 'xyz'; + $animate.enter(element, parent); + if ($sniffer.transitions) { + expect(element.hasClass('xyz')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('xyz ng-enter ng-enter-active')).toBe(true); + window.setTimeout.expect(0).process(); + } + expect(element.hasClass('xyz')).toBe(true); + })); + + it('should only append active to the newly append CSS className values', + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + var element = parent.find('span'); + $rootElement.append(parent); + angular.element(document.body).append($rootElement); + + element.attr('class','one two'); + + $animate.enter(element, parent); + if($sniffer.transitions) { + expect(element.hasClass('one two ng-enter')).toBe(true); + window.setTimeout.expect(1).process(); + 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); + window.setTimeout.expect(0).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + + expect(element.hasClass('one two')).toBe(true); + })); + }); + + describe("Callbacks", function() { + + var window, vendorPrefix; + beforeEach(function() { + module(function($animateProvider, $provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + $animateProvider.register('.custom', function() { + return { + show : function(element, done) { + window.setTimeout(done, 2000); + } + } + }); + $animateProvider.register('.other', function() { + return { + start : function(element, done) { + window.setTimeout(done, 10000); + } + } + }); + }) + inject(function($sniffer, $animate) { + vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + }); + }); + + it("should fire the enter callback", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + var element = parent.find('span'); + $rootElement.append(parent); + body.append($rootElement); + + var flag = false; + $animate.enter(element, parent, null, function() { + flag = true; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(flag).toBe(true); + })); + + it("should fire the leave callback", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + var element = parent.find('span'); + $rootElement.append(parent); + body.append($rootElement); + + var flag = false; + $animate.leave(element, function() { + flag = true; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(flag).toBe(true); + })); + + it("should fire the move callback", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + var parent2 = jqLite('<div id="nice"></div>'); + var element = parent.find('span'); + $rootElement.append(parent); + body.append($rootElement); + + var flag = false; + $animate.move(element, parent, parent2, function() { + flag = true; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + + expect(flag).toBe(true); + expect(element.parent().id).toBe(parent2.id); + })); + + it("should fire the addClass/removeClass callbacks", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + var element = parent.find('span'); + $rootElement.append(parent); + body.append($rootElement); + + var signature = ''; + $animate.addClass(element, 'on', function() { + signature += 'A'; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + + $animate.removeClass(element, 'on', function() { + signature += 'B'; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + + expect(signature).toBe('AB'); + })); + + it("should fire a done callback when provided with no animation", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + var element = parent.find('span'); + $rootElement.append(parent); + body.append($rootElement); + + var flag = false; + $animate.show(element, function() { + flag = true; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(flag).toBe(true); + })); + + it("should fire a done callback when provided with a css animation/transition", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var transition = 'transition:1s linear all;'; + var style = transition + ' ' + vendorPrefix + transition; + var parent = jqLite('<div><span style="' + style + '"></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = parent.find('span'); + + var flag = false; + $animate.show(element, function() { + flag = true; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(1000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(flag).toBe(true); + })); + + it("should fire a done callback when provided with a JS animation", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = parent.find('span'); + element.addClass('custom'); + + var flag = false; + $animate.show(element, function() { + flag = true; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + window.setTimeout.expect(2000).process(); + if($sniffer.transitions) { + window.setTimeout.expect(0).process(); + } + expect(flag).toBe(true); + })); + + it("should fire the callback right away if another animation is called right after", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = parent.find('span'); + + var signature = ''; + $animate.show(element, function() { + signature += 'A'; + }); + $animate.hide(element, function() { + signature += 'B'; + }); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + $animate.hide(element); //earlier animation cancelled + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + expect(signature).toBe('AB'); + })); + }); + + describe("addClass / removeClass", function() { + + var window, vendorPrefix; + beforeEach(function() { + module(function($animateProvider, $provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + $animateProvider.register('.klassy', function() { + return { + addClass : function(element, className, done) { + window.setTimeout(done, 500); + }, + removeClass : function(element, className, done) { + window.setTimeout(done, 3000); + } + } + }); + }) + inject(function($sniffer, $animate) { + vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + }); + }); + + it("should add and remove CSS classes after an animation even if no animation is present", + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = jqLite(parent.find('span')); + + $animate.addClass(element,'klass'); + + if($sniffer.transitions) { + expect(element.hasClass('klass-add')).toBe(true); + } + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + expect(element.hasClass('klass-add')).toBe(true); + expect(element.hasClass('klass-add-active')).toBe(true); + expect(element.hasClass('klass')).toBe(false); + + window.setTimeout.expect(0).process(); + expect(element.hasClass('klass-add')).toBe(false); + expect(element.hasClass('klass-add-active')).toBe(false); + } + expect(element.hasClass('klass')).toBe(true); + + $animate.removeClass(element,'klass'); + + if($sniffer.transitions) { + expect(element.hasClass('klass')).toBe(true); + expect(element.hasClass('klass-remove')).toBe(true); + + window.setTimeout.expect(1).process(); + expect(element.hasClass('klass')).toBe(true); + expect(element.hasClass('klass-remove')).toBe(true); + expect(element.hasClass('klass-remove-active')).toBe(true); + + window.setTimeout.expect(0).process(); + } + + expect(element.hasClass('klass')).toBe(false); + expect(element.hasClass('klass-remove')).toBe(false); + expect(element.hasClass('klass-remove-active')).toBe(false); + })); + + it("should add and remove CSS classes with a callback", + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = jqLite(parent.find('span')); + + var signature = ''; + + $animate.addClass(element,'klass', function() { + signature += 'A'; + }); + + if($sniffer.transitions) { + expect(element.hasClass('klass')).toBe(false); + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + expect(element.hasClass('klass')).toBe(true); + + $animate.removeClass(element,'klass', function() { + signature += 'B'; + }); + + if($sniffer.transitions) { + expect(element.hasClass('klass')).toBe(true); + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + expect(element.hasClass('klass')).toBe(false); + + expect(signature).toBe('AB'); + })); + + it("should end the current addClass animation, add the CSS class and then run the removeClass animation", + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var parent = jqLite('<div><span></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = jqLite(parent.find('span')); + + var signature = ''; + + $animate.addClass(element,'klass', function() { + signature += '1'; + }); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + expect(element.hasClass('klass')).toBe(false); + expect(element.hasClass('klass-add')).toBe(true); + } + + //this cancels out the older animation + $animate.removeClass(element,'klass', function() { + signature += '2'; + }); + + if($sniffer.transitions) { + expect(element.hasClass('klass')).toBe(true); + expect(element.hasClass('klass-add')).toBe(false); + expect(element.hasClass('klass-add-active')).toBe(false); + + expect(element.hasClass('klass-remove')).toBe(true); + window.setTimeout.expect(0).process(); + window.setTimeout.expect(1).process(); + window.setTimeout.expect(0).process(); + } + + expect(element.hasClass('klass')).toBe(false); + expect(signature).toBe('12'); + })); + + it("should properly execute JS animations and use callbacks when using addClass / removeClass", + inject(function($animate, $rootScope, $sniffer, $rootElement) { + var parent = jqLite('<div><span></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = jqLite(parent.find('span')); + + var signature = ''; + + $animate.addClass(element,'klassy', function() { + signature += 'X'; + }); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + window.setTimeout.expect(500).process(); + if($sniffer.transitions) { + window.setTimeout.expect(0).process(); + } + expect(element.hasClass('klassy')).toBe(true); + + $animate.removeClass(element,'klassy', function() { + signature += 'Y'; + }); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + } + window.setTimeout.expect(3000).process(); + if($sniffer.transitions) { + window.setTimeout.expect(0).process(); + } + expect(element.hasClass('klassy')).toBe(false); + + expect(signature).toBe('XY'); + })); + + it("should properly execute CSS animations/transitions and use callbacks when using addClass / removeClass", + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var transition = 'transition:11s linear all;'; + var style = transition + ' ' + vendorPrefix + transition; + var parent = jqLite('<div><span style="' + style + '"></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = jqLite(parent.find('span')); + + var signature = ''; + + $animate.addClass(element,'klass', function() { + signature += 'd'; + }); + if($sniffer.transitions) { + expect(element.hasClass('klass-add')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('klass-add-active')).toBe(true); + window.setTimeout.expect(11000).process(); + expect(element.hasClass('klass-add')).toBe(false); + expect(element.hasClass('klass-add-active')).toBe(false); + } + expect(element.hasClass('klass')).toBe(true); + + $animate.removeClass(element,'klass', function() { + signature += 'b'; + }); + if($sniffer.transitions) { + expect(element.hasClass('klass-remove')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('klass-remove-active')).toBe(true); + window.setTimeout.expect(11000).process(); + expect(element.hasClass('klass-remove')).toBe(false); + expect(element.hasClass('klass-remove-active')).toBe(false); + } + expect(element.hasClass('klass')).toBe(false); + + expect(signature).toBe('db'); + })); + + it("should allow for multiple css classes to be animated plus a callback when added", + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var transition = 'transition:7s linear all;'; + var style = transition + ' ' + vendorPrefix + transition; + var parent = jqLite('<div><span style="' + style + '"></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = jqLite(parent.find('span')); + + var flag = false; + $animate.addClass(element,'one two', function() { + flag = true; + }); + + if($sniffer.transitions) { + expect(element.hasClass('one-add')).toBe(true); + expect(element.hasClass('two-add')).toBe(true); + window.setTimeout.expect(1).process(); + + expect(element.hasClass('one-add-active')).toBe(true); + expect(element.hasClass('two-add-active')).toBe(true); + window.setTimeout.expect(7000).process(); + + expect(element.hasClass('one-add')).toBe(false); + expect(element.hasClass('one-add-active')).toBe(false); + expect(element.hasClass('two-add')).toBe(false); + expect(element.hasClass('two-add-active')).toBe(false); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + + expect(element.hasClass('one')).toBe(true); + expect(element.hasClass('two')).toBe(true); + + expect(flag).toBe(true); + })); + + it("should allow for multiple css classes to be animated plus a callback when removed", + inject(function($animate, $rootScope, $sniffer, $rootElement) { + + var transition = 'transition:9s linear all;'; + var style = transition + ' ' + vendorPrefix + transition; + var parent = jqLite('<div><span style="' + style + '"></span></div>'); + $rootElement.append(parent); + body.append($rootElement); + var element = jqLite(parent.find('span')); + + element.addClass('one two'); + expect(element.hasClass('one')).toBe(true); + expect(element.hasClass('two')).toBe(true); + + var flag = false; + $animate.removeClass(element,'one two', function() { + flag = true; + }); + + if($sniffer.transitions) { + expect(element.hasClass('one-remove')).toBe(true); + expect(element.hasClass('two-remove')).toBe(true); + window.setTimeout.expect(1).process(); + + expect(element.hasClass('one-remove-active')).toBe(true); + expect(element.hasClass('two-remove-active')).toBe(true); + window.setTimeout.expect(9000).process(); + + expect(element.hasClass('one-remove')).toBe(false); + expect(element.hasClass('one-remove-active')).toBe(false); + expect(element.hasClass('two-remove')).toBe(false); + expect(element.hasClass('two-remove-active')).toBe(false); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + + expect(element.hasClass('one')).toBe(false); + expect(element.hasClass('two')).toBe(false); + + expect(flag).toBe(true); + })); + }); + }); + + var $rootElement, $document, window; + beforeEach(module(function($provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + + return function(_$rootElement_, _$document_, $animate) { + $rootElement = _$rootElement_; + $document = _$document_; + $animate.enabled(true); + } + })); + + function html(element) { + var body = jqLite($document[0].body); + $rootElement.append(element); + body.append($rootElement); + return element; + } + + it("should properly animate and parse CSS3 transitions", + inject(function($compile, $rootScope, $animate, $sniffer) { + + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'transition: 1s linear all;' + + vendorPrefix + 'transition: 1s linear all;'; + var element = html($compile('<div>...</div>')($rootScope)); + var child = $compile('<div style="' + style + '">...</div>')($rootScope); + + $animate.enter(child, element); + + if($sniffer.transitions) { + expect(child.hasClass('ng-enter')).toBe(true); + window.setTimeout.expect(1).process(); + expect(child.hasClass('ng-enter-active')).toBe(true); + window.setTimeout.expect(1000).process(); + } + expect(child.hasClass('ng-enter')).toBe(false); + expect(child.hasClass('ng-enter-active')).toBe(false); + })); + + it("should properly animate and parse CSS3 animations", + inject(function($compile, $rootScope, $animate, $sniffer) { + + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'animation: some_animation 4s linear 1s 2 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 1s 2 alternate;'; + var element = html($compile('<div>...</div>')($rootScope)); + var child = $compile('<div style="' + style + '">...</div>')($rootScope); + $animate.enter(child, element); + + if($sniffer.transitions) { + expect(child.hasClass('ng-enter')).toBe(true); + window.setTimeout.expect(1).process(); + expect(child.hasClass('ng-enter-active')).toBe(true); + window.setTimeout.expect(9000).process(); + } + expect(child.hasClass('ng-enter')).toBe(false); + expect(child.hasClass('ng-enter-active')).toBe(false); + })); + + it("should skip animations if the browser does not support CSS3 transitions and CSS3 animations", + inject(function($compile, $rootScope, $animate, $sniffer) { + + $sniffer.animations = false; + $sniffer.transitions = false; + + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'animation: some_animation 4s linear 1s 2 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 1s 2 alternate;'; + var element = html($compile('<div>...</div>')($rootScope)); + var child = $compile('<div style="' + style + '">...</div>')($rootScope); + + expect(child.hasClass('ng-enter')).toBe(false); + $animate.enter(child, element); + expect(window.setTimeout.queue.length).toBe(0); + expect(child.hasClass('ng-enter')).toBe(false); + })); + + it("should run other defined animations inline with CSS3 animations", function() { + module(function($animateProvider) { + $animateProvider.register('.custom', function($window) { + return { + enter : function(element, done) { + element.addClass('i-was-animated'); + $window.setTimeout(done, 10); + } + } + }); + }) + inject(function($compile, $rootScope, $animate, $sniffer) { + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'transition: 1s linear all;' + + vendorPrefix + 'transition: 1s linear all;'; + var element = html($compile('<div>...</div>')($rootScope)); + var child = $compile('<div style="' + style + '">...</div>')($rootScope); + + expect(child.hasClass('i-was-animated')).toBe(false); + + child.addClass('custom'); + $animate.enter(child, element); + + expect(child.hasClass('ng-enter')).toBe(true); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + expect(child.hasClass('ng-enter-active')).toBe(true); + } + + window.setTimeout.expect(10).process(); + + if($sniffer.transitions) { + expect(child.hasClass('ng-enter-active')).toBe(true); + window.setTimeout.expect(1000).process(); + expect(child.hasClass('ng-enter')).toBe(false); + expect(child.hasClass('ng-enter-active')).toBe(false); + } + + expect(child.hasClass('i-was-animated')).toBe(true); + }); + }); + + it("should properly cancel CSS transitions or animations if another animation is fired", function() { + module(function($animateProvider) { + $animateProvider.register('.usurper', function($window) { + return { + leave : function(element, done) { + element.addClass('this-is-mine-now'); + $window.setTimeout(done, 55); + } + } + }); + }); + inject(function($compile, $rootScope, $animate, $sniffer) { + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'transition: 2s linear all;' + + vendorPrefix + 'transition: 2s linear all;'; + var element = html($compile('<div>...</div>')($rootScope)); + var child = $compile('<div style="' + style + '">...</div>')($rootScope); + + $animate.enter(child, element); + + if($sniffer.transitions) { + expect(child.hasClass('ng-enter')).toBe(true); + } + + expect(child.hasClass('this-is-mine-now')).toBe(false); + child.addClass('usurper'); + $animate.leave(child); + + expect(child.hasClass('ng-enter')).toBe(false); + expect(child.hasClass('ng-enter-active')).toBe(false); + + expect(child.hasClass('usurper')).toBe(true); + expect(child.hasClass('this-is-mine-now')).toBe(true); + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(1).process(); + } + window.setTimeout.expect(55).process(); + if($sniffer.transitions) { + window.setTimeout.expect(2000).process(); + + //even though this exists, the animation will still not happen + //since the done function has already been called in the cancellation + window.setTimeout.expect(2000).process(); + } + + expect(child.hasClass('usurper-active')).toBe(false); + }); + }); + + it("should add and remove CSS classes and perform CSS animations during the process", + inject(function($compile, $rootScope, $animate, $sniffer) { + + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'transition: 10s linear all;' + + vendorPrefix + 'transition: 10s linear all;'; + var element = html($compile('<div style="' + style + '"></div>')($rootScope)); + + expect(element.hasClass('on')).toBe(false); + + $animate.addClass(element, 'on'); + + if($sniffer.transitions) { + expect(element.hasClass('on')).toBe(false); + expect(element.hasClass('on-add')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('on-add-active')).toBe(true); + window.setTimeout.expect(10000).process(); + } + + expect(element.hasClass('on')).toBe(true); + expect(element.hasClass('on-add')).toBe(false); + expect(element.hasClass('on-add-active')).toBe(false); + + $animate.removeClass(element, 'on'); + if($sniffer.transitions) { + expect(element.hasClass('on')).toBe(true); + expect(element.hasClass('on-remove')).toBe(true); + window.setTimeout.expect(1).process(); + expect(element.hasClass('on-remove-active')).toBe(true); + window.setTimeout.expect(10000).process(); + } + + expect(element.hasClass('on')).toBe(false); + expect(element.hasClass('on-remove')).toBe(false); + expect(element.hasClass('on-remove-active')).toBe(false); + })); + + it("should show and hide elements with CSS & JS animations being performed in the process", function() { + module(function($animateProvider) { + $animateProvider.register('.displayer', function($window) { + return { + show : function(element, done) { + element.removeClass('hiding'); + element.addClass('showing'); + $window.setTimeout(done, 25); + }, + hide : function(element, done) { + element.removeClass('showing'); + element.addClass('hiding'); + $window.setTimeout(done, 555); + } + } + }); + }) + inject(function($compile, $rootScope, $animate, $sniffer) { + var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; + var style = 'transition: 5s linear all;' + + vendorPrefix + 'transition: 5s linear all;'; + var element = html($compile('<div style="' + style + '"></div>')($rootScope)); + + element.addClass('displayer'); + + expect(element).toBeShown(); + expect(element.hasClass('showing')).toBe(false); + expect(element.hasClass('hiding')).toBe(false); + + $animate.hide(element); + + if($sniffer.transitions) { + expect(element).toBeShown(); //still showing + window.setTimeout.expect(1).process(); + expect(element).toBeShown(); + } + window.setTimeout.expect(555).process(); + if($sniffer.transitions) { + expect(element).toBeShown(); + window.setTimeout.expect(5000).process(); + } + expect(element).toBeHidden(); + + expect(element.hasClass('showing')).toBe(false); + expect(element.hasClass('hiding')).toBe(true); + $animate.show(element); + + if($sniffer.transitions) { + expect(element).toBeHidden(); + window.setTimeout.expect(1).process(); + expect(element).toBeHidden(); + } + window.setTimeout.expect(25).process(); + if($sniffer.transitions) { + expect(element).toBeHidden(); + window.setTimeout.expect(5000).process(); + } + expect(element).toBeShown(); + + expect(element.hasClass('showing')).toBe(true); + expect(element.hasClass('hiding')).toBe(false); + }); + }); + +}); diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index 50531c18..5f021f2d 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -7,9 +7,9 @@ describe('ngView', function() { beforeEach(module(function($provide) { $provide.value('$window', angular.mock.createMockWindow()); - return function($rootScope, $compile, $animator) { + return function($rootScope, $compile, $animate) { element = $compile('<ng:view onload="load()"></ng:view>')($rootScope); - $animator.enabled(true); + $animate.enabled(true); }; })); @@ -509,8 +509,7 @@ describe('ngView', function() { }); }); - describe('ngAnimate ', function() { - var window, vendorPrefix; + describe('animations', function() { var body, element, $rootElement; function html(html) { @@ -520,11 +519,6 @@ describe('ngView', function() { return element; } - function applyCSS(element, cssProp, cssValue) { - element.css(cssProp, cssValue); - element.css(vendorPrefix + cssProp, cssValue); - } - beforeEach(module(function() { // we need to run animation on attached elements; return function(_$rootElement_) { @@ -540,128 +534,131 @@ describe('ngView', function() { beforeEach(module(function($provide, $routeProvider) { - $provide.value('$window', window = angular.mock.createMockWindow()); $routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'}); - return function($sniffer, $templateCache, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + return function($templateCache) { $templateCache.put('/foo.html', [200, '<div>data</div>', {}]); - $animator.enabled(true); } })); - it('should fire off the enter animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer, $location) { - element = $compile(html('<div ng-view ng-animate="{enter: \'custom-enter\'}"></div>'))($rootScope); + describe('hooks', function() { + beforeEach(module('mock.animate')); - $location.path('/foo'); - $rootScope.$digest(); + it('should fire off the enter animation', + inject(function($compile, $rootScope, $location, $animate) { + var item; + element = $compile(html('<div ng-view></div>'))($rootScope); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '1s linear all'); + $location.path('/foo'); + $rootScope.$digest(); - if ($sniffer.transitions) { - expect(child.attr('class')).toContain('custom-enter'); - window.setTimeout.expect(1).process(); - - expect(child.attr('class')).toContain('custom-enter-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + item = $animate.process('leave').element; + item = $animate.process('leave').element; + item = $animate.process('leave').element; - expect(child.attr('class')).not.toContain('custom-enter'); - expect(child.attr('class')).not.toContain('custom-enter-active'); - })); - - it('should fire off the leave animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { - $templateCache.put('/foo.html', [200, '<div>foo</div>', {}]); - element = $compile(html('<div ng-view ng-animate="{leave: \'custom-leave\'}"></div>'))($rootScope); - - $location.path('/foo'); - $rootScope.$digest(); + item = $animate.process('enter').element; + expect(item.text()).toBe('data'); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '1s linear all'); + item = $animate.process('leave').element; + item = $animate.process('enter').element; + expect(item.text()).toBe('data'); + })); - $location.path('/'); - $rootScope.$digest(); + it('should fire off the leave animation', + inject(function($compile, $rootScope, $location, $templateCache, $animate) { - if ($sniffer.transitions) { - expect(child.attr('class')).toContain('custom-leave'); - window.setTimeout.expect(1).process(); + var item; + $templateCache.put('/foo.html', [200, '<div>foo</div>', {}]); + element = $compile(html('<div ng-view></div>'))($rootScope); - expect(child.attr('class')).toContain('custom-leave-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } + $location.path('/foo'); + $rootScope.$digest(); - expect(child.attr('class')).not.toContain('custom-leave'); - expect(child.attr('class')).not.toContain('custom-leave-active'); - })); + item = $animate.process('leave').element; + item = $animate.process('leave').element; + item = $animate.process('leave').element; - it('should catch and use the correct duration for animations', - inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { - $templateCache.put('/foo.html', [200, '<div>foo</div>', {}]); - element = $compile(html( - '<div ' + - 'ng-view ' + - 'ng-animate="{enter: \'customEnter\'}">' + - '</div>' - ))($rootScope); + item = $animate.process('enter').element; + expect(item.text()).toBe('foo'); - $location.path('/foo'); - $rootScope.$digest(); + item = $animate.process('leave').element; + item = $animate.process('enter').element; + expect(item.text()).toBe('foo'); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '0.5s linear all'); + $location.path('/'); + $rootScope.$digest(); - if($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect($sniffer.transitions ? 500 : 0).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - })); + item = $animate.process('leave').element; + expect(item.text()).toBe('foo'); + item = $animate.process('leave').element; + expect(item.text()).toBe('foo'); + })); + }); it('should not double compile when route changes', function() { - module(function($routeProvider, $animationProvider, $provide) { + module('ngAnimate'); + module('mock.animate'); + module(function($routeProvider, $animateProvider, $provide) { $routeProvider.when('/foo', {template: '<div ng-repeat="i in [1,2]">{{i}}</div>'}); $routeProvider.when('/bar', {template: '<div ng-repeat="i in [3,4]">{{i}}</div>'}); - $animationProvider.register('my-animation-leave', function() { + $animateProvider.register('.my-animation', function() { return { - start: function(element, done) { + leave: function(element, done) { + dump('yes'); done(); } }; }); }); - inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer) { - element = $compile(html('<ng:view onload="load()" ng-animate="\'my-animation\'"></ng:view>'))($rootScope); + inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer, $animate) { + if (!$sniffer.transitions) return; + + element = $compile(html('<ng:view onload="load()"></ng:view>'))($rootScope); $location.path('/foo'); $rootScope.$digest(); - if ($sniffer.transitions) { - $window.setTimeout.expect(1).process(); - $window.setTimeout.expect(0).process(); - } + + $animate.process('leave'); + $animate.process('leave'); + $animate.process('leave'); + $animate.process('enter'); + $animate.process('leave'); + $animate.process('enter'); + $animate.process('enter'); + $animate.process('enter'); + $animate.process('enter'); + $animate.process('enter'); + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(0).process(); + $window.setTimeout.expect(0).process(); + $window.setTimeout.expect(0).process(); + expect(element.text()).toEqual('12'); $location.path('/bar'); $rootScope.$digest(); + $animate.process('leave'); + $animate.process('enter'); + $animate.process('leave'); + $animate.process('enter'); + $animate.process('enter'); + $animate.process('enter'); + $animate.process('enter'); + $animate.process('enter'); expect(n(element.text())).toEqual('1234'); - if ($sniffer.transitions) { - $window.setTimeout.expect(1).process(); - $window.setTimeout.expect(1).process(); - } else { - $window.setTimeout.expect(1).process(); - } + + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(0).process(); + $window.setTimeout.expect(0).process(); + $window.setTimeout.expect(0).process(); + $window.setTimeout.expect(0).process(); + expect(element.text()).toEqual('34'); function n(text) { diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index d97d88a1..32dd75ea 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -233,7 +233,7 @@ function sortedHtml(element, showNgClass) { */ function isCssVisible(node) { var display = node.css('display'); - return display != 'none'; + return !node.hasClass('ng-hide') && display != 'none'; } function assertHidden(node) { |
