diff options
| -rw-r--r-- | angularFiles.js | 4 | ||||
| -rw-r--r-- | docs/spec/ngdocSpec.js | 16 | ||||
| -rw-r--r-- | docs/src/dom.js | 4 | ||||
| -rw-r--r-- | docs/src/ngdoc.js | 54 | ||||
| -rw-r--r-- | lib/showdown/showdown-0.9.js | 6 | ||||
| -rw-r--r-- | src/Angular.js | 5 | ||||
| -rw-r--r-- | src/AngularPublic.js | 2 | ||||
| -rw-r--r-- | src/loader.js | 27 | ||||
| -rw-r--r-- | src/ng/animation.js | 65 | ||||
| -rw-r--r-- | src/ng/animator.js | 312 | ||||
| -rw-r--r-- | src/ng/directive/ngInclude.js | 28 | ||||
| -rw-r--r-- | src/ng/directive/ngRepeat.js | 29 | ||||
| -rw-r--r-- | src/ng/directive/ngShowHide.js | 54 | ||||
| -rw-r--r-- | src/ng/directive/ngSwitch.js | 92 | ||||
| -rw-r--r-- | src/ng/directive/ngView.js | 20 | ||||
| -rw-r--r-- | src/ng/sniffer.js | 26 | ||||
| -rw-r--r-- | src/ngMock/angular-mocks.js | 51 | ||||
| -rw-r--r-- | test/ng/animationSpec.js | 15 | ||||
| -rw-r--r-- | test/ng/animatorSpec.js | 195 | ||||
| -rw-r--r-- | test/ng/directive/ngIncludeSpec.js | 113 | ||||
| -rw-r--r-- | test/ng/directive/ngRepeatSpec.js | 211 | ||||
| -rw-r--r-- | test/ng/directive/ngShowHideSpec.js | 101 | ||||
| -rw-r--r-- | test/ng/directive/ngSwitchSpec.js | 118 | ||||
| -rw-r--r-- | test/ng/directive/ngViewSpec.js | 104 | ||||
| -rw-r--r-- | test/ng/snifferSpec.js | 43 | 
25 files changed, 1611 insertions, 84 deletions
| diff --git a/angularFiles.js b/angularFiles.js index 968405af..6adc6b18 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -9,6 +9,8 @@ angularFiles = {      'src/auto/injector.js',      'src/ng/anchorScroll.js', +    'src/ng/animation.js', +    'src/ng/animator.js',      'src/ng/browser.js',      'src/ng/cacheFactory.js',      'src/ng/compile.js', @@ -71,7 +73,6 @@ angularFiles = {      'src/ngMock/angular-mocks.js',      'src/ngMobile/mobile.js',      'src/ngMobile/directive/ngClick.js', -      'src/bootstrap/bootstrap.js'    ], @@ -103,6 +104,7 @@ angularFiles = {      'test/ng/*.js',      'test/ng/directive/*.js',      'test/ng/filter/*.js', +    'test/ngAnimate/*.js',      'test/ngCookies/*.js',      'test/ngResource/*.js',      'test/ngSanitize/*.js', diff --git a/docs/spec/ngdocSpec.js b/docs/spec/ngdocSpec.js index 48db580b..b1bbbd61 100644 --- a/docs/spec/ngdocSpec.js +++ b/docs/spec/ngdocSpec.js @@ -475,6 +475,22 @@ describe('ngdoc', function() {              '<div><p>I am self.</p></div>');        });      }); + +    describe('@animations', function() { +      it('should render @this', function() { +        var doc = new Doc('@name a\n@animations\nenter - Add text\nleave - Remove text\n'); +        doc.ngdoc = 'filter'; +        doc.parse(); +        expect(doc.html()).toContain( +            '<h3 id="Animations">Animations</h3>\n' + +            '<div class="animations">' + +              '<ul>' + +                '<li>enter - Add text</li>' + +                '<li>leave - Remove text</li>' + +              '</ul>' + +            '</div>'); +      }); +    });    });    describe('usage', function() { diff --git a/docs/src/dom.js b/docs/src/dom.js index 2c276297..1bb9578e 100644 --- a/docs/src/dom.js +++ b/docs/src/dom.js @@ -89,7 +89,9 @@ DOM.prototype = {            replace(/-+/gm, '-').            replace(/-*$/gm, '');        anchor = {'id': id}; -      className = {'class': id.toLowerCase().replace(/[._]/mg, '-')}; +      var classNameValue = id.toLowerCase().replace(/[._]/mg, '-'); +      if(classNameValue == 'hide') classNameValue = ''; +      className = {'class': classNameValue};      }      this.tag('h' + this.headingDepth, anchor, heading);      if (content instanceof Array) { diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index f8f6cdf5..2a96abd9 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -328,6 +328,18 @@ Doc.prototype = {        });        dom.html(param.description);      }); +    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) { @@ -433,6 +445,48 @@ 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/lib/showdown/showdown-0.9.js b/lib/showdown/showdown-0.9.js index 1bab5b39..200a5ae7 100644 --- a/lib/showdown/showdown-0.9.js +++ b/lib/showdown/showdown-0.9.js @@ -977,9 +977,9 @@ var _EncodeCode = function(text) {  var _DoItalicsAndBold = function(text) { -  // <strong> must go first: +  // ** must go first:    text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, -    "<strong>$2</strong>"); +    "**$2</strong>");    text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,      "<em>$2</em>"); @@ -1293,4 +1293,4 @@ var escapeCharacters_callback = function(wholeMatch,m1) {    return "~E"+charCodeToEscape+"E";  } -} // end of Showdown.converter
\ No newline at end of file +} // end of Showdown.converter diff --git a/src/Angular.js b/src/Angular.js index d7648d72..dc2d530a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -247,6 +247,11 @@ function inherit(parent, extra) {    return extend(new (extend(function() {}, {prototype:parent}))(), extra);  } +var START_SPACE = /^\s*/; +var END_SPACE = /\s*$/; +function stripWhitespace(str) { +  return isString(str) ? str.replace(START_SPACE, '').replace(END_SPACE, '') : str; +}  /**   * @ngdoc function diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 3c5e46cb..a66c35b3 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -107,6 +107,8 @@ function publishExternalAPI(angular){          directive(ngEventDirectives);        $provide.provider({          $anchorScroll: $AnchorScrollProvider, +        $animation: $AnimationProvider, +        $animator: $AnimatorProvider,          $browser: $BrowserProvider,          $cacheFactory: $CacheFactoryProvider,          $controller: $ControllerProvider, diff --git a/src/loader.js b/src/loader.js index ecb16608..5b74a4f3 100644 --- a/src/loader.js +++ b/src/loader.js @@ -165,6 +165,33 @@ function setupModuleLoader(window) {            /**             * @ngdoc method +           * @name angular.Module#animation +           * @methodOf angular.Module +           * @param {string} name animation name +           * @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. +           * <pre> +           * 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) { ... } +           *   } +           * }) +           * </pre> +           * +           * See {@link ng.$animationProvider#register $animationProvider.register()} and +           * {@link ng.directive:ngAnimate ngAnimate} for more information. +           */ +          animation: invokeLater('$animationProvider', 'register'), + +          /** +           * @ngdoc method             * @name angular.Module#filter             * @methodOf angular.Module             * @param {string} name Filter name. diff --git a/src/ng/animation.js b/src/ng/animation.js new file mode 100644 index 00000000..9a5a5a95 --- /dev/null +++ b/src/ng/animation.js @@ -0,0 +1,65 @@ +/** + * @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) { +        try { +          return $injector.get(camelCase(name) + suffix); +        } catch (e) { +          //TODO(misko): this is a hack! we should have a better way to test if the injector has a given key. +          // The issue is that the animations are optional, and if not present they should be silently ignored. +          // The proper way to fix this is to add API onto the injector so that we can ask to see if a given +          // animation is supported. +        } +      } +    } +  }]; +}; diff --git a/src/ng/animator.js b/src/ng/animator.js new file mode 100644 index 00000000..4bd5ae3c --- /dev/null +++ b/src/ng/animator.js @@ -0,0 +1,312 @@ +'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 while + * without burduning the directive which uses the animation with animation details. The built dn 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: + * + * * {@link ng.directive:ngRepeat#animations ngRepeat} — enter, leave and move + * * {@link ng.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:ngShow#animations ngShow & ngHide} - show and hide respectively + * + * 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 --> + * <ANY ng-directive ng-animate=" 'animation' "></ANY> + * <!-- which expands to --> + * <ANY ng-directive ng-animate="{ enter: 'animation-enter', leave: 'animation-leave', ...}"></ANY> + * + * <!-- keep in mind that ng-animate can take expressions --> + * <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. + * + * <h2>CSS-defined Animations</h2> + * By default, ngAnimate attaches two CSS3 classes per animation event to the DOM element to achieve the animation. + * This is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions. + * All that is required is the following CSS code: + * + * <pre> + * <style type="text/css"> + * /* + *  The animate-enter prefix is the event name that you + *  have provided within the ngAnimate attribute. + * */ + * .animate-enter-setup { + *  -webkit-transition: 1s linear all; /* Safari/Chrome */ + *  -moz-transition: 1s linear all; /* Firefox */ + *  -ms-transition: 1s linear all; /* IE10 */ + *  -o-transition: 1s linear all; /* Opera */ + *  transition: 1s linear all; /* 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-setup.animate-enter-start { + *  /* The animation code itself */ + *  opacity: 1; + * } + * </style> + * + * <div ng-directive ng-animate="{enter: 'animate-enter'}"></div> + * </pre> + * + * Upon DOM mutation, the setup class is added first, then the browser is allowed to reflow the content and then, + * the start 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 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 animation classes surrounding it. + * + * <h2>JavaScript-defined Animations</h2> + * In the event that you do not want to use 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 ngModule. + * + * <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 using + * JavaScript animations) to animated the element, but it will not attempt to find any CSS3 transition duration value. + * 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. + *  + */ + +/** + * @ngdoc function + * @name ng.$animator + * + * @description + * The $animator 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 $AnimatorProvider = function() { +  this.$get = ['$animation', '$window', '$sniffer', function($animation, $window, $sniffer) { +    return function(scope, attrs) { +      var ngAnimateAttr = attrs.ngAnimate; +      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); +      return animator; + +      function animateActionFactory(type, beforeFn, afterFn) { +        var ngAnimateValue = ngAnimateAttr && scope.$eval(ngAnimateAttr); +        var className = ngAnimateAttr +            ? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type +            : ''; +        var animationPolyfill = $animation(className); + +        var polyfillSetup = animationPolyfill && animationPolyfill.setup; +        var polyfillStart = animationPolyfill && animationPolyfill.start; + +        if (!className) { +          return function(element, parent, after) { +            beforeFn(element, parent, after); +            afterFn(element, parent, after); +          } +        } else { +          var setupClass = className + '-setup'; +          var startClass = className + '-start'; + +          return function(element, parent, after) { +            if (!$sniffer.supportsTransitions && !polyfillSetup && !polyfillStart) { +              beforeFn(element, parent, after); +              afterFn(element, parent, after); +              return; +            } + +            element.addClass(setupClass); +            beforeFn(element, parent, after); +            if (element.length == 0) return done(); + +            var memento = (noop || polyfillSetup)(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 beginAnimation() { +              element.addClass(startClass); +              if (polyfillStart) { +                polyfillStart(element, done, memento); +              } else if (isFunction($window.getComputedStyle)) { +                var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; +                var w3cTransitionProp = 'transition'; //one day all browsers will have this + +                var durationKey = 'Duration'; +                var duration = 0; +                //we want all the styles defined before and after +                forEach(element, function(element) { +                  var globalStyles = $window.getComputedStyle(element) || {}; +                  duration = Math.max( +                      parseFloat(globalStyles[w3cTransitionProp    + durationKey]) || +                      parseFloat(globalStyles[vendorTransitionProp + durationKey]) || +                      0, +                      duration); +                }); + +                $window.setTimeout(done, duration * 1000); +              } else { +                dump(3) +                done(); +              } +            } + +            function done() { +              afterFn(element, parent, after); +              element.removeClass(setupClass); +              element.removeClass(startClass); +            } +          } +        } +      } +    } + +    function show(element) { +      element.css('display', 'block'); +    } + +    function hide(element) { +      element.css('display', 'none'); +    } + +    function insert(element, parent, after) { +      if (after) { +        after.after(element); +      } else { +        parent.append(element); +      } +    } + +    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); +    } +  }]; +}; diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js index d4eacbe3..a385d00b 100644 --- a/src/ng/directive/ngInclude.js +++ b/src/ng/directive/ngInclude.js @@ -12,6 +12,13 @@   * (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 + *   * @scope   *   * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, @@ -78,8 +85,8 @@   * @description   * Emitted every time the ngInclude content is reloaded.   */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', -                  function($http,   $templateCache,   $anchorScroll,   $compile) { +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animator', +                  function($http,   $templateCache,   $anchorScroll,   $compile,   $animator) {    return {      restrict: 'ECA',      terminal: true, @@ -88,7 +95,8 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'            onloadExp = attr.onload || '',            autoScrollExp = attr.autoscroll; -      return function(scope, element) { +      return function(scope, element, attr) { +        var animate = $animator(scope, attr);          var changeCounter = 0,              childScope; @@ -97,8 +105,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'              childScope.$destroy();              childScope = null;            } - -          element.html(''); +          animate.leave(element.contents(), element);          };          scope.$watch(srcExp, function ngIncludeWatchAction(src) { @@ -110,9 +117,12 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'                if (childScope) childScope.$destroy();                childScope = scope.$new(); +              animate.leave(element.contents(), element); -              element.html(response); -              $compile(element.contents())(childScope); +              var contents = jqLite('<div/>').html(response).contents(); + +              animate.enter(contents, element); +              $compile(contents)(childScope);                if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {                  $anchorScroll(); @@ -123,7 +133,9 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'              }).error(function() {                if (thisChangeId === changeCounter) clearContent();              }); -          } else clearContent(); +          } else { +            clearContent(); +          }          });        };      } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index a00bc9e4..fada0696 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -16,6 +16,13 @@   *   * `$middle` – `{boolean}` – true if the repeated element is between the first and last in the iterator.   *   * `$last` – `{boolean}` – true if the repeated element is last in the iterator.   * + * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**, + * **leave** and **move** effects. + * + * @animations + * enter - when a new item is added to the list or when an item is revealed after a filter + * leave - when an item is removed from the list or when an item is filtered out + * move - when an adjacent item is filtered out causing a reorder or when the item contents are reordered   *   * @element ANY   * @scope @@ -75,13 +82,15 @@        </doc:scenario>      </doc:example>   */ -var ngRepeatDirective = ['$parse', function($parse) { +var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { +  var NG_REMOVED = '$$NG_REMOVED';    return {      transclude: 'element',      priority: 1000,      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, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier, @@ -130,6 +139,7 @@ var ngRepeatDirective = ['$parse', function($parse) {          $scope.$watchCollection(rhs, function ngRepeatAction(collection){            var index, length,                cursor = $element,     // current position of the node +              nextCursor,                // Same as lastBlockMap but it has the current state. It will become the                // lastBlockMap on the next iteration.                nextBlockMap = {}, @@ -184,7 +194,8 @@ var ngRepeatDirective = ['$parse', function($parse) {            for (key in lastBlockMap) {              if (lastBlockMap.hasOwnProperty(key)) {                block = lastBlockMap[key]; -              block.element.remove(); +              animate.leave(block.element); +              block.element[0][NG_REMOVED] = true;                block.scope.$destroy();              }            } @@ -200,12 +211,17 @@ var ngRepeatDirective = ['$parse', function($parse) {                // associated scope/element                childScope = block.scope; -              if (block.element == cursor) { +              nextCursor = cursor[0]; +              do { +                nextCursor = nextCursor.nextSibling; +              } while(nextCursor && nextCursor[NG_REMOVED]); + +              if (block.element[0] == nextCursor) {                  // do nothing                  cursor = block.element;                } else {                  // existing item which got moved -                cursor.after(block.element); +                animate.move(block.element, null, cursor);                  cursor = block.element;                }              } else { @@ -221,8 +237,8 @@ var ngRepeatDirective = ['$parse', function($parse) {              childScope.$middle = !(childScope.$first || childScope.$last);              if (!block.element) { -              linker(childScope, function(clone){ -                cursor.after(clone); +              linker(childScope, function(clone) { +                animate.enter(clone, null, cursor);                  cursor = clone;                  block.scope = childScope;                  block.element = clone; @@ -236,3 +252,4 @@ var ngRepeatDirective = ['$parse', function($parse) {      }    };  }]; + diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js index 74195468..418a43ff 100644 --- a/src/ng/directive/ngShowHide.js +++ b/src/ng/directive/ngShowHide.js @@ -6,7 +6,18 @@   *   * @description   * The `ngShow` and `ngHide` directives show or hide a portion of the DOM tree (HTML) - * conditionally. + * conditionally based on **"truthy"** values evaluated within an {expression}. In other + * words, if the expression assigned to **ngShow evaluates to a true value** then **the element is set to visible** + * (via `display:block` in css) and **if false** then **the element is set to hidden** (so display:none). + * 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 + * hide - happens before the ngShow expression evaluates to a non truthy value and just before the contents are set to hidden   *   * @element ANY   * @param {expression} ngShow If the {@link guide/expression expression} is truthy @@ -33,11 +44,14 @@     </doc:example>   */  //TODO(misko): refactor to remove element from the DOM -var ngShowDirective = ngDirective(function(scope, element, attr){ -  scope.$watch(attr.ngShow, function ngShowWatchAction(value){ -    element.css('display', toBoolean(value) ? '' : 'none'); -  }); -}); +var ngShowDirective = ['$animator', function($animator) { +  return function(scope, element, attr) { +    var animate = $animator(scope, attr); +    scope.$watch(attr.ngShow, function ngShowWatchAction(value){ +      animate[toBoolean(value) ? 'show' : 'hide'](element); +    }); +  }; +}];  /** @@ -45,8 +59,19 @@ var ngShowDirective = ngDirective(function(scope, element, attr){   * @name ng.directive:ngHide   *   * @description - * The `ngHide` and `ngShow` directives hide or show a portion of the DOM tree (HTML) - * conditionally. + * The `ngShow` and `ngHide` directives show or hide a portion of the DOM tree (HTML) + * conditionally based on **"truthy"** values evaluated within an {expression}. In other + * words, if the expression assigned to **ngShow evaluates to a true value** then **the element is set to visible** + * (via `display:block` in css) and **if false** then **the element is set to hidden** (so display:none). + * 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   *   * @element ANY   * @param {expression} ngHide If the {@link guide/expression expression} is truthy then @@ -73,8 +98,11 @@ var ngShowDirective = ngDirective(function(scope, element, attr){     </doc:example>   */  //TODO(misko): refactor to remove element from the DOM -var ngHideDirective = ngDirective(function(scope, element, attr){ -  scope.$watch(attr.ngHide, function ngHideWatchAction(value){ -    element.css('display', toBoolean(value) ? 'none' : ''); -  }); -}); +var ngHideDirective = ['$animator', function($animator) { +  return function(scope, element, attr) { +    var animate = $animator(scope, attr); +    scope.$watch(attr.ngHide, function ngHideWatchAction(value){ +      animate[toBoolean(value) ? 'hide' : 'show'](element); +    }); +  }; +}]; diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js index f22e634d..8b0dab31 100644 --- a/src/ng/directive/ngSwitch.js +++ b/src/ng/directive/ngSwitch.js @@ -6,15 +6,30 @@   * @restrict EA   *   * @description - * Conditionally change the DOM structure. Elements within ngSwitch but without - * ngSwitchWhen or ngSwitchDefault directives will be preserved at the location - * as specified in the template + * The ngSwitch directive is used to conditionally swap DOM structure on your template based on a scope expression. + * Elements within ngSwitch but without ngSwitchWhen or ngSwitchDefault directives will be preserved at the location + * as specified in the template. + * + * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it + * from the template cache), ngSwitch simply choses one of the nested elements and makes it visible based on which element + * matches the value obtained from the evaluated expression. In other words, you define a container element + * (where you place the directive), place an expression on the **on="..." attribute** + * (or the **ng-switch="..." attribute**), define any inner elements inside of the directive and place + * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on + * 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   *   * @usage   * <ANY ng-switch="expression">   *   <ANY ng-switch-when="matchValue1">...</ANY>   *   <ANY ng-switch-when="matchValue2">...</ANY> - *   ...   *   <ANY ng-switch-default>...</ANY>   * </ANY>   * @@ -67,43 +82,48 @@        </doc:scenario>      </doc:example>   */ -var NG_SWITCH = 'ng-switch'; -var ngSwitchDirective = valueFn({ -  restrict: 'EA', -  require: 'ngSwitch', -  // asks for $scope to fool the BC controller module -  controller: ['$scope', function ngSwitchController() { -    this.cases = {}; -  }], -  link: function(scope, element, attr, ctrl) { -    var watchExpr = attr.ngSwitch || attr.on, -        selectedTranscludes, -        selectedElements, -        selectedScopes = []; +var ngSwitchDirective = ['$animator', function($animator) { +  return { +    restrict: 'EA', +    require: 'ngSwitch', -    scope.$watch(watchExpr, function ngSwitchWatchAction(value) { -      for (var i= 0, ii=selectedScopes.length; i<ii; i++) { -        selectedScopes[i].$destroy(); -        selectedElements[i].remove(); -      } +    // asks for $scope to fool the BC controller module +    controller: ['$scope', function ngSwitchController() { +     this.cases = {}; +    }], +    link: function(scope, element, attr, ngSwitchController) { +      var animate = $animator(scope, attr); +      var watchExpr = attr.ngSwitch || attr.on, +          selectedTranscludes, +          selectedElements, +          selectedScopes = []; -      selectedElements = []; -      selectedScopes = []; +      scope.$watch(watchExpr, function ngSwitchWatchAction(value) { +        for (var i= 0, ii=selectedScopes.length; i<ii; i++) { +          selectedScopes[i].$destroy(); +          animate.leave(selectedElements[i]); +        } -      if ((selectedTranscludes = ctrl.cases['!' + value] || ctrl.cases['?'])) { -        scope.$eval(attr.change); -        forEach(selectedTranscludes, function(selectedTransclude) { -          var selectedScope = scope.$new(); -          selectedScopes.push(selectedScope); -          selectedTransclude.transclude(selectedScope, function(caseElement) { -            selectedElements.push(caseElement); -            selectedTransclude.element.after(caseElement); +        selectedElements = []; +        selectedScopes = []; + +        if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { +          scope.$eval(attr.change); +          forEach(selectedTranscludes, function(selectedTransclude) { +            var selectedScope = scope.$new(); +            selectedScopes.push(selectedScope); +            selectedTransclude.transclude(selectedScope, function(caseElement) { +              var anchor = selectedTransclude.element; + +              selectedElements.push(caseElement); +              animate.enter(caseElement, anchor.parent(), anchor); +            });            }); -        }); -      } -    }); +        } +      }); +    }    } -}); +}];  var ngSwitchWhenDirective = ngDirective({    transclude: 'element', diff --git a/src/ng/directive/ngView.js b/src/ng/directive/ngView.js index 6e92c2d8..2ffd64da 100644 --- a/src/ng/directive/ngView.js +++ b/src/ng/directive/ngView.js @@ -12,6 +12,13 @@   * 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 + *   * @scope   * @example      <example module="ngView"> @@ -105,15 +112,16 @@   * Emitted every time the ngView content is reloaded.   */  var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile', -                       '$controller', +                       '$controller', '$animator',                 function($http,   $templateCache,   $route,   $anchorScroll,   $compile, -                        $controller) { +                        $controller,  $animator) {    return {      restrict: 'ECA',      terminal: true,      link: function(scope, element, attr) {        var lastScope, -          onloadExp = attr.onload || ''; +          onloadExp = attr.onload || '', +          animate = $animator(scope, attr);        scope.$on('$routeChangeSuccess', update);        update(); @@ -127,7 +135,7 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c        }        function clearContent() { -        element.html(''); +        animate.leave(element.contents(), element);          destroyLastScope();        } @@ -136,8 +144,8 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c              template = locals && locals.$template;          if (template) { -          element.html(template); -          destroyLastScope(); +          clearContent(); +          animate.enter(jqLite('<div></div>').html(template).contents(), element);            var link = $compile(element.contents()),                current = $route.current, diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js index 9342fbd5..19877b87 100644 --- a/src/ng/sniffer.js +++ b/src/ng/sniffer.js @@ -9,6 +9,7 @@   *   * @property {boolean} history Does the browser support html5 history api ?   * @property {boolean} hashchange Does the browser support hashchange event ? + * @property {boolean} supportsTransitions Does the browser support CSS transition events ?   *   * @description   * This is very simple implementation of testing browser's features. @@ -16,8 +17,25 @@  function $SnifferProvider() {    this.$get = ['$window', '$document', function($window, $document) {      var eventSupport = {}, -        android = int((/android (\d+)/.exec(lowercase($window.navigator.userAgent)) || [])[1]), -        document = $document[0]; +        android = int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), +        document = $document[0] || {}, +        vendorPrefix, +        vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, +        bodyStyle = document.body && document.body.style, +        transitions = false, +        match; + +    if (bodyStyle) { +      for(var prop in bodyStyle) { +        if(match = vendorRegex.exec(prop)) { +          vendorPrefix = match[0]; +          vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); +          break; +        } +      } +      transitions = !!(vendorPrefix + 'Transition' in bodyStyle); +    } +      return {        // Android has history.pushState, but it does not update location correctly @@ -41,7 +59,9 @@ function $SnifferProvider() {          return eventSupport[event];        }, -      csp: document.securityPolicy ? document.securityPolicy.isActive : false +      csp: document.securityPolicy ? document.securityPolicy.isActive : false, +      vendorPrefix: vendorPrefix, +      supportsTransitions : transitions      };    }];  } diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 8c5686ac..1f96c1e5 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -587,6 +587,57 @@ angular.mock.$LogProvider = function() {    angular.mock.TzDate.prototype = Date.prototype;  })(); +/** + * @ngdoc function + * @name angular.mock.createMockWindow + * @description + * + * This function creates a mock window object useful for controlling access ot setTimeout, but mocking out + * sufficient window's properties to allow Angular to execute. + * + * @example + * + * <pre> +    beforeEach(module(function($provide) { +      $provide.value('$window', window = angular.mock.createMockWindow()); +    })); + +    it('should do something', inject(function($window) { +      var val = null; +      $window.setTimeout(function() { val = 123; }, 10); +      expect(val).toEqual(null); +      window.setTimeout.expect(10).process(); +      expect(val).toEqual(123); +    }); + * </pre> + * + */ +angular.mock.createMockWindow = function() { +  var mockWindow = {}; +  var setTimeoutQueue = []; + +  mockWindow.document = window.document; +  mockWindow.getComputedStyle = angular.bind(window, window.getComputedStyle); +  mockWindow.scrollTo = angular.bind(window, window.scrollTo); +  mockWindow.navigator = window.navigator; +  mockWindow.setTimeout = function(fn, delay) { +    setTimeoutQueue.push({fn: fn, delay: delay}); +  }; +  mockWindow.setTimeout.queue = setTimeoutQueue; +  mockWindow.setTimeout.expect = function(delay) { +    if (setTimeoutQueue.length > 0) { +      return { +        process: function() { +          setTimeoutQueue.shift().fn(); +        } +      }; +    } else { +      expect('SetTimoutQueue empty. Expecting delay of ').toEqual(delay); +    } +  }; + +  return mockWindow; +};  /**   * @ngdoc function diff --git a/test/ng/animationSpec.js b/test/ng/animationSpec.js new file mode 100644 index 00000000..86592643 --- /dev/null +++ b/test/ng/animationSpec.js @@ -0,0 +1,15 @@ +'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 new file mode 100644 index 00000000..d8ceffab --- /dev/null +++ b/test/ng/animatorSpec.js @@ -0,0 +1,195 @@ +'use strict'; + +describe("$animator", function() { + +  var element; + +  afterEach(function(){ +    dealoc(element); +  }); + +  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) { +        animator = $animator($rootScope, {}); +        element = $compile('<div></div>')($rootScope); +      }) +    }); + +    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($animator, $compile, $rootScope) { +      element.css('display','none'); +      expect(element.css('display')).toBe('none'); +      animator.show(element); +      expect(element.css('display')).toBe('block'); +    })); + +    it("should animate the hide animation event", inject(function($animator, $compile, $rootScope) { +      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(); +            } +          } +        }); +      }) +      inject(function($animator, $compile, $rootScope) { +        element = $compile('<div></div>')($rootScope); +        child   = $compile('<div></div>')($rootScope); +        after   = $compile('<div></div>')($rootScope); +      }); +    }) + +    it("should animate the enter animation event", inject(function($animator, $rootScope) { +      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 = $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 = $animator($rootScope, { +        ngAnimate : '{move: \'custom\'}' +      }); +      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 = $animator($rootScope, { +        ngAnimate : '{show: \'custom\'}' +      }); +      element.css('display','none'); +      expect(element.css('display')).toBe('none'); +      animator.show(element); +      expect(element.css('display')).toBe('block'); +      window.setTimeout.expect(1).process(); +      expect(element.css('display')).toBe('block'); +    })); + +    it("should animate the hide animation event", inject(function($animator, $rootScope) { +      animator = $animator($rootScope, { +        ngAnimate : '{hide: \'custom\'}' +      }); +      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) { +      if (!$sniffer.supportsTransitions) return; +      animator = $animator($rootScope, { +        ngAnimate : '"custom"' +      }); + +      //enter +      animator.enter(child, element); +      expect(child.attr('class')).toContain('custom-enter-setup'); +      window.setTimeout.expect(1).process(); +      expect(child.attr('class')).toContain('custom-enter-start'); +      window.setTimeout.expect(0).process(); + +      //leave +      element.append(after); +      animator.move(child, element, after); +      expect(child.attr('class')).toContain('custom-move-setup'); +      window.setTimeout.expect(1).process(); +      expect(child.attr('class')).toContain('custom-move-start'); +      window.setTimeout.expect(0).process(); + +      //hide +      animator.hide(child); +      expect(child.attr('class')).toContain('custom-hide-setup'); +      window.setTimeout.expect(1).process(); +      expect(child.attr('class')).toContain('custom-hide-start'); +      window.setTimeout.expect(0).process(); + +      //show +      animator.show(child); +      expect(child.attr('class')).toContain('custom-show-setup'); +      window.setTimeout.expect(1).process(); +      expect(child.attr('class')).toContain('custom-show-start'); +      window.setTimeout.expect(0).process(); + +      //leave +      animator.leave(child); +      expect(child.attr('class')).toContain('custom-leave-setup'); +      window.setTimeout.expect(1).process(); +      expect(child.attr('class')).toContain('custom-leave-start'); +      window.setTimeout.expect(0).process(); +    })); +  }); + +  it("should throw an error when an invalid ng-animate syntax is provided", inject(function($compile, $rootScope) { +    expect(function() { +      element = $compile('<div ng-repeat="i in is" ng-animate=":"></div>')($rootScope); +    }).toThrow("Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:]."); +  })); +}); diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js index dce803b5..191eaa05 100644 --- a/test/ng/directive/ngIncludeSpec.js +++ b/test/ng/directive/ngIncludeSpec.js @@ -280,3 +280,116 @@ describe('ngInclude', function() {      }));    });  }); + +describe('ngInclude ngAnimate', function() { +  var element, vendorPrefix, window; + +  beforeEach(module(function($animationProvider, $provide) { +    $provide.value('$window', window = angular.mock.createMockWindow()); +    return function($sniffer) { +      vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; +    }; +  })); + +  afterEach(function(){ +    dealoc(element); +  }); + +  it('should fire off the enter animation + add and remove the css classes', +    inject(function($compile, $rootScope, $templateCache, $sniffer) { + +      $templateCache.put('enter', [200, '<div>data</div>', {}]); +      $rootScope.tpl = 'enter'; +      element = $compile( +        '<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]); +      var cssProp = vendorPrefix + 'transition'; +      var cssValue = '1s linear all'; +      child.css(cssProp, cssValue); + +      if ($sniffer.supportsTransitions) { +        expect(child.attr('class')).toContain('custom-enter-setup'); +        window.setTimeout.expect(1).process(); + +        expect(child.attr('class')).toContain('custom-enter-start'); +        window.setTimeout.expect(1000).process(); +      } else { +       expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(child.attr('class')).not.toContain('custom-enter-setup'); +      expect(child.attr('class')).not.toContain('custom-enter-start'); +  })); + +  it('should fire off the leave animation + add and remove the css classes', +    inject(function($compile, $rootScope, $templateCache, $sniffer) { +      $templateCache.put('enter', [200, '<div>data</div>', {}]); +      $rootScope.tpl = 'enter'; +      element = $compile( +        '<div ' + +          'ng-include="tpl" ' + +          'ng-animate="{leave: \'custom-leave\'}">' + +        '</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]); +      var cssProp = vendorPrefix + 'transition'; +      var cssValue = '1s linear all'; +      child.css(cssProp, cssValue); + +      $rootScope.tpl = ''; +      $rootScope.$digest(); + +      if ($sniffer.supportsTransitions) { +        expect(child.attr('class')).toContain('custom-leave-setup'); +        window.setTimeout.expect(1).process(); + +        expect(child.attr('class')).toContain('custom-leave-start'); +        window.setTimeout.expect(1000).process(); +      } else { +       expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(child.attr('class')).not.toContain('custom-leave-setup'); +      expect(child.attr('class')).not.toContain('custom-leave-start'); +  })); + +  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( +        '<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]); +      var cssProp = vendorPrefix + 'transition'; +      var cssValue = '0.5s linear all'; +      child.css(cssProp, cssValue); + +      $rootScope.tpl = 'enter'; +      $rootScope.$digest(); + +      if ($sniffer.supportsTransitions) { +        window.setTimeout.expect(1).process(); +        window.setTimeout.expect(500).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } +  })); + +}); diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 44406d6d..533b83c8 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -189,11 +189,11 @@ describe('ngRepeat', function() {        element = $compile(            '<ul>' + -              '<li ng-repeat="(key, value) in items track by $index">' + +            '<li ng-repeat="(key, value) in items track by $index">' +                '{{key}}:{{value}};' +                '<input type="checkbox" ng-model="items[key]">' + -              '</li>' + -              '</ul>')(scope); +            '</li>' + +          '</ul>')(scope);        scope.items = {misko: true, shyam: true, zhenbo:true};        scope.$digest(); @@ -410,6 +410,24 @@ describe('ngRepeat', function() {    }); +  it('should preserve data on move of elements', function() { +    element = $compile('<ul><li ng-repeat="item in array">{{item}}|</li></ul>')(scope); +    scope.array = ['a', 'b']; +    scope.$digest(); + +    var lis = element.find('li'); +    lis.eq(0).data('mark', 'a'); +    lis.eq(1).data('mark', 'b'); + +    scope.array = ['b', 'a']; +    scope.$digest(); + +    var lis = element.find('li'); +    expect(lis.eq(0).data('mark')).toEqual('b'); +    expect(lis.eq(1).data('mark')).toEqual('a'); +  }); + +    describe('stability', function() {      var a, b, c, d, lis; @@ -481,6 +499,7 @@ describe('ngRepeat', function() {        scope.items = ['hello', 'cau', 'ahoj'];        scope.$digest();        lis = element.find('li'); +      lis[2].id = 'yes';        scope.items = ['ahoj', 'hello', 'cau'];        scope.$digest(); @@ -492,3 +511,189 @@ describe('ngRepeat', function() {      });    });  }); + +describe('ngRepeat ngAnimate', function() { +  var element, vendorPrefix, window; + +  beforeEach(module(function($animationProvider, $provide) { +    $provide.value('$window', window = angular.mock.createMockWindow()); +    return function($sniffer) { +      vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; +    }; +  })); + +  afterEach(function(){ +    dealoc(element); +  }); + +  it('should fire off the enter animation + add and remove the css classes', +    inject(function($compile, $rootScope, $sniffer) { + +    element = $compile( +      '<div><div ' + +        'ng-repeat="item in items" ' + +        'ng-animate="{enter: \'custom-enter\'}">' + +        '{{ item }}' +  +      '</div></div>' +    )($rootScope); + +    $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 cssProp = vendorPrefix + 'transition'; +    var cssValue = '1s linear all'; +    var kids = element.children(); +    for(var i=0;i<kids.length;i++) { +      kids[i] = jqLite(kids[i]); +      kids[i].css(cssProp, cssValue); +    } + +    if ($sniffer.supportsTransitions) { +      angular.forEach(kids, function(kid) { +        expect(kid.attr('class')).toContain('custom-enter-setup'); +        window.setTimeout.expect(1).process(); +      }); + +      angular.forEach(kids, function(kid) { +        expect(kid.attr('class')).toContain('custom-enter-start'); +        window.setTimeout.expect(1000).process(); +      }); +    } else { +      expect(window.setTimeout.queue).toEqual([]); +    } + +    angular.forEach(kids, function(kid) { +      expect(kid.attr('class')).not.toContain('custom-enter-setup'); +      expect(kid.attr('class')).not.toContain('custom-enter-start'); +    }); +  })); + +  it('should fire off the leave animation + add and remove the css classes', +    inject(function($compile, $rootScope, $sniffer) { + +    element = $compile( +      '<div><div ' + +        'ng-repeat="item in items" ' + +        'ng-animate="{leave: \'custom-leave\'}">' + +        '{{ item }}' +  +      '</div></div>' +    )($rootScope); + +    $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 cssProp = vendorPrefix + 'transition'; +    var cssValue = '1s linear all'; +    var kids = element.children(); +    for(var i=0;i<kids.length;i++) { +      kids[i] = jqLite(kids[i]); +      kids[i].css(cssProp, cssValue); +    } + +    $rootScope.items = ['1','3']; +    $rootScope.$digest(); + +    //the last element gets pushed down when it animates +    var kid = jqLite(element.children()[1]); +    if ($sniffer.supportsTransitions) { +      expect(kid.attr('class')).toContain('custom-leave-setup'); +      window.setTimeout.expect(1).process(); +      expect(kid.attr('class')).toContain('custom-leave-start'); +      window.setTimeout.expect(1000).process(); +    } else { +      expect(window.setTimeout.queue).toEqual([]); +    } + +    expect(kid.attr('class')).not.toContain('custom-leave-setup'); +    expect(kid.attr('class')).not.toContain('custom-leave-start'); +  })); + +  it('should fire off the move animation + add and remove the css classes', +    inject(function($compile, $rootScope, $sniffer) { +      element = $compile( +        '<div>' + +          '<div ng-repeat="item in items" ng-animate="{move: \'custom-move\'}">' + +            '{{ item }}' + +          '</div>' + +        '</div>' +      )($rootScope); + +      $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 cssProp = '-' + $sniffer.vendorPrefix + '-transition'; +      var cssValue = '1s linear all'; +      var kids = element.children(); +      for(var i=0;i<kids.length;i++) { +        kids[i] = jqLite(kids[i]); +        kids[i].css(cssProp, cssValue); +      } + +      $rootScope.items = ['2','3','1']; +      $rootScope.$digest(); + +      //the last element gets pushed down when it animates +      var kids  = element.children(); +      var first = jqLite(kids[0]); +      var left  = jqLite(kids[1]); +      var right = jqLite(kids[2]); + +      if ($sniffer.supportsTransitions) { +        expect(first.attr('class')).toContain('custom-move-setup'); +        window.setTimeout.expect(1).process(); +        expect(left.attr('class')).toContain('custom-move-setup'); +        window.setTimeout.expect(1).process(); + +        expect(first.attr('class')).toContain('custom-move-start'); +        window.setTimeout.expect(1000).process(); +        expect(left.attr('class')).toContain('custom-move-start'); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(first.attr('class')).not.toContain('custom-move-setup'); +      expect(first.attr('class')).not.toContain('custom-move-start'); +      expect(left.attr('class')).not.toContain('custom-move-setup'); +      expect(left.attr('class')).not.toContain('custom-move-start'); +      expect(right.attr('class')).not.toContain('custom-move-setup'); +      expect(right.attr('class')).not.toContain('custom-move-start'); +  })); + +  it('should catch and use the correct duration for animation', +    inject(function($compile, $rootScope, $sniffer) { + +      element = $compile( +        '<div><div ' + +          'ng-repeat="item in items" ' + +          'ng-animate="{enter: \'custom-enter\'}">' + +          '{{ item }}' + +        '</div></div>' +      )($rootScope); + +      $rootScope.items = ['a','b']; +      $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 = '-' + $sniffer.vendorPrefix + '-transition'; +      var cssValue = '0.5s linear all'; +      first.css(cssProp, cssValue); +      second.css(cssProp, cssValue); + +      if ($sniffer.supportsTransitions) { +        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([]); +      } +  })); + +}); diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js index ee251dbf..d1d314e7 100644 --- a/test/ng/directive/ngShowHideSpec.js +++ b/test/ng/directive/ngShowHideSpec.js @@ -41,3 +41,104 @@ describe('ngShow / ngHide', function() {      }));    });  }); + +describe('ngShow / ngHide - ngAnimate', function() { +  var element, window; +  var vendorPrefix; + +  beforeEach(module(function($animationProvider, $provide) { +    $provide.value('$window', window = angular.mock.createMockWindow()); +    return function($sniffer) { +      vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; +    }; +  })); + +  afterEach(function() { +    dealoc(element); +  }); + +  describe('ngShow', function() { +    it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) { +      var $scope = $rootScope.$new(); +      $scope.on = true; +      element = $compile( +        '<div ' + +          'style="'+vendorPrefix+'transition: 1s linear all"' + +          'ng-show="on" ' + +          'ng-animate="{show: \'custom-show\', hide: \'custom-hide\'}">' + +        '</div>' +      )($scope); +      $scope.$digest(); + +      if ($sniffer.supportsTransitions) { +        expect(element.attr('class')).toContain('custom-show-setup'); +        window.setTimeout.expect(1).process(); + +        expect(element.attr('class')).toContain('custom-show-start'); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(element.attr('class')).not.toContain('custom-show-start'); +      expect(element.attr('class')).not.toContain('custom-show-setup'); + +      $scope.on = false; +      $scope.$digest(); +      if ($sniffer.supportsTransitions) { +        expect(element.attr('class')).toContain('custom-hide-setup'); +        window.setTimeout.expect(1).process(); +        expect(element.attr('class')).toContain('custom-hide-start'); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(element.attr('class')).not.toContain('custom-hide-start'); +      expect(element.attr('class')).not.toContain('custom-hide-setup'); +    })); +  }); + +  describe('ngHide', function() { +    it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) { +      var $scope = $rootScope.$new(); +      $scope.off = true; +      element = $compile( +          '<div ' + +              'style="'+vendorPrefix+'transition: 1s linear all"' + +              'ng-hide="off" ' + +              'ng-animate="{show: \'custom-show\', hide: \'custom-hide\'}">' + +              '</div>' +      )($scope); +      $scope.$digest(); + +      if ($sniffer.supportsTransitions) { +        expect(element.attr('class')).toContain('custom-hide-setup'); +        window.setTimeout.expect(1).process(); + +        expect(element.attr('class')).toContain('custom-hide-start'); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(element.attr('class')).not.toContain('custom-hide-start'); +      expect(element.attr('class')).not.toContain('custom-hide-setup'); + +      $scope.off = false; +      $scope.$digest(); + +      if ($sniffer.supportsTransitions) { +        expect(element.attr('class')).toContain('custom-show-setup'); +        window.setTimeout.expect(1).process(); +        expect(element.attr('class')).toContain('custom-show-start'); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(element.attr('class')).not.toContain('custom-show-start'); +      expect(element.attr('class')).not.toContain('custom-show-setup'); +    })); +  }); +}); diff --git a/test/ng/directive/ngSwitchSpec.js b/test/ng/directive/ngSwitchSpec.js index 85240b19..9d3eceaa 100644 --- a/test/ng/directive/ngSwitchSpec.js +++ b/test/ng/directive/ngSwitchSpec.js @@ -213,3 +213,121 @@ describe('ngSwitch', function() {      // afterwards a global afterEach will check for leaks in jq data cache object    }));  }); + +describe('ngSwitch ngAnimate', function() { +  var element, vendorPrefix, window; + +  beforeEach(module(function($animationProvider, $provide) { +    $provide.value('$window', window = angular.mock.createMockWindow()); +    return function($sniffer) { +      vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; +    }; +  })); + +  afterEach(function(){ +    dealoc(element); +  }); + +  it('should fire off the enter animation + set and remove the classes', +    inject(function($compile, $rootScope, $sniffer) { +      var $scope = $rootScope.$new(); +      var style = vendorPrefix + 'transition: 1s linear all'; +      element = $compile( +        '<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>' +      )($scope); + +      $scope.val = 'one'; +      $scope.$digest(); + +      expect(element.children().length).toBe(1); +      var first = element.children()[0]; + +      if ($sniffer.supportsTransitions) { +        expect(first.className).toContain('cool-enter-setup'); +        window.setTimeout.expect(1).process(); + +        expect(first.className).toContain('cool-enter-start'); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(first.className).not.toContain('cool-enter-setup'); +      expect(first.className).not.toContain('cool-enter-start'); +  })); + + +  it('should fire off the leave animation + set and remove the classes', +    inject(function($compile, $rootScope, $sniffer) { +      var $scope = $rootScope.$new(); +      var style = vendorPrefix + 'transition: 1s linear all'; +      element = $compile( +        '<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>' +      )($scope); + +      $scope.val = 'two'; +      $scope.$digest(); + +      if ($sniffer.supportsTransitions) { +        window.setTimeout.expect(1).process(); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      $scope.val = 'three'; +      $scope.$digest(); + +      expect(element.children().length).toBe($sniffer.supportsTransitions ? 2 : 1); +      var first = element.children()[0]; + + +      if ($sniffer.supportsTransitions) { +        expect(first.className).toContain('cool-leave-setup'); +        window.setTimeout.expect(1).process(); +        window.setTimeout.expect(1).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + + +      if ($sniffer.supportsTransitions) { +        expect(first.className).toContain('cool-leave-start'); +        window.setTimeout.expect(1000).process(); +        window.setTimeout.expect(1000).process(); +      } else { +        expect(window.setTimeout.queue).toEqual([]); +      } + +      expect(first.className).not.toContain('cool-leave-setup'); +      expect(first.className).not.toContain('cool-leave-start'); +  })); + +  it('should catch and use the correct duration for animation', +    inject(function($compile, $rootScope, $sniffer) { +      element = $compile( +        '<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.val = 'one'; +      $rootScope.$digest(); + +      if ($sniffer.supportsTransitions) { +        window.setTimeout.expect(1).process(); +        window.setTimeout.expect(500).process(); +      } else { +       expect(window.setTimeout.queue).toEqual([]); +      } +  })); + +}); diff --git a/test/ng/directive/ngViewSpec.js b/test/ng/directive/ngViewSpec.js index e781b98b..dcdfe686 100644 --- a/test/ng/directive/ngViewSpec.js +++ b/test/ng/directive/ngViewSpec.js @@ -473,7 +473,7 @@ describe('ngView', function() {        $rootScope.$digest();        forEach(element.contents(), function(node) { -        if ( node.nodeType == 3 ) { +        if ( node.nodeType == 3 /* text node */) {            expect(jqLite(node).scope()).not.toBe($route.current.scope);            expect(jqLite(node).controller()).not.toBeDefined();          } else { @@ -484,3 +484,105 @@ describe('ngView', function() {      });    });  }); + +describe('ngAnimate', function() { +  var element, window; + +  beforeEach(module(function($provide, $routeProvider) { +    $provide.value('$window', window = angular.mock.createMockWindow()); +    $routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'}); +    return function($templateCache) { +      $templateCache.put('/foo.html', [200, '<div>data</div>', {}]); +    } +  })); + +  afterEach(function(){ +    dealoc(element); +  }); + +  it('should fire off the enter animation + add and remove the css classes', +      inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { +        element = $compile('<div ng-view ng-animate="{enter: \'custom-enter\'}"></div>')($rootScope); + +        $location.path('/foo'); +        $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]); +        var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; +        var cssValue = '1s linear all'; +        child.css(cssProp, cssValue); + +        if ($sniffer.supportsTransitions) { +          expect(child.attr('class')).toContain('custom-enter-setup'); +          window.setTimeout.expect(1).process(); + +          expect(child.attr('class')).toContain('custom-enter-start'); +          window.setTimeout.expect(1000).process(); +        } else { +          expect(window.setTimeout.queue).toEqual([]); +        } + +        expect(child.attr('class')).not.toContain('custom-enter-setup'); +        expect(child.attr('class')).not.toContain('custom-enter-start'); +      })); + +  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('<div ng-view ng-animate="{leave: \'custom-leave\'}"></div>')($rootScope); + +    $location.path('/foo'); +    $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]); +    var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; +    var cssValue = '1s linear all'; +    child.css(cssProp, cssValue); + +    $location.path('/'); +    $rootScope.$digest(); + +    if ($sniffer.supportsTransitions) { +      expect(child.attr('class')).toContain('custom-leave-setup'); +      window.setTimeout.expect(1).process(); + +      expect(child.attr('class')).toContain('custom-leave-start'); +      window.setTimeout.expect(1000).process(); +    } else { +      expect(window.setTimeout.queue).toEqual([]); +    } + +    expect(child.attr('class')).not.toContain('custom-leave-setup'); +    expect(child.attr('class')).not.toContain('custom-leave-start'); +  })); + +  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( +        '<div ' + +            'ng-view ' + +            'ng-animate="{enter: \'customEnter\'}">' + +            '</div>' +    )($rootScope); + +    $location.path('/foo'); +    $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]); +    var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; +    var cssValue = '0.5s linear all'; +    child.css(cssProp, cssValue); + +    if($sniffer.supportsTransitions) { +      window.setTimeout.expect(1).process(); +      window.setTimeout.expect($sniffer.supportsTransitions ? 500 : 0).process(); +    } else { +      expect(window.setTimeout.queue).toEqual([]); +    } +  })); + +}); diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js index 2369deaf..d791c17b 100644 --- a/test/ng/snifferSpec.js +++ b/test/ng/snifferSpec.js @@ -5,6 +5,9 @@ describe('$sniffer', function() {    function sniffer($window, $document) {      $window.navigator = {};      $document = jqLite($document || {}); +    if (!$document[0].body) { +      $document[0].body = window.document.body; +    }      return new $SnifferProvider().$get[2]($window, $document);    } @@ -21,11 +24,11 @@ describe('$sniffer', function() {    describe('hashchange', function() {      it('should be true if onhashchange property defined', function() { -      expect(sniffer({onhashchange: true}, {}).hashchange).toBe(true); +      expect(sniffer({onhashchange: true}).hashchange).toBe(true);      });      it('should be false if onhashchange property not defined', function() { -      expect(sniffer({}, {}).hashchange).toBe(false); +      expect(sniffer({}).hashchange).toBe(false);      });      it('should be false if documentMode is 7 (IE8 comp mode)', function() { @@ -83,7 +86,7 @@ describe('$sniffer', function() {    describe('csp', function() {      it('should be false if document.securityPolicy.isActive not available', function() { -      expect(sniffer({}, {}).csp).toBe(false); +      expect(sniffer({}).csp).toBe(false);      }); @@ -96,4 +99,38 @@ describe('$sniffer', function() {        expect(sniffer({}, createDocumentWithCSP(true)).csp).toBe(true);      });    }); + +  describe('vendorPrefix', function() { + +    it('should return the correct vendor prefix based on the browser', function() { +      inject(function($sniffer, $window) { +        var expectedPrefix; +        var ua = $window.navigator.userAgent.toLowerCase(); +        if(/chrome/i.test(ua) || /safari/i.test(ua) || /webkit/i.test(ua)) { +          expectedPrefix = 'Webkit'; +        } +        else if(/firefox/i.test(ua)) { +          expectedPrefix = 'Moz'; +        } +        else if(/ie/i.test(ua)) { +          expectedPrefix = 'Ms'; +        } +        else if(/opera/i.test(ua)) { +          expectedPrefix = 'O'; +        } +        expect($sniffer.vendorPrefix).toBe(expectedPrefix); +      }); +    }); + +  }); + +  describe('supportsTransitions', function() { + +    it('should be either true or false', function() { +      inject(function($sniffer) { +        expect($sniffer.supportsTransitions).not.toBe(undefined); +      }); +    }); + +  });  }); | 
