diff options
| author | Misko Hevery | 2013-03-20 16:24:23 -0700 |
|---|---|---|
| committer | Misko Hevery | 2013-04-02 14:05:06 -0700 |
| commit | 0b6f1ce5f89f47f9302ff1e8cd8f4b92f837c413 (patch) | |
| tree | 8cbc0c86024dd4f97d0aa54e0c9b7df9b0d56b86 /src | |
| parent | 4bfb66ce0be46d3a0e9da2f80f3e1d0c2b559828 (diff) | |
| download | angular.js-0b6f1ce5f89f47f9302ff1e8cd8f4b92f837c413.tar.bz2 | |
feat(ngAnimate): add support for animation
Diffstat (limited to 'src')
| -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 |
12 files changed, 639 insertions, 72 deletions
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 |
