Commit 1239e6a6 authored by Addy Osmani's avatar Addy Osmani

Merge pull request #751 from passy/angular-routing

AngularJS: Utilize ngRoute for filters
parents 47353c4a 723b0059
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
"name": "todomvc-angular", "name": "todomvc-angular",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"angular": "~1.2.1", "angular": "1.2.5",
"todomvc-common": "~0.1.4" "todomvc-common": "~0.1.4"
}, },
"devDependencies": { "devDependencies": {
"angular-mocks": "~1.2.1" "angular-mocks": "1.2.5",
"angular-route": "1.2.5"
} }
} }
/**
* @license AngularJS v1.2.5
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/**
* @ngdoc overview
* @name ngRoute
* @description
*
* # ngRoute
*
* The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
*
* ## Example
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
*
* {@installModule route}
*
* <div doc-module-components="ngRoute"></div>
*/
/* global -ngRouteModule */
var ngRouteModule = angular.module('ngRoute', ['ng']).
provider('$route', $RouteProvider);
/**
* @ngdoc object
* @name ngRoute.$routeProvider
* @function
*
* @description
*
* Used for configuring routes.
*
* ## Example
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
*
* ## Dependencies
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*/
function $RouteProvider(){
function inherit(parent, extra) {
return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra);
}
var routes = {};
/**
* @ngdoc method
* @name ngRoute.$routeProvider#when
* @methodOf ngRoute.$routeProvider
*
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
* contains redundant trailing slash or is missing one, the route will still match and the
* `$location.path` will be updated to add or drop the trailing slash to exactly match the
* route definition.
*
* * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up
* to the next slash are matched and stored in `$routeParams` under the given `name`
* when the route matches.
* * `path` can contain named groups starting with a colon and ending with a star:
* e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name`
* when the route matches.
* * `path` can contain optional named groups with a question mark: e.g.`:name?`.
*
* For example, routes like `/color/:color/largecode/:largecode*\/edit` will match
* `/color/brown/largecode/code/with/slashs/edit` and extract:
*
* * `color: brown`
* * `largecode: code/with/slashs`.
*
*
* @param {Object} route Mapping information to be assigned to `$route.current` on route
* match.
*
* Object properties:
*
* - `controller` – `{(string|function()=}` – Controller fn that should be associated with
* newly created scope or the name of a {@link angular.Module#controller registered
* controller} if passed as a string.
* - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be
* published to scope under the `controllerAs` name.
* - `template` – `{string=|function()=}` – html template as a string or a function that
* returns an html template as a string which should be used by {@link
* ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
* This property takes precedence over `templateUrl`.
*
* If `template` is a function, it will be called with the following parameters:
*
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
* - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
* template that should be used by {@link ngRoute.directive:ngView ngView}.
*
* If `templateUrl` is a function, it will be called with the following parameters:
*
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, the router
* will wait for them all to be resolved or one to be rejected before the controller is
* instantiated.
* If all the promises are resolved successfully, the values of the resolved promises are
* injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
* fired. If any of the promises are rejected the
* {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object
* is:
*
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected}
* and the return value is treated as the dependency. If the result is a promise, it is
* resolved before its value is injected into the controller. Be aware that
* `ngRoute.$routeParams` will still refer to the previous route within these resolve
* functions. Use `$route.current.params` to access the new route parameters, instead.
*
* - `redirectTo` – {(string|function())=} – value to update
* {@link ng.$location $location} path with and trigger route redirection.
*
* If `redirectTo` is a function, it will be called with the following parameters:
*
* - `{Object.<string>}` - route parameters extracted from the current
* `$location.path()` by applying the current route templateUrl.
* - `{string}` - current `$location.path()`
* - `{Object}` - current `$location.search()`
*
* The custom `redirectTo` function is expected to return a string which will be used
* to update `$location.path()` and `$location.search()`.
*
* - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()`
* or `$location.hash()` changes.
*
* If the option is set to `false` and url in the browser changes, then
* `$routeUpdate` event is broadcasted on the root scope.
*
* - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
*
* If the option is set to `true`, then the particular route can be matched without being
* case sensitive
*
* @returns {Object} self
*
* @description
* Adds a new route definition to the `$route` service.
*/
this.when = function(path, route) {
routes[path] = angular.extend(
{reloadOnSearch: true},
route,
path && pathRegExp(path, route)
);
// create redirection for trailing slashes
if (path) {
var redirectPath = (path[path.length-1] == '/')
? path.substr(0, path.length-1)
: path +'/';
routes[redirectPath] = angular.extend(
{redirectTo: path},
pathRegExp(redirectPath, route)
);
}
return this;
};
/**
* @param path {string} path
* @param opts {Object} options
* @return {?Object}
*
* @description
* Normalizes the given path, returning a regular expression
* and the original path.
*
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
*/
function pathRegExp(path, opts) {
var insensitive = opts.caseInsensitiveMatch,
ret = {
originalPath: path,
regexp: path
},
keys = ret.keys = [];
path = path
.replace(/([().])/g, '\\$1')
.replace(/(\/)?:(\w+)([\?|\*])?/g, function(_, slash, key, option){
var optional = option === '?' ? option : null;
var star = option === '*' ? option : null;
keys.push({ name: key, optional: !!optional });
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (star && '(.+?)' || '([^/]+)')
+ (optional || '')
+ ')'
+ (optional || '');
})
.replace(/([\/$\*])/g, '\\$1');
ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
return ret;
}
/**
* @ngdoc method
* @name ngRoute.$routeProvider#otherwise
* @methodOf ngRoute.$routeProvider
*
* @description
* Sets route definition that will be used on route change when no other route definition
* is matched.
*
* @param {Object} params Mapping information to be assigned to `$route.current`.
* @returns {Object} self
*/
this.otherwise = function(params) {
this.when(null, params);
return this;
};
this.$get = ['$rootScope',
'$location',
'$routeParams',
'$q',
'$injector',
'$http',
'$templateCache',
'$sce',
function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) {
/**
* @ngdoc object
* @name ngRoute.$route
* @requires $location
* @requires $routeParams
*
* @property {Object} current Reference to the current route definition.
* The route definition contains:
*
* - `controller`: The controller constructor as define in route definition.
* - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
* controller instantiation. The `locals` contain
* the resolved values of the `resolve` map. Additionally the `locals` also contain:
*
* - `$scope` - The current route scope.
* - `$template` - The current route template HTML.
*
* @property {Array.<Object>} routes Array of all configured routes.
*
* @description
* `$route` is used for deep-linking URLs to controllers and views (HTML partials).
* It watches `$location.url()` and tries to map the path to an existing route definition.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API.
*
* The `$route` service is typically used in conjunction with the
* {@link ngRoute.directive:ngView `ngView`} directive and the
* {@link ngRoute.$routeParams `$routeParams`} service.
*
* @example
This example shows how changing the URL hash causes the `$route` to match a route against the
URL, and the `ngView` pulls in the partial.
Note that this example is using {@link ng.directive:script inlined templates}
to get it working on jsfiddle as well.
<example module="ngViewExample" deps="angular-route.js">
<file name="index.html">
<div ng-controller="MainCntl">
Choose:
<a href="Book/Moby">Moby</a> |
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
<a href="Book/Gatsby">Gatsby</a> |
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
<a href="Book/Scarlet">Scarlet Letter</a><br/>
<div ng-view></div>
<hr />
<pre>$location.path() = {{$location.path()}}</pre>
<pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{$route.current.params}}</pre>
<pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
<pre>$routeParams = {{$routeParams}}</pre>
</div>
</file>
<file name="book.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
</file>
<file name="chapter.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
Chapter Id: {{params.chapterId}}
</file>
<file name="script.js">
angular.module('ngViewExample', ['ngRoute'])
.config(function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: BookCntl,
resolve: {
// I will cause a 1 second delay
delay: function($q, $timeout) {
var delay = $q.defer();
$timeout(delay.resolve, 1000);
return delay.promise;
}
}
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
templateUrl: 'chapter.html',
controller: ChapterCntl
});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
}
function BookCntl($scope, $routeParams) {
$scope.name = "BookCntl";
$scope.params = $routeParams;
}
function ChapterCntl($scope, $routeParams) {
$scope.name = "ChapterCntl";
$scope.params = $routeParams;
}
</file>
<file name="scenario.js">
it('should load and compile correct template', function() {
element('a:contains("Moby: Ch1")').click();
var content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
sleep(2); // promises are not part of scenario waiting
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
});
</file>
</example>
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeStart
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted before a route change. At this point the route services starts
* resolving all of the dependencies needed for the route change to occurs.
* Typically this involves fetching the view template as well as any dependencies
* defined in `resolve` route property. Once all of the dependencies are resolved
* `$routeChangeSuccess` is fired.
*
* @param {Object} angularEvent Synthetic event object.
* @param {Route} next Future route information.
* @param {Route} current Current route information.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeSuccess
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted after a route dependencies are resolved.
* {@link ngRoute.directive:ngView ngView} listens for the directive
* to instantiate the controller and render the view.
*
* @param {Object} angularEvent Synthetic event object.
* @param {Route} current Current route information.
* @param {Route|Undefined} previous Previous route information, or undefined if current is
* first route entered.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeError
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted if any of the resolve promises are rejected.
*
* @param {Object} angularEvent Synthetic event object
* @param {Route} current Current route information.
* @param {Route} previous Previous route information.
* @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeUpdate
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
*
* The `reloadOnSearch` property has been set to false, and we are reusing the same
* instance of the Controller.
*/
var forceReload = false,
$route = {
routes: routes,
/**
* @ngdoc method
* @name ngRoute.$route#reload
* @methodOf ngRoute.$route
*
* @description
* Causes `$route` service to reload the current route even if
* {@link ng.$location $location} hasn't changed.
*
* As a result of that, {@link ngRoute.directive:ngView ngView}
* creates new scope, reinstantiates the controller.
*/
reload: function() {
forceReload = true;
$rootScope.$evalAsync(updateRoute);
}
};
$rootScope.$on('$locationChangeSuccess', updateRoute);
return $route;
/////////////////////////////////////////////////////
/**
* @param on {string} current url
* @param route {Object} route regexp to match the url against
* @return {?Object}
*
* @description
* Check if the route matches the current url.
*
* Inspired by match in
* visionmedia/express/lib/router/router.js.
*/
function switchRouteMatcher(on, route) {
var keys = route.keys,
params = {};
if (!route.regexp) return null;
var m = route.regexp.exec(on);
if (!m) return null;
for (var i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1];
var val = 'string' == typeof m[i]
? decodeURIComponent(m[i])
: m[i];
if (key && val) {
params[key.name] = val;
}
}
return params;
}
function updateRoute() {
var next = parseRoute(),
last = $route.current;
if (next && last && next.$$route === last.$$route
&& angular.equals(next.pathParams, last.pathParams)
&& !next.reloadOnSearch && !forceReload) {
last.params = next.params;
angular.copy(last.params, $routeParams);
$rootScope.$broadcast('$routeUpdate', last);
} else if (next || last) {
forceReload = false;
$rootScope.$broadcast('$routeChangeStart', next, last);
$route.current = next;
if (next) {
if (next.redirectTo) {
if (angular.isString(next.redirectTo)) {
$location.path(interpolate(next.redirectTo, next.params)).search(next.params)
.replace();
} else {
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
.replace();
}
}
}
$q.when(next).
then(function() {
if (next) {
var locals = angular.extend({}, next.resolve),
template, templateUrl;
angular.forEach(locals, function(value, key) {
locals[key] = angular.isString(value) ?
$injector.get(value) : $injector.invoke(value);
});
if (angular.isDefined(template = next.template)) {
if (angular.isFunction(template)) {
template = template(next.params);
}
} else if (angular.isDefined(templateUrl = next.templateUrl)) {
if (angular.isFunction(templateUrl)) {
templateUrl = templateUrl(next.params);
}
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
if (angular.isDefined(templateUrl)) {
next.loadedTemplateUrl = templateUrl;
template = $http.get(templateUrl, {cache: $templateCache}).
then(function(response) { return response.data; });
}
}
if (angular.isDefined(template)) {
locals['$template'] = template;
}
return $q.all(locals);
}
}).
// after route change
then(function(locals) {
if (next == $route.current) {
if (next) {
next.locals = locals;
angular.copy(next.params, $routeParams);
}
$rootScope.$broadcast('$routeChangeSuccess', next, last);
}
}, function(error) {
if (next == $route.current) {
$rootScope.$broadcast('$routeChangeError', next, last, error);
}
});
}
}
/**
* @returns the current active route, by matching it against the URL
*/
function parseRoute() {
// Match a route
var params, match;
angular.forEach(routes, function(route, path) {
if (!match && (params = switchRouteMatcher($location.path(), route))) {
match = inherit(route, {
params: angular.extend({}, $location.search(), params),
pathParams: params});
match.$$route = route;
}
});
// No route matched; fallback to "otherwise" route
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
}
/**
* @returns interpolation of the redirect path with the parameters
*/
function interpolate(string, params) {
var result = [];
angular.forEach((string||'').split(':'), function(segment, i) {
if (i === 0) {
result.push(segment);
} else {
var segmentMatch = segment.match(/(\w+)(.*)/);
var key = segmentMatch[1];
result.push(params[key]);
result.push(segmentMatch[2] || '');
delete params[key];
}
});
return result.join('');
}
}];
}
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
/**
* @ngdoc object
* @name ngRoute.$routeParams
* @requires $route
*
* @description
* The `$routeParams` service allows you to retrieve the current set of route parameters.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* The route parameters are a combination of {@link ng.$location `$location`}'s
* {@link ng.$location#methods_search `search()`} and {@link ng.$location#methods_path `path()`}.
* The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched.
*
* In case of parameter name collision, `path` params take precedence over `search` params.
*
* The service guarantees that the identity of the `$routeParams` object will remain unchanged
* (but its properties will likely change) even when a route change occurs.
*
* Note that the `$routeParams` are only updated *after* a route change completes successfully.
* This means that you cannot rely on `$routeParams` being correct in route resolve functions.
* Instead you can use `$route.current.params` to access the new route's parameters.
*
* @example
* <pre>
* // Given:
* // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
* // Route: /Chapter/:chapterId/Section/:sectionId
* //
* // Then
* $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
* </pre>
*/
function $RouteParamsProvider() {
this.$get = function() { return {}; };
}
ngRouteModule.directive('ngView', ngViewFactory);
ngRouteModule.directive('ngView', ngViewFillContentFactory);
/**
* @ngdoc directive
* @name ngRoute.directive:ngView
* @restrict ECA
*
* @description
* # Overview
* `ngView` is a directive that complements the {@link ngRoute.$route $route} service by
* including the rendered template of the current route into the main layout (`index.html`) file.
* Every time the current route changes, the included view changes with it according to the
* configuration of the `$route` service.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* @animations
* enter - animation is used to bring new content into the browser.
* leave - animation is used to animate existing content away.
*
* The enter and leave animation occur concurrently.
*
* @scope
* @priority 400
* @example
<example module="ngViewExample" deps="angular-route.js" animations="true">
<file name="index.html">
<div ng-controller="MainCntl as main">
Choose:
<a href="Book/Moby">Moby</a> |
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
<a href="Book/Gatsby">Gatsby</a> |
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
<a href="Book/Scarlet">Scarlet Letter</a><br/>
<div class="view-animate-container">
<div ng-view class="view-animate"></div>
</div>
<hr />
<pre>$location.path() = {{main.$location.path()}}</pre>
<pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{main.$route.current.params}}</pre>
<pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre>
<pre>$routeParams = {{main.$routeParams}}</pre>
</div>
</file>
<file name="book.html">
<div>
controller: {{book.name}}<br />
Book Id: {{book.params.bookId}}<br />
</div>
</file>
<file name="chapter.html">
<div>
controller: {{chapter.name}}<br />
Book Id: {{chapter.params.bookId}}<br />
Chapter Id: {{chapter.params.chapterId}}
</div>
</file>
<file name="animations.css">
.view-animate-container {
position:relative;
height:100px!important;
position:relative;
background:white;
border:1px solid black;
height:40px;
overflow:hidden;
}
.view-animate {
padding:10px;
}
.view-animate.ng-enter, .view-animate.ng-leave {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
display:block;
width:100%;
border-left:1px solid black;
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
padding:10px;
}
.view-animate.ng-enter {
left:100%;
}
.view-animate.ng-enter.ng-enter-active {
left:0;
}
.view-animate.ng-leave.ng-leave-active {
left:-100%;
}
</file>
<file name="script.js">
angular.module('ngViewExample', ['ngRoute', 'ngAnimate'],
function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: BookCntl,
controllerAs: 'book'
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
templateUrl: 'chapter.html',
controller: ChapterCntl,
controllerAs: 'chapter'
});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($route, $routeParams, $location) {
this.$route = $route;
this.$location = $location;
this.$routeParams = $routeParams;
}
function BookCntl($routeParams) {
this.name = "BookCntl";
this.params = $routeParams;
}
function ChapterCntl($routeParams) {
this.name = "ChapterCntl";
this.params = $routeParams;
}
</file>
<file name="scenario.js">
it('should load and compile correct template', function() {
element('a:contains("Moby: Ch1")').click();
var content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
});
</file>
</example>
*/
/**
* @ngdoc event
* @name ngRoute.directive:ngView#$viewContentLoaded
* @eventOf ngRoute.directive:ngView
* @eventType emit on the current ngView scope
* @description
* Emitted every time the ngView content is reloaded.
*/
ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate'];
function ngViewFactory( $route, $anchorScroll, $animate) {
return {
restrict: 'ECA',
terminal: true,
priority: 400,
transclude: 'element',
link: function(scope, $element, attr, ctrl, $transclude) {
var currentScope,
currentElement,
autoScrollExp = attr.autoscroll,
onloadExp = attr.onload || '';
scope.$on('$routeChangeSuccess', update);
update();
function cleanupLastView() {
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if(currentElement) {
$animate.leave(currentElement);
currentElement = null;
}
}
function update() {
var locals = $route.current && $route.current.locals,
template = locals && locals.$template;
if (template) {
var newScope = scope.$new();
var current = $route.current;
// Note: This will also link all children of ng-view that were contained in the original
// html. If that content contains controllers, ... they could pollute/change the scope.
// However, using ng-view on an element with additional content does not make sense...
// Note: We can't remove them in the cloneAttchFn of $transclude as that
// function is called before linking the content, which would apply child
// directives to non existing elements.
var clone = $transclude(newScope, function(clone) {
$animate.enter(clone, null, currentElement || $element, function onNgViewEnter () {
if (angular.isDefined(autoScrollExp)
&& (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll();
}
});
cleanupLastView();
});
currentElement = clone;
currentScope = current.scope = newScope;
currentScope.$emit('$viewContentLoaded');
currentScope.$eval(onloadExp);
} else {
cleanupLastView();
}
}
}
};
}
// This directive is called during the $transclude call of the first `ngView` directive.
// It will replace and compile the content of the element with the loaded template.
// We need this directive so that the element content is already filled when
// the link function of another directive on the same element as ngView
// is called.
ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route'];
function ngViewFillContentFactory($compile, $controller, $route) {
return {
restrict: 'ECA',
priority: -400,
link: function(scope, $element) {
var current = $route.current,
locals = current.locals;
$element.html(locals.$template);
var link = $compile($element.contents());
if (current.controller) {
locals.$scope = scope;
var controller = $controller(current.controller, locals);
if (current.controllerAs) {
scope[current.controllerAs] = controller;
}
$element.data('$ngControllerController', controller);
$element.children().data('$ngControllerController', controller);
}
link(scope);
}
};
}
})(window, window.angular);
/** /**
* @license AngularJS v1.2.1 * @license AngularJS v1.2.5
* (c) 2010-2012 Google, Inc. http://angularjs.org * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
(function(window, document, undefined) {'use strict'; (function(window, document, undefined) {'use strict';
...@@ -68,7 +68,7 @@ function minErr(module) { ...@@ -68,7 +68,7 @@ function minErr(module) {
return match; return match;
}); });
message = message + '\nhttp://errors.angularjs.org/1.2.1/' + message = message + '\nhttp://errors.angularjs.org/1.2.5/' +
(module ? module + '/' : '') + code; (module ? module + '/' : '') + code;
for (i = 2; i < arguments.length; i++) { for (i = 2; i < arguments.length; i++) {
message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' +
...@@ -159,7 +159,7 @@ function minErr(module) { ...@@ -159,7 +159,7 @@ function minErr(module) {
-assertArgFn, -assertArgFn,
-assertNotHasOwnProperty, -assertNotHasOwnProperty,
-getter, -getter,
-getBlockElements -getBlockElements,
*/ */
...@@ -472,7 +472,7 @@ function valueFn(value) {return function() {return value;};} ...@@ -472,7 +472,7 @@ function valueFn(value) {return function() {return value;};}
* @param {*} value Reference to check. * @param {*} value Reference to check.
* @returns {boolean} True if `value` is undefined. * @returns {boolean} True if `value` is undefined.
*/ */
function isUndefined(value){return typeof value == 'undefined';} function isUndefined(value){return typeof value === 'undefined';}
/** /**
...@@ -486,7 +486,7 @@ function isUndefined(value){return typeof value == 'undefined';} ...@@ -486,7 +486,7 @@ function isUndefined(value){return typeof value == 'undefined';}
* @param {*} value Reference to check. * @param {*} value Reference to check.
* @returns {boolean} True if `value` is defined. * @returns {boolean} True if `value` is defined.
*/ */
function isDefined(value){return typeof value != 'undefined';} function isDefined(value){return typeof value !== 'undefined';}
/** /**
...@@ -501,7 +501,7 @@ function isDefined(value){return typeof value != 'undefined';} ...@@ -501,7 +501,7 @@ function isDefined(value){return typeof value != 'undefined';}
* @param {*} value Reference to check. * @param {*} value Reference to check.
* @returns {boolean} True if `value` is an `Object` but not `null`. * @returns {boolean} True if `value` is an `Object` but not `null`.
*/ */
function isObject(value){return value != null && typeof value == 'object';} function isObject(value){return value != null && typeof value === 'object';}
/** /**
...@@ -515,7 +515,7 @@ function isObject(value){return value != null && typeof value == 'object';} ...@@ -515,7 +515,7 @@ function isObject(value){return value != null && typeof value == 'object';}
* @param {*} value Reference to check. * @param {*} value Reference to check.
* @returns {boolean} True if `value` is a `String`. * @returns {boolean} True if `value` is a `String`.
*/ */
function isString(value){return typeof value == 'string';} function isString(value){return typeof value === 'string';}
/** /**
...@@ -529,7 +529,7 @@ function isString(value){return typeof value == 'string';} ...@@ -529,7 +529,7 @@ function isString(value){return typeof value == 'string';}
* @param {*} value Reference to check. * @param {*} value Reference to check.
* @returns {boolean} True if `value` is a `Number`. * @returns {boolean} True if `value` is a `Number`.
*/ */
function isNumber(value){return typeof value == 'number';} function isNumber(value){return typeof value === 'number';}
/** /**
...@@ -544,7 +544,7 @@ function isNumber(value){return typeof value == 'number';} ...@@ -544,7 +544,7 @@ function isNumber(value){return typeof value == 'number';}
* @returns {boolean} True if `value` is a `Date`. * @returns {boolean} True if `value` is a `Date`.
*/ */
function isDate(value){ function isDate(value){
return toString.apply(value) == '[object Date]'; return toString.call(value) === '[object Date]';
} }
...@@ -560,7 +560,7 @@ function isDate(value){ ...@@ -560,7 +560,7 @@ function isDate(value){
* @returns {boolean} True if `value` is an `Array`. * @returns {boolean} True if `value` is an `Array`.
*/ */
function isArray(value) { function isArray(value) {
return toString.apply(value) == '[object Array]'; return toString.call(value) === '[object Array]';
} }
...@@ -575,7 +575,7 @@ function isArray(value) { ...@@ -575,7 +575,7 @@ function isArray(value) {
* @param {*} value Reference to check. * @param {*} value Reference to check.
* @returns {boolean} True if `value` is a `Function`. * @returns {boolean} True if `value` is a `Function`.
*/ */
function isFunction(value){return typeof value == 'function';} function isFunction(value){return typeof value === 'function';}
/** /**
...@@ -586,7 +586,7 @@ function isFunction(value){return typeof value == 'function';} ...@@ -586,7 +586,7 @@ function isFunction(value){return typeof value == 'function';}
* @returns {boolean} True if `value` is a `RegExp`. * @returns {boolean} True if `value` is a `RegExp`.
*/ */
function isRegExp(value) { function isRegExp(value) {
return toString.apply(value) == '[object RegExp]'; return toString.call(value) === '[object RegExp]';
} }
...@@ -608,12 +608,12 @@ function isScope(obj) { ...@@ -608,12 +608,12 @@ function isScope(obj) {
function isFile(obj) { function isFile(obj) {
return toString.apply(obj) === '[object File]'; return toString.call(obj) === '[object File]';
} }
function isBoolean(value) { function isBoolean(value) {
return typeof value == 'boolean'; return typeof value === 'boolean';
} }
...@@ -623,7 +623,7 @@ var trim = (function() { ...@@ -623,7 +623,7 @@ var trim = (function() {
// TODO: we should move this into IE/ES5 polyfill // TODO: we should move this into IE/ES5 polyfill
if (!String.prototype.trim) { if (!String.prototype.trim) {
return function(value) { return function(value) {
return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value;
}; };
} }
return function(value) { return function(value) {
...@@ -644,9 +644,9 @@ var trim = (function() { ...@@ -644,9 +644,9 @@ var trim = (function() {
* @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element).
*/ */
function isElement(node) { function isElement(node) {
return node && return !!(node &&
(node.nodeName // we are a direct element (node.nodeName // we are a direct element
|| (node.on && node.find)); // we have an on and find method part of jQuery API || (node.on && node.find))); // we have an on and find method part of jQuery API
} }
/** /**
...@@ -717,7 +717,7 @@ function includes(array, obj) { ...@@ -717,7 +717,7 @@ function includes(array, obj) {
function indexOf(array, obj) { function indexOf(array, obj) {
if (array.indexOf) return array.indexOf(obj); if (array.indexOf) return array.indexOf(obj);
for ( var i = 0; i < array.length; i++) { for (var i = 0; i < array.length; i++) {
if (obj === array[i]) return i; if (obj === array[i]) return i;
} }
return -1; return -1;
...@@ -847,7 +847,7 @@ function shallowCopy(src, dst) { ...@@ -847,7 +847,7 @@ function shallowCopy(src, dst) {
for(var key in src) { for(var key in src) {
// shallowCopy is only ever called by $compile nodeLinkFn, which has control over src // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src
// so we don't need to worry hasOwnProperty here // so we don't need to worry about using our custom hasOwnProperty here
if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') {
dst[key] = src[key]; dst[key] = src[key];
} }
...@@ -1053,7 +1053,7 @@ function startingTag(element) { ...@@ -1053,7 +1053,7 @@ function startingTag(element) {
try { try {
// turns out IE does not let you set .html() on elements which // turns out IE does not let you set .html() on elements which
// are not allowed to have children. So we just ignore it. // are not allowed to have children. So we just ignore it.
element.html(''); element.empty();
} catch(e) {} } catch(e) {}
// As Per DOM Standards // As Per DOM Standards
var TEXT_NODE = 3; var TEXT_NODE = 3;
...@@ -1181,26 +1181,38 @@ function encodeUriQuery(val, pctEncodeSpaces) { ...@@ -1181,26 +1181,38 @@ function encodeUriQuery(val, pctEncodeSpaces) {
* *
* @description * @description
* *
* Use this directive to auto-bootstrap an application. Only * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive
* one ngApp directive can be used per HTML document. The directive * designates the **root element** of the application and is typically placed near the root element
* designates the root of the application and is typically placed * of the page - e.g. on the `<body>` or `<html>` tags.
* at the root of the page.
* *
* The first ngApp found in the document will be auto-bootstrapped. To use multiple applications in * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp`
* an HTML document you must manually bootstrap them using {@link angular.bootstrap}. * found in the document will be used to define the root element to auto-bootstrap as an
* Applications cannot be nested. * application. To run multiple applications in an HTML document you must manually bootstrap them using
* {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other.
* *
* In the example below if the `ngApp` directive were not placed * You can specify an **AngularJS module** to be used as the root module for the application. This
* on the `html` element then the document would not be compiled * module will be loaded into the {@link AUTO.$injector} when the application is bootstrapped and
* and the `{{ 1+2 }}` would not be resolved to `3`. * should contain the application code needed or have dependencies on other modules that will
* contain the code. See {@link angular.module} for more information.
* *
* `ngApp` is the easiest way to bootstrap an application. * In the example below if the `ngApp` directive were not placed on the `html` element then the
* document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}`
* would not be resolved to `3`.
* *
<doc:example> * `ngApp` is the easiest, and most common, way to bootstrap an application.
<doc:source> *
I can add: 1 + 2 = {{ 1+2 }} <example module="ngAppDemo">
</doc:source> <file name="index.html">
</doc:example> <div ng-controller="ngAppDemoController">
I can add: {{a}} + {{b}} = {{ a+b }}
</file>
<file name="script.js">
angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) {
$scope.a = 1;
$scope.b = 2;
});
</file>
</example>
* *
*/ */
function angularInit(element, bootstrap) { function angularInit(element, bootstrap) {
...@@ -1397,23 +1409,25 @@ function getter(obj, path, bindFnToScope) { ...@@ -1397,23 +1409,25 @@ function getter(obj, path, bindFnToScope) {
} }
/** /**
* Return the siblings between `startNode` and `endNode`, inclusive * Return the DOM siblings between the first and last node in the given array.
* @param {Object} object with `startNode` and `endNode` properties * @param {Array} array like object
* @returns jQlite object containing the elements * @returns jQlite object containing the elements
*/ */
function getBlockElements(block) { function getBlockElements(nodes) {
if (block.startNode === block.endNode) { var startNode = nodes[0],
return jqLite(block.startNode); endNode = nodes[nodes.length - 1];
if (startNode === endNode) {
return jqLite(startNode);
} }
var element = block.startNode; var element = startNode;
var elements = [element]; var elements = [element];
do { do {
element = element.nextSibling; element = element.nextSibling;
if (!element) break; if (!element) break;
elements.push(element); elements.push(element);
} while (element !== block.endNode); } while (element !== endNode);
return jqLite(elements); return jqLite(elements);
} }
...@@ -1435,7 +1449,12 @@ function setupModuleLoader(window) { ...@@ -1435,7 +1449,12 @@ function setupModuleLoader(window) {
return obj[name] || (obj[name] = factory()); return obj[name] || (obj[name] = factory());
} }
return ensure(ensure(window, 'angular', Object), 'module', function() { var angular = ensure(window, 'angular', Object);
// We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap
angular.$$minErr = angular.$$minErr || minErr;
return ensure(angular, 'module', function() {
/** @type {Object.<string, angular.Module>} */ /** @type {Object.<string, angular.Module>} */
var modules = {}; var modules = {};
...@@ -1748,6 +1767,7 @@ function setupModuleLoader(window) { ...@@ -1748,6 +1767,7 @@ function setupModuleLoader(window) {
ngHideDirective, ngHideDirective,
ngIfDirective, ngIfDirective,
ngIncludeDirective, ngIncludeDirective,
ngIncludeFillContentDirective,
ngInitDirective, ngInitDirective,
ngNonBindableDirective, ngNonBindableDirective,
ngPluralizeDirective, ngPluralizeDirective,
...@@ -1785,6 +1805,7 @@ function setupModuleLoader(window) { ...@@ -1785,6 +1805,7 @@ function setupModuleLoader(window) {
$ParseProvider, $ParseProvider,
$RootScopeProvider, $RootScopeProvider,
$QProvider, $QProvider,
$$SanitizeUriProvider,
$SceProvider, $SceProvider,
$SceDelegateProvider, $SceDelegateProvider,
$SnifferProvider, $SnifferProvider,
...@@ -1808,11 +1829,11 @@ function setupModuleLoader(window) { ...@@ -1808,11 +1829,11 @@ function setupModuleLoader(window) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/ */
var version = { var version = {
full: '1.2.1', // all of these placeholder strings will be replaced by grunt's full: '1.2.5', // all of these placeholder strings will be replaced by grunt's
major: 1, // package task major: 1, // package task
minor: 2, minor: 2,
dot: 1, dot: 5,
codeName: 'underscore-empathy' codeName: 'singularity-expansion'
}; };
...@@ -1856,6 +1877,10 @@ function publishExternalAPI(angular){ ...@@ -1856,6 +1877,10 @@ function publishExternalAPI(angular){
angularModule('ng', ['ngLocale'], ['$provide', angularModule('ng', ['ngLocale'], ['$provide',
function ngModule($provide) { function ngModule($provide) {
// $$sanitizeUriProvider needs to be before $compileProvider as it is used by it.
$provide.provider({
$$sanitizeUri: $$SanitizeUriProvider
});
$provide.provider('$compile', $CompileProvider). $provide.provider('$compile', $CompileProvider).
directive({ directive({
a: htmlAnchorDirective, a: htmlAnchorDirective,
...@@ -1896,6 +1921,9 @@ function publishExternalAPI(angular){ ...@@ -1896,6 +1921,9 @@ function publishExternalAPI(angular){
ngRequired: requiredDirective, ngRequired: requiredDirective,
ngValue: ngValueDirective ngValue: ngValueDirective
}). }).
directive({
ngInclude: ngIncludeFillContentDirective
}).
directive(ngAttributeAliasDirectives). directive(ngAttributeAliasDirectives).
directive(ngEventDirectives); directive(ngEventDirectives);
$provide.provider({ $provide.provider({
...@@ -1973,6 +2001,7 @@ function publishExternalAPI(angular){ ...@@ -1973,6 +2001,7 @@ function publishExternalAPI(angular){
* - [`contents()`](http://api.jquery.com/contents/) * - [`contents()`](http://api.jquery.com/contents/)
* - [`css()`](http://api.jquery.com/css/) * - [`css()`](http://api.jquery.com/css/)
* - [`data()`](http://api.jquery.com/data/) * - [`data()`](http://api.jquery.com/data/)
* - [`empty()`](http://api.jquery.com/empty/)
* - [`eq()`](http://api.jquery.com/eq/) * - [`eq()`](http://api.jquery.com/eq/)
* - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name
* - [`hasClass()`](http://api.jquery.com/hasClass/) * - [`hasClass()`](http://api.jquery.com/hasClass/)
...@@ -2285,6 +2314,15 @@ function jqLiteInheritedData(element, name, value) { ...@@ -2285,6 +2314,15 @@ function jqLiteInheritedData(element, name, value) {
} }
} }
function jqLiteEmpty(element) {
for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) {
jqLiteDealoc(childNodes[i]);
}
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
////////////////////////////////////////// //////////////////////////////////////////
// Functions which are declared directly. // Functions which are declared directly.
////////////////////////////////////////// //////////////////////////////////////////
...@@ -2479,7 +2517,9 @@ forEach({ ...@@ -2479,7 +2517,9 @@ forEach({
jqLiteDealoc(childNodes[i]); jqLiteDealoc(childNodes[i]);
} }
element.innerHTML = value; element.innerHTML = value;
} },
empty: jqLiteEmpty
}, function(fn, name){ }, function(fn, name){
/** /**
* Properties: writes return selection, reads return first value * Properties: writes return selection, reads return first value
...@@ -2489,11 +2529,13 @@ forEach({ ...@@ -2489,11 +2529,13 @@ forEach({
// jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it
// in a way that survives minification. // in a way that survives minification.
if (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined) { // jqLiteEmpty takes no arguments but is a setter.
if (fn !== jqLiteEmpty &&
(((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) {
if (isObject(arg1)) { if (isObject(arg1)) {
// we are a write, but the object properties are the key/values // we are a write, but the object properties are the key/values
for(i=0; i < this.length; i++) { for (i = 0; i < this.length; i++) {
if (fn === jqLiteData) { if (fn === jqLiteData) {
// data() takes the whole object in jQuery // data() takes the whole object in jQuery
fn(this[i], arg1); fn(this[i], arg1);
...@@ -2518,7 +2560,7 @@ forEach({ ...@@ -2518,7 +2560,7 @@ forEach({
} }
} else { } else {
// we are a write, so apply to all children // we are a write, so apply to all children
for(i=0; i < this.length; i++) { for (i = 0; i < this.length; i++) {
fn(this[i], arg1, arg2); fn(this[i], arg1, arg2);
} }
// return self for chaining // return self for chaining
...@@ -2749,7 +2791,11 @@ forEach({ ...@@ -2749,7 +2791,11 @@ forEach({
}, },
find: function(element, selector) { find: function(element, selector) {
if (element.getElementsByTagName) {
return element.getElementsByTagName(selector); return element.getElementsByTagName(selector);
} else {
return [];
}
}, },
clone: jqLiteClone, clone: jqLiteClone,
...@@ -2885,6 +2931,28 @@ HashMap.prototype = { ...@@ -2885,6 +2931,28 @@ HashMap.prototype = {
* $rootScope.$digest(); * $rootScope.$digest();
* }); * });
* </pre> * </pre>
*
* Sometimes you want to get access to the injector of a currently running Angular app
* from outside Angular. Perhaps, you want to inject and compile some markup after the
* application has been bootstrapped. You can do this using extra `injector()` added
* to JQuery/jqLite elements. See {@link angular.element}.
*
* *This is fairly rare but could be the case if a third party library is injecting the
* markup.*
*
* In the following example a new block of HTML containing a `ng-controller`
* directive is added to the end of the document body by JQuery. We then compile and link
* it into the current AngularJS scope.
*
* <pre>
* var $div = $('<div ng-controller="MyCtrl">{{content.label}}</div>');
* $(document.body).append($div);
*
* angular.element(document).injector().invoke(function($compile) {
* var scope = angular.element($div).scope();
* $compile($div)(scope);
* });
* </pre>
*/ */
...@@ -3079,7 +3147,7 @@ function annotate(fn) { ...@@ -3079,7 +3147,7 @@ function annotate(fn) {
* // ... * // ...
* } * }
* // Define function dependencies * // Define function dependencies
* MyController.$inject = ['$scope', '$route']; * MyController['$inject'] = ['$scope', '$route'];
* *
* // Then * // Then
* expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
...@@ -3359,11 +3427,11 @@ function annotate(fn) { ...@@ -3359,11 +3427,11 @@ function annotate(fn) {
* @example * @example
* Here are some examples of creating value services. * Here are some examples of creating value services.
* <pre> * <pre>
* $provide.constant('ADMIN_USER', 'admin'); * $provide.value('ADMIN_USER', 'admin');
* *
* $provide.constant('RoleLookup', { admin: 0, writer: 1, reader: 2 }); * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 });
* *
* $provide.constant('halfOf', function(value) { * $provide.value('halfOf', function(value) {
* return value / 2; * return value / 2;
* }); * });
* </pre> * </pre>
...@@ -3605,24 +3673,9 @@ function createInjector(modulesToLoad) { ...@@ -3605,24 +3673,9 @@ function createInjector(modulesToLoad) {
fn = fn[length]; fn = fn[length];
} }
// http://jsperf.com/angularjs-invoke-apply-vs-switch
// Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke // #5388
switch (self ? -1 : args.length) { return fn.apply(self, args);
case 0: return fn();
case 1: return fn(args[0]);
case 2: return fn(args[0], args[1]);
case 3: return fn(args[0], args[1], args[2]);
case 4: return fn(args[0], args[1], args[2], args[3]);
case 5: return fn(args[0], args[1], args[2], args[3], args[4]);
case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]);
case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7],
args[8]);
case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7],
args[8], args[9]);
default: return fn.apply(self, args);
}
} }
function instantiate(Type, locals) { function instantiate(Type, locals) {
...@@ -3849,13 +3902,14 @@ var $AnimateProvider = ['$provide', function($provide) { ...@@ -3849,13 +3902,14 @@ var $AnimateProvider = ['$provide', function($provide) {
* inserted into the DOM * inserted into the DOM
*/ */
enter : function(element, parent, after, done) { enter : function(element, parent, after, done) {
var afterNode = after && after[after.length - 1]; if (after) {
var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; after.after(element);
// IE does not like undefined so we have to pass null. } else {
var afterNextSibling = (afterNode && afterNode.nextSibling) || null; if (!parent || !parent[0]) {
forEach(element, function(node) { parent = after.parent();
parentNode.insertBefore(node, afterNextSibling); }
}); parent.append(element);
}
done && $timeout(done, 0, false); done && $timeout(done, 0, false);
}, },
...@@ -4780,7 +4834,7 @@ function $TemplateCacheProvider() { ...@@ -4780,7 +4834,7 @@ function $TemplateCacheProvider() {
* * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
* * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
* * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found.
* * `?^` - Attempt to locate the required controller by searching the element's parentsor pass `null` to the * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the
* `link` fn if not found. * `link` fn if not found.
* *
* *
...@@ -4871,7 +4925,7 @@ function $TemplateCacheProvider() { ...@@ -4871,7 +4925,7 @@ function $TemplateCacheProvider() {
* </div> * </div>
* *
* <div class="alert alert-error"> * <div class="alert alert-error">
* **Note:** The `transclude` function that is passed to the compile function is deperecated, as it * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it
* e.g. does not know about the right outer scope. Please use the transclude function that is passed * e.g. does not know about the right outer scope. Please use the transclude function that is passed
* to the link function instead. * to the link function instead.
* </div> * </div>
...@@ -5081,14 +5135,12 @@ var $compileMinErr = minErr('$compile'); ...@@ -5081,14 +5135,12 @@ var $compileMinErr = minErr('$compile');
* *
* @description * @description
*/ */
$CompileProvider.$inject = ['$provide']; $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider'];
function $CompileProvider($provide) { function $CompileProvider($provide, $$sanitizeUriProvider) {
var hasDirectives = {}, var hasDirectives = {},
Suffix = 'Directive', Suffix = 'Directive',
COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/;
aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//;
// Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes
// The assumption is that future DOM event attribute names will begin with // The assumption is that future DOM event attribute names will begin with
...@@ -5172,10 +5224,11 @@ function $CompileProvider($provide) { ...@@ -5172,10 +5224,11 @@ function $CompileProvider($provide) {
*/ */
this.aHrefSanitizationWhitelist = function(regexp) { this.aHrefSanitizationWhitelist = function(regexp) {
if (isDefined(regexp)) { if (isDefined(regexp)) {
aHrefSanitizationWhitelist = regexp; $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp);
return this; return this;
} else {
return $$sanitizeUriProvider.aHrefSanitizationWhitelist();
} }
return aHrefSanitizationWhitelist;
}; };
...@@ -5202,18 +5255,18 @@ function $CompileProvider($provide) { ...@@ -5202,18 +5255,18 @@ function $CompileProvider($provide) {
*/ */
this.imgSrcSanitizationWhitelist = function(regexp) { this.imgSrcSanitizationWhitelist = function(regexp) {
if (isDefined(regexp)) { if (isDefined(regexp)) {
imgSrcSanitizationWhitelist = regexp; $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp);
return this; return this;
} else {
return $$sanitizeUriProvider.imgSrcSanitizationWhitelist();
} }
return imgSrcSanitizationWhitelist;
}; };
this.$get = [ this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
'$controller', '$rootScope', '$document', '$sce', '$animate', '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri',
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
$controller, $rootScope, $document, $sce, $animate) { $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {
var Attributes = function(element, attr) { var Attributes = function(element, attr) {
this.$$element = element; this.$$element = element;
...@@ -5260,6 +5313,24 @@ function $CompileProvider($provide) { ...@@ -5260,6 +5313,24 @@ function $CompileProvider($provide) {
} }
}, },
/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$updateClass
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Adds and removes the appropriate CSS class values to the element based on the difference
* between the new and old CSS class values (specified as newClasses and oldClasses).
*
* @param {string} newClasses The current CSS className value
* @param {string} oldClasses The former CSS className value
*/
$updateClass : function(newClasses, oldClasses) {
this.$removeClass(tokenDifference(oldClasses, newClasses));
this.$addClass(tokenDifference(newClasses, oldClasses));
},
/** /**
* Set a normalized attribute on the element in a way such that all directives * Set a normalized attribute on the element in a way such that all directives
* can share the attribute. This function properly handles boolean attributes. * can share the attribute. This function properly handles boolean attributes.
...@@ -5270,15 +5341,10 @@ function $CompileProvider($provide) { ...@@ -5270,15 +5341,10 @@ function $CompileProvider($provide) {
* @param {string=} attrName Optional none normalized name. Defaults to key. * @param {string=} attrName Optional none normalized name. Defaults to key.
*/ */
$set: function(key, value, writeAttr, attrName) { $set: function(key, value, writeAttr, attrName) {
//special case for class attribute addition + removal // TODO: decide whether or not to throw an error if "class"
//so that class changes can tap into the animation //is set through this function since it may cause $updateClass to
//hooks provided by the $animate service //become unstable.
if(key == 'class') {
value = value || '';
var current = this.$$element.attr('class') || '';
this.$removeClass(tokenDifference(current, value).join(' '));
this.$addClass(tokenDifference(value, current).join(' '));
} else {
var booleanKey = getBooleanAttrName(this.$$element[0], key), var booleanKey = getBooleanAttrName(this.$$element[0], key),
normalizedVal, normalizedVal,
nodeName; nodeName;
...@@ -5305,16 +5371,7 @@ function $CompileProvider($provide) { ...@@ -5305,16 +5371,7 @@ function $CompileProvider($provide) {
// sanitize a[href] and img[src] values // sanitize a[href] and img[src] values
if ((nodeName === 'A' && key === 'href') || if ((nodeName === 'A' && key === 'href') ||
(nodeName === 'IMG' && key === 'src')) { (nodeName === 'IMG' && key === 'src')) {
// NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. this[key] = value = $$sanitizeUri(value, key === 'src');
if (!msie || msie >= 8 ) {
normalizedVal = urlResolve(value).href;
if (normalizedVal !== '') {
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
this[key] = value = 'unsafe:' + normalizedVal;
}
}
}
} }
if (writeAttr !== false) { if (writeAttr !== false) {
...@@ -5324,7 +5381,6 @@ function $CompileProvider($provide) { ...@@ -5324,7 +5381,6 @@ function $CompileProvider($provide) {
this.$$element.attr(attrName, value); this.$$element.attr(attrName, value);
} }
} }
}
// fire observers // fire observers
var $$observers = this.$$observers; var $$observers = this.$$observers;
...@@ -5335,22 +5391,6 @@ function $CompileProvider($provide) { ...@@ -5335,22 +5391,6 @@ function $CompileProvider($provide) {
$exceptionHandler(e); $exceptionHandler(e);
} }
}); });
function tokenDifference(str1, str2) {
var values = [],
tokens1 = str1.split(/\s+/),
tokens2 = str2.split(/\s+/);
outer:
for(var i=0;i<tokens1.length;i++) {
var token = tokens1[i];
for(var j=0;j<tokens2.length;j++) {
if(token == tokens2[j]) continue outer;
}
values.push(token);
}
return values;
}
}, },
...@@ -5533,7 +5573,7 @@ function $CompileProvider($provide) { ...@@ -5533,7 +5573,7 @@ function $CompileProvider($provide) {
createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn) createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn)
); );
} else { } else {
nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn); nodeLinkFn(childLinkFn, childScope, node, $rootElement, boundTranscludeFn);
} }
} else if (childLinkFn) { } else if (childLinkFn) {
childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn); childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
...@@ -5821,7 +5861,7 @@ function $CompileProvider($provide) { ...@@ -5821,7 +5861,7 @@ function $CompileProvider($provide) {
}); });
} else { } else {
$template = jqLite(jqLiteClone(compileNode)).contents(); $template = jqLite(jqLiteClone(compileNode)).contents();
$compileNode.html(''); // clear contents $compileNode.empty(); // clear contents
childTranscludeFn = compile($template, transcludeFn); childTranscludeFn = compile($template, transcludeFn);
} }
} }
...@@ -6002,7 +6042,7 @@ function $CompileProvider($provide) { ...@@ -6002,7 +6042,7 @@ function $CompileProvider($provide) {
optional = (match[2] == '?'), optional = (match[2] == '?'),
mode = match[1], // @, =, or & mode = match[1], // @, =, or &
lastValue, lastValue,
parentGet, parentSet; parentGet, parentSet, compare;
isolateScope.$$isolateBindings[scopeName] = mode + attrName; isolateScope.$$isolateBindings[scopeName] = mode + attrName;
...@@ -6025,6 +6065,11 @@ function $CompileProvider($provide) { ...@@ -6025,6 +6065,11 @@ function $CompileProvider($provide) {
return; return;
} }
parentGet = $parse(attrs[attrName]); parentGet = $parse(attrs[attrName]);
if (parentGet.literal) {
compare = equals;
} else {
compare = function(a,b) { return a === b; };
}
parentSet = parentGet.assign || function() { parentSet = parentGet.assign || function() {
// reset the change, or we will throw this exception on every $digest // reset the change, or we will throw this exception on every $digest
lastValue = isolateScope[scopeName] = parentGet(scope); lastValue = isolateScope[scopeName] = parentGet(scope);
...@@ -6035,19 +6080,18 @@ function $CompileProvider($provide) { ...@@ -6035,19 +6080,18 @@ function $CompileProvider($provide) {
lastValue = isolateScope[scopeName] = parentGet(scope); lastValue = isolateScope[scopeName] = parentGet(scope);
isolateScope.$watch(function parentValueWatch() { isolateScope.$watch(function parentValueWatch() {
var parentValue = parentGet(scope); var parentValue = parentGet(scope);
if (!compare(parentValue, isolateScope[scopeName])) {
if (parentValue !== isolateScope[scopeName]) {
// we are out of sync and need to copy // we are out of sync and need to copy
if (parentValue !== lastValue) { if (!compare(parentValue, lastValue)) {
// parent changed and it has precedence // parent changed and it has precedence
lastValue = isolateScope[scopeName] = parentValue; isolateScope[scopeName] = parentValue;
} else { } else {
// if the parent can be assigned then do so // if the parent can be assigned then do so
parentSet(scope, parentValue = lastValue = isolateScope[scopeName]); parentSet(scope, parentValue = isolateScope[scopeName]);
} }
} }
return parentValue; return lastValue = parentValue;
}); }, null, parentGet.literal);
break; break;
case '&': case '&':
...@@ -6249,7 +6293,7 @@ function $CompileProvider($provide) { ...@@ -6249,7 +6293,7 @@ function $CompileProvider($provide) {
? origAsyncDirective.templateUrl($compileNode, tAttrs) ? origAsyncDirective.templateUrl($compileNode, tAttrs)
: origAsyncDirective.templateUrl; : origAsyncDirective.templateUrl;
$compileNode.html(''); $compileNode.empty();
$http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}).
success(function(content) { success(function(content) {
...@@ -6372,9 +6416,14 @@ function $CompileProvider($provide) { ...@@ -6372,9 +6416,14 @@ function $CompileProvider($provide) {
function getTrustedContext(node, attrNormalizedName) { function getTrustedContext(node, attrNormalizedName) {
if (attrNormalizedName == "srcdoc") {
return $sce.HTML;
}
var tag = nodeName_(node);
// maction[xlink:href] can source SVG. It's not limited to <maction>. // maction[xlink:href] can source SVG. It's not limited to <maction>.
if (attrNormalizedName == "xlinkHref" || if (attrNormalizedName == "xlinkHref" ||
(nodeName_(node) != "IMG" && (attrNormalizedName == "src" || (tag == "FORM" && attrNormalizedName == "action") ||
(tag != "IMG" && (attrNormalizedName == "src" ||
attrNormalizedName == "ngSrc"))) { attrNormalizedName == "ngSrc"))) {
return $sce.RESOURCE_URL; return $sce.RESOURCE_URL;
} }
...@@ -6420,8 +6469,18 @@ function $CompileProvider($provide) { ...@@ -6420,8 +6469,18 @@ function $CompileProvider($provide) {
attr[name] = interpolateFn(scope); attr[name] = interpolateFn(scope);
($$observers[name] || ($$observers[name] = [])).$$inter = true; ($$observers[name] || ($$observers[name] = [])).$$inter = true;
(attr.$$observers && attr.$$observers[name].$$scope || scope). (attr.$$observers && attr.$$observers[name].$$scope || scope).
$watch(interpolateFn, function interpolateFnWatchAction(value) { $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
attr.$set(name, value); //special case for class attribute addition + removal
//so that class changes can tap into the animation
//hooks provided by the $animate service. Be sure to
//skip animations when the first digest occurs (when
//both the new and the old values are the same) since
//the CSS classes are the non-interpolated values
if(name === 'class' && newValue != oldValue) {
attr.$updateClass(newValue, oldValue);
} else {
attr.$set(name, newValue);
}
}); });
} }
}; };
...@@ -6563,6 +6622,22 @@ function directiveLinkingFn( ...@@ -6563,6 +6622,22 @@ function directiveLinkingFn(
/* function(Function) */ boundTranscludeFn /* function(Function) */ boundTranscludeFn
){} ){}
function tokenDifference(str1, str2) {
var values = '',
tokens1 = str1.split(/\s+/),
tokens2 = str2.split(/\s+/);
outer:
for(var i = 0; i < tokens1.length; i++) {
var token = tokens1[i];
for(var j = 0; j < tokens2.length; j++) {
if(token == tokens2[j]) continue outer;
}
values += (values.length > 0 ? ' ' : '') + token;
}
return values;
}
/** /**
* @ngdoc object * @ngdoc object
* @name ng.$controllerProvider * @name ng.$controllerProvider
...@@ -7773,12 +7848,13 @@ var XHR = window.XMLHttpRequest || function() { ...@@ -7773,12 +7848,13 @@ var XHR = window.XMLHttpRequest || function() {
*/ */
function $HttpBackendProvider() { function $HttpBackendProvider() {
this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) {
return createHttpBackend($browser, XHR, $browser.defer, $window.angular.callbacks, return createHttpBackend($browser, XHR, $browser.defer, $window.angular.callbacks, $document[0]);
$document[0], $window.location.protocol.replace(':', ''));
}]; }];
} }
function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, locationProtocol) { function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument) {
var ABORTED = -1;
// TODO(vojta): fix the signature // TODO(vojta): fix the signature
return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
var status; var status;
...@@ -7814,13 +7890,19 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, ...@@ -7814,13 +7890,19 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
// always async // always async
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
if (xhr.readyState == 4) { if (xhr.readyState == 4) {
var responseHeaders = xhr.getAllResponseHeaders(); var responseHeaders = null,
response = null;
if(status !== ABORTED) {
responseHeaders = xhr.getAllResponseHeaders();
response = xhr.responseType ? xhr.response : xhr.responseText;
}
// responseText is the old-school way of retrieving response (supported by IE8 & 9) // responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10) // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
completeRequest(callback, completeRequest(callback,
status || xhr.status, status || xhr.status,
(xhr.responseType ? xhr.response : xhr.responseText), response,
responseHeaders); responseHeaders);
} }
}; };
...@@ -7844,20 +7926,20 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, ...@@ -7844,20 +7926,20 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
function timeoutRequest() { function timeoutRequest() {
status = -1; status = ABORTED;
jsonpDone && jsonpDone(); jsonpDone && jsonpDone();
xhr && xhr.abort(); xhr && xhr.abort();
} }
function completeRequest(callback, status, response, headersString) { function completeRequest(callback, status, response, headersString) {
var protocol = locationProtocol || urlResolve(url).protocol; var protocol = urlResolve(url).protocol;
// cancel timeout and subsequent timeout promise resolution // cancel timeout and subsequent timeout promise resolution
timeoutId && $browserDefer.cancel(timeoutId); timeoutId && $browserDefer.cancel(timeoutId);
jsonpDone = xhr = null; jsonpDone = xhr = null;
// fix status code for file protocol (it's always 0) // fix status code for file protocol (it's always 0)
status = (protocol == 'file') ? (response ? 200 : 404) : status; status = (protocol == 'file' && status === 0) ? (response ? 200 : 404) : status;
// normalize IE bug (http://bugs.jquery.com/ticket/1450) // normalize IE bug (http://bugs.jquery.com/ticket/1450)
status = status == 1223 ? 204 : status; status = status == 1223 ? 204 : status;
...@@ -7873,6 +7955,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, ...@@ -7873,6 +7955,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
// - adds and immediately removes script elements from the document // - adds and immediately removes script elements from the document
var script = rawDocument.createElement('script'), var script = rawDocument.createElement('script'),
doneWrapper = function() { doneWrapper = function() {
script.onreadystatechange = script.onload = script.onerror = null;
rawDocument.body.removeChild(script); rawDocument.body.removeChild(script);
if (done) done(); if (done) done();
}; };
...@@ -7880,12 +7963,16 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, ...@@ -7880,12 +7963,16 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
script.type = 'text/javascript'; script.type = 'text/javascript';
script.src = url; script.src = url;
if (msie) { if (msie && msie <= 8) {
script.onreadystatechange = function() { script.onreadystatechange = function() {
if (/loaded|complete/.test(script.readyState)) doneWrapper(); if (/loaded|complete/.test(script.readyState)) {
doneWrapper();
}
}; };
} else { } else {
script.onload = script.onerror = doneWrapper; script.onload = script.onerror = function() {
doneWrapper();
};
} }
rawDocument.body.appendChild(script); rawDocument.body.appendChild(script);
...@@ -7996,8 +8083,8 @@ function $InterpolateProvider() { ...@@ -7996,8 +8083,8 @@ function $InterpolateProvider() {
* *
<pre> <pre>
var $interpolate = ...; // injected var $interpolate = ...; // injected
var exp = $interpolate('Hello {{name}}!'); var exp = $interpolate('Hello {{name | uppercase}}!');
expect(exp({name:'Angular'}).toEqual('Hello Angular!'); expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!');
</pre> </pre>
* *
* *
...@@ -8472,7 +8559,47 @@ function LocationHashbangUrl(appBase, hashPrefix) { ...@@ -8472,7 +8559,47 @@ function LocationHashbangUrl(appBase, hashPrefix) {
hashPrefix); hashPrefix);
} }
parseAppUrl(withoutHashUrl, this, appBase); parseAppUrl(withoutHashUrl, this, appBase);
this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase);
this.$$compose(); this.$$compose();
/*
* In Windows, on an anchor node on documents loaded from
* the filesystem, the browser will return a pathname
* prefixed with the drive name ('/C:/path') when a
* pathname without a drive is set:
* * a.setAttribute('href', '/foo')
* * a.pathname === '/C:/foo' //true
*
* Inside of Angular, we're always using pathnames that
* do not include drive names for routing.
*/
function removeWindowsDriveName (path, url, base) {
/*
Matches paths for file protocol on windows,
such as /C:/foo/bar, and captures only /foo/bar.
*/
var windowsFilePathExp = /^\/?.*?:(\/.*)/;
var firstPathSegmentMatch;
//Get the relative path from the input URL.
if (url.indexOf(base) === 0) {
url = url.replace(base, '');
}
/*
* The input URL intentionally contains a
* first path segment that ends with a colon.
*/
if (windowsFilePathExp.exec(url)) {
return path;
}
firstPathSegmentMatch = windowsFilePathExp.exec(path);
return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path;
}
}; };
/** /**
...@@ -8962,7 +9089,7 @@ function $LocationProvider(){ ...@@ -8962,7 +9089,7 @@ function $LocationProvider(){
* *
* The main purpose of this service is to simplify debugging and troubleshooting. * The main purpose of this service is to simplify debugging and troubleshooting.
* *
* The default is not to log `debug` messages. You can use * The default is to log `debug` messages. You can use
* {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this.
* *
* @example * @example
...@@ -9155,23 +9282,24 @@ function ensureSafeMemberName(name, fullExpression) { ...@@ -9155,23 +9282,24 @@ function ensureSafeMemberName(name, fullExpression) {
function ensureSafeObject(obj, fullExpression) { function ensureSafeObject(obj, fullExpression) {
// nifty check if obj is Function that is fast and works across iframes and other contexts // nifty check if obj is Function that is fast and works across iframes and other contexts
if (obj && obj.constructor === obj) { if (obj) {
if (obj.constructor === obj) {
throw $parseMinErr('isecfn', throw $parseMinErr('isecfn',
'Referencing Function in Angular expressions is disallowed! Expression: {0}', 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
fullExpression); fullExpression);
} else if (// isWindow(obj) } else if (// isWindow(obj)
obj && obj.document && obj.location && obj.alert && obj.setInterval) { obj.document && obj.location && obj.alert && obj.setInterval) {
throw $parseMinErr('isecwindow', throw $parseMinErr('isecwindow',
'Referencing the Window in Angular expressions is disallowed! Expression: {0}', 'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
fullExpression); fullExpression);
} else if (// isElement(obj) } else if (// isElement(obj)
obj && (obj.nodeName || (obj.on && obj.find))) { obj.children && (obj.nodeName || (obj.on && obj.find))) {
throw $parseMinErr('isecdom', throw $parseMinErr('isecdom',
'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
fullExpression); fullExpression);
} else {
return obj;
} }
}
return obj;
} }
var OPERATORS = { var OPERATORS = {
...@@ -10128,7 +10256,7 @@ function getterFn(path, options, fullExp) { ...@@ -10128,7 +10256,7 @@ function getterFn(path, options, fullExp) {
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
(options.unwrapPromises (options.unwrapPromises
? 'if (s && s.then) {\n' + ? 'if (s && s.then) {\n' +
' pw("' + fullExp.replace(/\"/g, '\\"') + '");\n' + ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' +
' if (!("$$v" in s)) {\n' + ' if (!("$$v" in s)) {\n' +
' p=s;\n' + ' p=s;\n' +
' p.$$v = undefined;\n' + ' p.$$v = undefined;\n' +
...@@ -10480,7 +10608,7 @@ function $ParseProvider() { ...@@ -10480,7 +10608,7 @@ function $ParseProvider() {
* *
* # Differences between Kris Kowal's Q and $q * # Differences between Kris Kowal's Q and $q
* *
* There are three main differences: * There are two main differences:
* *
* - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation
* mechanism in angular, which means faster propagation of resolution or rejection into your * mechanism in angular, which means faster propagation of resolution or rejection into your
...@@ -10929,6 +11057,7 @@ function qFactory(nextTick, exceptionHandler) { ...@@ -10929,6 +11057,7 @@ function qFactory(nextTick, exceptionHandler) {
function $RootScopeProvider(){ function $RootScopeProvider(){
var TTL = 10; var TTL = 10;
var $rootScopeMinErr = minErr('$rootScope'); var $rootScopeMinErr = minErr('$rootScope');
var lastDirtyWatch = null;
this.digestTtl = function(value) { this.digestTtl = function(value) {
if (arguments.length) { if (arguments.length) {
...@@ -11013,11 +11142,11 @@ function $RootScopeProvider(){ ...@@ -11013,11 +11142,11 @@ function $RootScopeProvider(){
* @description * @description
* Creates a new child {@link ng.$rootScope.Scope scope}. * Creates a new child {@link ng.$rootScope.Scope scope}.
* *
* The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and * The parent scope will propagate the {@link ng.$rootScope.Scope#methods_$digest $digest()} and
* {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the * {@link ng.$rootScope.Scope#methods_$digest $digest()} events. The scope can be removed from the
* scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * scope hierarchy using {@link ng.$rootScope.Scope#methods_$destroy $destroy()}.
* *
* {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is * {@link ng.$rootScope.Scope#methods_$destroy $destroy()} must be called on a scope when it is
* desired for the scope and its child scopes to be permanently detached from the parent and * desired for the scope and its child scopes to be permanently detached from the parent and
* thus stop participating in model change detection and listener notification by invoking. * thus stop participating in model change detection and listener notification by invoking.
* *
...@@ -11030,7 +11159,7 @@ function $RootScopeProvider(){ ...@@ -11030,7 +11159,7 @@ function $RootScopeProvider(){
* *
*/ */
$new: function(isolate) { $new: function(isolate) {
var Child, var ChildScope,
child; child;
if (isolate) { if (isolate) {
...@@ -11040,11 +11169,11 @@ function $RootScopeProvider(){ ...@@ -11040,11 +11169,11 @@ function $RootScopeProvider(){
child.$$asyncQueue = this.$$asyncQueue; child.$$asyncQueue = this.$$asyncQueue;
child.$$postDigestQueue = this.$$postDigestQueue; child.$$postDigestQueue = this.$$postDigestQueue;
} else { } else {
Child = function() {}; // should be anonymous; This is so that when the minifier munges ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges
// the name it does not become random set of chars. This will then show up as class // the name it does not become random set of chars. This will then show up as class
// name in the debugger. // name in the debugger.
Child.prototype = this; ChildScope.prototype = this;
child = new Child(); child = new ChildScope();
child.$id = nextUid(); child.$id = nextUid();
} }
child['this'] = child; child['this'] = child;
...@@ -11070,11 +11199,11 @@ function $RootScopeProvider(){ ...@@ -11070,11 +11199,11 @@ function $RootScopeProvider(){
* @description * @description
* Registers a `listener` callback to be executed whenever the `watchExpression` changes. * Registers a `listener` callback to be executed whenever the `watchExpression` changes.
* *
* - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#methods_$digest
* $digest()} and should return the value that will be watched. (Since * $digest()} and should return the value that will be watched. (Since
* {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the * {@link ng.$rootScope.Scope#methods_$digest $digest()} reruns when it detects changes the
* `watchExpression` can execute multiple times per * `watchExpression` can execute multiple times per
* {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) * {@link ng.$rootScope.Scope#methods_$digest $digest()} and should be idempotent.)
* - The `listener` is called only when the value from the current `watchExpression` and the * - The `listener` is called only when the value from the current `watchExpression` and the
* previous call to `watchExpression` are not equal (with the exception of the initial run, * previous call to `watchExpression` are not equal (with the exception of the initial run,
* see below). The inequality is determined according to * see below). The inequality is determined according to
...@@ -11086,13 +11215,13 @@ function $RootScopeProvider(){ ...@@ -11086,13 +11215,13 @@ function $RootScopeProvider(){
* iteration limit is 10 to prevent an infinite loop deadlock. * iteration limit is 10 to prevent an infinite loop deadlock.
* *
* *
* If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, * If you want to be notified whenever {@link ng.$rootScope.Scope#methods_$digest $digest} is called,
* you can register a `watchExpression` function with no `listener`. (Since `watchExpression` * you can register a `watchExpression` function with no `listener`. (Since `watchExpression`
* can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a * can execute multiple times per {@link ng.$rootScope.Scope#methods_$digest $digest} cycle when a
* change is detected, be prepared for multiple calls to your listener.) * change is detected, be prepared for multiple calls to your listener.)
* *
* After a watcher is registered with the scope, the `listener` fn is called asynchronously * After a watcher is registered with the scope, the `listener` fn is called asynchronously
* (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the * (via {@link ng.$rootScope.Scope#methods_$evalAsync $evalAsync}) to initialize the
* watcher. In rare cases, this is undesirable because the listener is called when the result * watcher. In rare cases, this is undesirable because the listener is called when the result
* of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you
* can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the
...@@ -11156,7 +11285,7 @@ function $RootScopeProvider(){ ...@@ -11156,7 +11285,7 @@ function $RootScopeProvider(){
* *
* *
* @param {(function()|string)} watchExpression Expression that is evaluated on each * @param {(function()|string)} watchExpression Expression that is evaluated on each
* {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. A change in the return value triggers
* a call to the `listener`. * a call to the `listener`.
* *
* - `string`: Evaluated as {@link guide/expression expression} * - `string`: Evaluated as {@link guide/expression expression}
...@@ -11183,6 +11312,8 @@ function $RootScopeProvider(){ ...@@ -11183,6 +11312,8 @@ function $RootScopeProvider(){
eq: !!objectEquality eq: !!objectEquality
}; };
lastDirtyWatch = null;
// in the case user pass string, we need to compile it, do we really need this ? // in the case user pass string, we need to compile it, do we really need this ?
if (!isFunction(listener)) { if (!isFunction(listener)) {
var listenFn = compileToFn(listener || noop, 'listener'); var listenFn = compileToFn(listener || noop, 'listener');
...@@ -11252,7 +11383,7 @@ function $RootScopeProvider(){ ...@@ -11252,7 +11383,7 @@ function $RootScopeProvider(){
* *
* @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The * @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The
* expression value should evaluate to an object or an array which is observed on each * expression value should evaluate to an object or an array which is observed on each
* {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. Any shallow change within the
* collection will trigger a call to the `listener`. * collection will trigger a call to the `listener`.
* *
* @param {function(newCollection, oldCollection, scope)} listener a callback function that is * @param {function(newCollection, oldCollection, scope)} listener a callback function that is
...@@ -11357,9 +11488,9 @@ function $RootScopeProvider(){ ...@@ -11357,9 +11488,9 @@ function $RootScopeProvider(){
* @function * @function
* *
* @description * @description
* Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and * Processes all of the {@link ng.$rootScope.Scope#methods_$watch watchers} of the current scope and
* its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change * its children. Because a {@link ng.$rootScope.Scope#methods_$watch watcher}'s listener can change
* the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#methods_$watch watchers}
* until no more listeners are firing. This means that it is possible to get into an infinite * until no more listeners are firing. This means that it is possible to get into an infinite
* loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of
* iterations exceeds 10. * iterations exceeds 10.
...@@ -11367,12 +11498,12 @@ function $RootScopeProvider(){ ...@@ -11367,12 +11498,12 @@ function $RootScopeProvider(){
* Usually, you don't call `$digest()` directly in * Usually, you don't call `$digest()` directly in
* {@link ng.directive:ngController controllers} or in * {@link ng.directive:ngController controllers} or in
* {@link ng.$compileProvider#methods_directive directives}. * {@link ng.$compileProvider#methods_directive directives}.
* Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within * Instead, you should call {@link ng.$rootScope.Scope#methods_$apply $apply()} (typically from within
* a {@link ng.$compileProvider#methods_directive directives}), which will force a `$digest()`. * a {@link ng.$compileProvider#methods_directive directives}), which will force a `$digest()`.
* *
* If you want to be notified whenever `$digest()` is called, * If you want to be notified whenever `$digest()` is called,
* you can register a `watchExpression` function with * you can register a `watchExpression` function with
* {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. * {@link ng.$rootScope.Scope#methods_$watch $watch()} with no `listener`.
* *
* In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * In unit tests, you may need to call `$digest()` to simulate the scope life cycle.
* *
...@@ -11411,6 +11542,8 @@ function $RootScopeProvider(){ ...@@ -11411,6 +11542,8 @@ function $RootScopeProvider(){
beginPhase('$digest'); beginPhase('$digest');
lastDirtyWatch = null;
do { // "while dirty" loop do { // "while dirty" loop
dirty = false; dirty = false;
current = target; current = target;
...@@ -11420,10 +11553,13 @@ function $RootScopeProvider(){ ...@@ -11420,10 +11553,13 @@ function $RootScopeProvider(){
asyncTask = asyncQueue.shift(); asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression); asyncTask.scope.$eval(asyncTask.expression);
} catch (e) { } catch (e) {
clearPhase();
$exceptionHandler(e); $exceptionHandler(e);
} }
lastDirtyWatch = null;
} }
traverseScopesLoop:
do { // "traverse the scopes" loop do { // "traverse the scopes" loop
if ((watchers = current.$$watchers)) { if ((watchers = current.$$watchers)) {
// process our watches // process our watches
...@@ -11433,12 +11569,14 @@ function $RootScopeProvider(){ ...@@ -11433,12 +11569,14 @@ function $RootScopeProvider(){
watch = watchers[length]; watch = watchers[length];
// Most common watches are on primitives, in which case we can short // Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals // circuit it with === operator, only when === fails do we use .equals
if (watch && (value = watch.get(current)) !== (last = watch.last) && if (watch) {
if ((value = watch.get(current)) !== (last = watch.last) &&
!(watch.eq !(watch.eq
? equals(value, last) ? equals(value, last)
: (typeof value == 'number' && typeof last == 'number' : (typeof value == 'number' && typeof last == 'number'
&& isNaN(value) && isNaN(last)))) { && isNaN(value) && isNaN(last)))) {
dirty = true; dirty = true;
lastDirtyWatch = watch;
watch.last = watch.eq ? copy(value) : value; watch.last = watch.eq ? copy(value) : value;
watch.fn(value, ((last === initWatchVal) ? value : last), current); watch.fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) { if (ttl < 5) {
...@@ -11450,8 +11588,15 @@ function $RootScopeProvider(){ ...@@ -11450,8 +11588,15 @@ function $RootScopeProvider(){
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
watchLog[logIdx].push(logMsg); watchLog[logIdx].push(logMsg);
} }
} else if (watch === lastDirtyWatch) {
// If the most recently dirty watcher is now clean, short circuit since the remaining watchers
// have already been tested.
dirty = false;
break traverseScopesLoop;
}
} }
} catch (e) { } catch (e) {
clearPhase();
$exceptionHandler(e); $exceptionHandler(e);
} }
} }
...@@ -11460,13 +11605,16 @@ function $RootScopeProvider(){ ...@@ -11460,13 +11605,16 @@ function $RootScopeProvider(){
// Insanity Warning: scope depth-first traversal // Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it! // yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast // this piece should be kept in sync with the traversal in $broadcast
if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) { while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent; current = current.$parent;
} }
} }
} while ((current = next)); } while ((current = next));
// `break traverseScopesLoop;` takes us to here
if(dirty && !(ttl--)) { if(dirty && !(ttl--)) {
clearPhase(); clearPhase();
throw $rootScopeMinErr('infdig', throw $rootScopeMinErr('infdig',
...@@ -11474,6 +11622,7 @@ function $RootScopeProvider(){ ...@@ -11474,6 +11622,7 @@ function $RootScopeProvider(){
'Watchers fired in the last 5 iterations: {1}', 'Watchers fired in the last 5 iterations: {1}',
TTL, toJson(watchLog)); TTL, toJson(watchLog));
} }
} while (dirty || asyncQueue.length); } while (dirty || asyncQueue.length);
clearPhase(); clearPhase();
...@@ -11509,7 +11658,7 @@ function $RootScopeProvider(){ ...@@ -11509,7 +11658,7 @@ function $RootScopeProvider(){
* *
* @description * @description
* Removes the current scope (and all of its children) from the parent scope. Removal implies * Removes the current scope (and all of its children) from the parent scope. Removal implies
* that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer * that calls to {@link ng.$rootScope.Scope#methods_$digest $digest()} will no longer
* propagate to the current scope and its children. Removal also implies that the current * propagate to the current scope and its children. Removal also implies that the current
* scope is eligible for garbage collection. * scope is eligible for garbage collection.
* *
...@@ -11526,11 +11675,12 @@ function $RootScopeProvider(){ ...@@ -11526,11 +11675,12 @@ function $RootScopeProvider(){
*/ */
$destroy: function() { $destroy: function() {
// we can't destroy the root scope or a scope that has been already destroyed // we can't destroy the root scope or a scope that has been already destroyed
if ($rootScope == this || this.$$destroyed) return; if (this.$$destroyed) return;
var parent = this.$parent; var parent = this.$parent;
this.$broadcast('$destroy'); this.$broadcast('$destroy');
this.$$destroyed = true; this.$$destroyed = true;
if (this === $rootScope) return;
if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
...@@ -11590,7 +11740,7 @@ function $RootScopeProvider(){ ...@@ -11590,7 +11740,7 @@ function $RootScopeProvider(){
* *
* - it will execute after the function that scheduled the evaluation (preferably before DOM * - it will execute after the function that scheduled the evaluation (preferably before DOM
* rendering). * rendering).
* - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after * - at least one {@link ng.$rootScope.Scope#methods_$digest $digest cycle} will be performed after
* `expression` execution. * `expression` execution.
* *
* Any exceptions from the execution of the expression are forwarded to the * Any exceptions from the execution of the expression are forwarded to the
...@@ -11635,7 +11785,7 @@ function $RootScopeProvider(){ ...@@ -11635,7 +11785,7 @@ function $RootScopeProvider(){
* framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries).
* Because we are calling into the angular framework we need to perform proper scope life * Because we are calling into the angular framework we need to perform proper scope life
* cycle of {@link ng.$exceptionHandler exception handling}, * cycle of {@link ng.$exceptionHandler exception handling},
* {@link ng.$rootScope.Scope#$digest executing watches}. * {@link ng.$rootScope.Scope#methods_$digest executing watches}.
* *
* ## Life cycle * ## Life cycle
* *
...@@ -11656,11 +11806,11 @@ function $RootScopeProvider(){ ...@@ -11656,11 +11806,11 @@ function $RootScopeProvider(){
* Scope's `$apply()` method transitions through the following stages: * Scope's `$apply()` method transitions through the following stages:
* *
* 1. The {@link guide/expression expression} is executed using the * 1. The {@link guide/expression expression} is executed using the
* {@link ng.$rootScope.Scope#$eval $eval()} method. * {@link ng.$rootScope.Scope#methods_$eval $eval()} method.
* 2. Any exceptions from the execution of the expression are forwarded to the * 2. Any exceptions from the execution of the expression are forwarded to the
* {@link ng.$exceptionHandler $exceptionHandler} service. * {@link ng.$exceptionHandler $exceptionHandler} service.
* 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the * 3. The {@link ng.$rootScope.Scope#methods_$watch watch} listeners are fired immediately after the
* expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. * expression was executed using the {@link ng.$rootScope.Scope#methods_$digest $digest()} method.
* *
* *
* @param {(string|function())=} exp An angular expression to be executed. * @param {(string|function())=} exp An angular expression to be executed.
...@@ -11694,7 +11844,7 @@ function $RootScopeProvider(){ ...@@ -11694,7 +11844,7 @@ function $RootScopeProvider(){
* @function * @function
* *
* @description * @description
* Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for * Listens on events of a given type. See {@link ng.$rootScope.Scope#methods_$emit $emit} for
* discussion of event life cycle. * discussion of event life cycle.
* *
* The event listener function format is: `function(event, args...)`. The `event` object * The event listener function format is: `function(event, args...)`. The `event` object
...@@ -11735,20 +11885,20 @@ function $RootScopeProvider(){ ...@@ -11735,20 +11885,20 @@ function $RootScopeProvider(){
* *
* @description * @description
* Dispatches an event `name` upwards through the scope hierarchy notifying the * Dispatches an event `name` upwards through the scope hierarchy notifying the
* registered {@link ng.$rootScope.Scope#$on} listeners. * registered {@link ng.$rootScope.Scope#methods_$on} listeners.
* *
* The event life cycle starts at the scope on which `$emit` was called. All * The event life cycle starts at the scope on which `$emit` was called. All
* {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * {@link ng.$rootScope.Scope#methods_$on listeners} listening for `name` event on this scope get
* notified. Afterwards, the event traverses upwards toward the root scope and calls all * notified. Afterwards, the event traverses upwards toward the root scope and calls all
* registered listeners along the way. The event will stop propagating if one of the listeners * registered listeners along the way. The event will stop propagating if one of the listeners
* cancels it. * cancels it.
* *
* Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * Any exception emitted from the {@link ng.$rootScope.Scope#methods_$on listeners} will be passed
* onto the {@link ng.$exceptionHandler $exceptionHandler} service. * onto the {@link ng.$exceptionHandler $exceptionHandler} service.
* *
* @param {string} name Event name to emit. * @param {string} name Event name to emit.
* @param {...*} args Optional set of arguments which will be passed onto the event listeners. * @param {...*} args Optional set of arguments which will be passed onto the event listeners.
* @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). * @return {Object} Event object (see {@link ng.$rootScope.Scope#methods_$on}).
*/ */
$emit: function(name, args) { $emit: function(name, args) {
var empty = [], var empty = [],
...@@ -11804,19 +11954,19 @@ function $RootScopeProvider(){ ...@@ -11804,19 +11954,19 @@ function $RootScopeProvider(){
* *
* @description * @description
* Dispatches an event `name` downwards to all child scopes (and their children) notifying the * Dispatches an event `name` downwards to all child scopes (and their children) notifying the
* registered {@link ng.$rootScope.Scope#$on} listeners. * registered {@link ng.$rootScope.Scope#methods_$on} listeners.
* *
* The event life cycle starts at the scope on which `$broadcast` was called. All * The event life cycle starts at the scope on which `$broadcast` was called. All
* {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * {@link ng.$rootScope.Scope#methods_$on listeners} listening for `name` event on this scope get
* notified. Afterwards, the event propagates to all direct and indirect scopes of the current * notified. Afterwards, the event propagates to all direct and indirect scopes of the current
* scope and calls all registered listeners along the way. The event cannot be canceled. * scope and calls all registered listeners along the way. The event cannot be canceled.
* *
* Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * Any exception emitted from the {@link ng.$rootScope.Scope#methods_$on listeners} will be passed
* onto the {@link ng.$exceptionHandler $exceptionHandler} service. * onto the {@link ng.$exceptionHandler $exceptionHandler} service.
* *
* @param {string} name Event name to broadcast. * @param {string} name Event name to broadcast.
* @param {...*} args Optional set of arguments which will be passed onto the event listeners. * @param {...*} args Optional set of arguments which will be passed onto the event listeners.
* @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} * @return {Object} Event object, see {@link ng.$rootScope.Scope#methods_$on}
*/ */
$broadcast: function(name, args) { $broadcast: function(name, args) {
var target = this, var target = this,
...@@ -11899,6 +12049,79 @@ function $RootScopeProvider(){ ...@@ -11899,6 +12049,79 @@ function $RootScopeProvider(){
}]; }];
} }
/**
* @description
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
function $$SanitizeUriProvider() {
var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//;
/**
* @description
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during a[href] sanitization.
*
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
*
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
this.aHrefSanitizationWhitelist = function(regexp) {
if (isDefined(regexp)) {
aHrefSanitizationWhitelist = regexp;
return this;
}
return aHrefSanitizationWhitelist;
};
/**
* @description
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during img[src] sanitization.
*
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
*
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into
* an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
this.imgSrcSanitizationWhitelist = function(regexp) {
if (isDefined(regexp)) {
imgSrcSanitizationWhitelist = regexp;
return this;
}
return imgSrcSanitizationWhitelist;
};
this.$get = function() {
return function sanitizeUri(uri, isImage) {
var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
var normalizedVal;
// NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case.
if (!msie || msie >= 8 ) {
normalizedVal = urlResolve(uri).href;
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
return 'unsafe:'+normalizedVal;
}
}
return uri;
};
};
}
var $sceMinErr = minErr('$sce'); var $sceMinErr = minErr('$sce');
var SCE_CONTEXTS = { var SCE_CONTEXTS = {
...@@ -12098,8 +12321,7 @@ function $SceDelegateProvider() { ...@@ -12098,8 +12321,7 @@ function $SceDelegateProvider() {
return resourceUrlBlacklist; return resourceUrlBlacklist;
}; };
this.$get = ['$log', '$document', '$injector', function( this.$get = ['$injector', function($injector) {
$log, $document, $injector) {
var htmlSanitizer = function htmlSanitizer(html) { var htmlSanitizer = function htmlSanitizer(html) {
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
...@@ -12630,19 +12852,16 @@ function $SceProvider() { ...@@ -12630,19 +12852,16 @@ function $SceProvider() {
* sce.js and sceSpecs.js would need to be aware of this detail. * sce.js and sceSpecs.js would need to be aware of this detail.
*/ */
this.$get = ['$parse', '$document', '$sceDelegate', function( this.$get = ['$parse', '$sniffer', '$sceDelegate', function(
$parse, $document, $sceDelegate) { $parse, $sniffer, $sceDelegate) {
// Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows
// the "expression(javascript expression)" syntax which is insecure. // the "expression(javascript expression)" syntax which is insecure.
if (enabled && msie) { if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) {
var documentMode = $document[0].documentMode;
if (documentMode !== undefined && documentMode < 8) {
throw $sceMinErr('iequirks', throw $sceMinErr('iequirks',
'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' +
'mode. You can fix this by adding the text <!doctype html> to the top of your HTML ' + 'mode. You can fix this by adding the text <!doctype html> to the top of your HTML ' +
'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); 'document. See http://docs.angularjs.org/api/ng.$sce for more information.');
} }
}
var sce = copy(SCE_CONTEXTS); var sce = copy(SCE_CONTEXTS);
...@@ -13003,6 +13222,7 @@ function $SnifferProvider() { ...@@ -13003,6 +13222,7 @@ function $SnifferProvider() {
int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent), boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
document = $document[0] || {}, document = $document[0] || {},
documentMode = document.documentMode,
vendorPrefix, vendorPrefix,
vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/,
bodyStyle = document.body && document.body.style, bodyStyle = document.body && document.body.style,
...@@ -13047,7 +13267,7 @@ function $SnifferProvider() { ...@@ -13047,7 +13267,7 @@ function $SnifferProvider() {
// jshint +W018 // jshint +W018
hashchange: 'onhashchange' in $window && hashchange: 'onhashchange' in $window &&
// IE8 compatible mode lies // IE8 compatible mode lies
(!document.documentMode || document.documentMode > 7), (!documentMode || documentMode > 7),
hasEvent: function(event) { hasEvent: function(event) {
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
// it. In particular the event is not fired when backspace or delete key are pressed or // it. In particular the event is not fired when backspace or delete key are pressed or
...@@ -13065,7 +13285,8 @@ function $SnifferProvider() { ...@@ -13065,7 +13285,8 @@ function $SnifferProvider() {
vendorPrefix: vendorPrefix, vendorPrefix: vendorPrefix,
transitions : transitions, transitions : transitions,
animations : animations, animations : animations,
msie : msie msie : msie,
msieDocumentMode: documentMode
}; };
}]; }];
} }
...@@ -13250,11 +13471,6 @@ function $TimeoutProvider() { ...@@ -13250,11 +13471,6 @@ function $TimeoutProvider() {
// exactly the behavior needed here. There is little value is mocking these out for this // exactly the behavior needed here. There is little value is mocking these out for this
// service. // service.
var urlParsingNode = document.createElement("a"); var urlParsingNode = document.createElement("a");
/*
Matches paths for file protocol on windows,
such as /C:/foo/bar, and captures only /foo/bar.
*/
var windowsFilePathExp = /^\/?.*?:(\/.*)/;
var originUrl = urlResolve(window.location.href, true); var originUrl = urlResolve(window.location.href, true);
...@@ -13311,8 +13527,7 @@ var originUrl = urlResolve(window.location.href, true); ...@@ -13311,8 +13527,7 @@ var originUrl = urlResolve(window.location.href, true);
* *
*/ */
function urlResolve(url, base) { function urlResolve(url, base) {
var href = url, var href = url;
pathname;
if (msie) { if (msie) {
// Normalize before parse. Refer Implementation Notes on why this is // Normalize before parse. Refer Implementation Notes on why this is
...@@ -13323,21 +13538,6 @@ function urlResolve(url, base) { ...@@ -13323,21 +13538,6 @@ function urlResolve(url, base) {
urlParsingNode.setAttribute('href', href); urlParsingNode.setAttribute('href', href);
/*
* In Windows, on an anchor node on documents loaded from
* the filesystem, the browser will return a pathname
* prefixed with the drive name ('/C:/path') when a
* pathname without a drive is set:
* * a.setAttribute('href', '/foo')
* * a.pathname === '/C:/foo' //true
*
* Inside of Angular, we're always using pathnames that
* do not include drive names for routing.
*/
pathname = removeWindowsDriveName(urlParsingNode.pathname, url, base);
pathname = (pathname.charAt(0) === '/') ? pathname : '/' + pathname;
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
return { return {
href: urlParsingNode.href, href: urlParsingNode.href,
...@@ -13347,11 +13547,12 @@ function urlResolve(url, base) { ...@@ -13347,11 +13547,12 @@ function urlResolve(url, base) {
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
hostname: urlParsingNode.hostname, hostname: urlParsingNode.hostname,
port: urlParsingNode.port, port: urlParsingNode.port,
pathname: pathname pathname: (urlParsingNode.pathname.charAt(0) === '/')
? urlParsingNode.pathname
: '/' + urlParsingNode.pathname
}; };
} }
/** /**
* Parse a request URL and determine whether this is a same-origin request as the application document. * Parse a request URL and determine whether this is a same-origin request as the application document.
* *
...@@ -13365,26 +13566,6 @@ function urlIsSameOrigin(requestUrl) { ...@@ -13365,26 +13566,6 @@ function urlIsSameOrigin(requestUrl) {
parsed.host === originUrl.host); parsed.host === originUrl.host);
} }
function removeWindowsDriveName (path, url, base) {
var firstPathSegmentMatch;
//Get the relative path from the input URL.
if (url.indexOf(base) === 0) {
url = url.replace(base, '');
}
/*
* The input URL intentionally contains a
* first path segment that ends with a colon.
*/
if (windowsFilePathExp.exec(url)) {
return path;
}
firstPathSegmentMatch = windowsFilePathExp.exec(path);
return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path;
}
/** /**
* @ngdoc object * @ngdoc object
* @name ng.$window * @name ng.$window
...@@ -13405,13 +13586,15 @@ function removeWindowsDriveName (path, url, base) { ...@@ -13405,13 +13586,15 @@ function removeWindowsDriveName (path, url, base) {
<doc:source> <doc:source>
<script> <script>
function Ctrl($scope, $window) { function Ctrl($scope, $window) {
$scope.$window = $window;
$scope.greeting = 'Hello, World!'; $scope.greeting = 'Hello, World!';
$scope.doGreeting = function(greeting) {
$window.alert(greeting);
};
} }
</script> </script>
<div ng-controller="Ctrl"> <div ng-controller="Ctrl">
<input type="text" ng-model="greeting" /> <input type="text" ng-model="greeting" />
<button ng-click="$window.alert(greeting)">ALERT</button> <button ng-click="doGreeting(greeting)">ALERT</button>
</div> </div>
</doc:source> </doc:source>
<doc:scenario> <doc:scenario>
...@@ -14520,6 +14703,7 @@ var htmlAnchorDirective = valueFn({ ...@@ -14520,6 +14703,7 @@ var htmlAnchorDirective = valueFn({
element.append(document.createComment('IE fix')); element.append(document.createComment('IE fix'));
} }
if (!attr.href && !attr.name) {
return function(scope, element) { return function(scope, element) {
element.on('click', function(event){ element.on('click', function(event){
// if we have no href url, then don't navigate anywhere. // if we have no href url, then don't navigate anywhere.
...@@ -14529,6 +14713,7 @@ var htmlAnchorDirective = valueFn({ ...@@ -14529,6 +14713,7 @@ var htmlAnchorDirective = valueFn({
}); });
}; };
} }
}
}); });
/** /**
...@@ -14680,8 +14865,11 @@ var htmlAnchorDirective = valueFn({ ...@@ -14680,8 +14865,11 @@ var htmlAnchorDirective = valueFn({
* *
* The HTML specification does not require browsers to preserve the values of boolean attributes * The HTML specification does not require browsers to preserve the values of boolean attributes
* such as disabled. (Their presence means true and their absence means false.) * such as disabled. (Their presence means true and their absence means false.)
* This prevents the Angular compiler from retrieving the binding expression. * If we put an Angular interpolation expression into such an attribute then the
* binding information would be lost when the browser removes the attribute.
* The `ngDisabled` directive solves this problem for the `disabled` attribute. * The `ngDisabled` directive solves this problem for the `disabled` attribute.
* This complementary directive is not removed by the browser and so provides
* a permanent reliable place to store the binding information.
* *
* @example * @example
<doc:example> <doc:example>
...@@ -14712,8 +14900,11 @@ var htmlAnchorDirective = valueFn({ ...@@ -14712,8 +14900,11 @@ var htmlAnchorDirective = valueFn({
* @description * @description
* The HTML specification does not require browsers to preserve the values of boolean attributes * The HTML specification does not require browsers to preserve the values of boolean attributes
* such as checked. (Their presence means true and their absence means false.) * such as checked. (Their presence means true and their absence means false.)
* This prevents the Angular compiler from retrieving the binding expression. * If we put an Angular interpolation expression into such an attribute then the
* binding information would be lost when the browser removes the attribute.
* The `ngChecked` directive solves this problem for the `checked` attribute. * The `ngChecked` directive solves this problem for the `checked` attribute.
* This complementary directive is not removed by the browser and so provides
* a permanent reliable place to store the binding information.
* @example * @example
<doc:example> <doc:example>
<doc:source> <doc:source>
...@@ -14743,8 +14934,12 @@ var htmlAnchorDirective = valueFn({ ...@@ -14743,8 +14934,12 @@ var htmlAnchorDirective = valueFn({
* @description * @description
* The HTML specification does not require browsers to preserve the values of boolean attributes * The HTML specification does not require browsers to preserve the values of boolean attributes
* such as readonly. (Their presence means true and their absence means false.) * such as readonly. (Their presence means true and their absence means false.)
* This prevents the Angular compiler from retrieving the binding expression. * If we put an Angular interpolation expression into such an attribute then the
* binding information would be lost when the browser removes the attribute.
* The `ngReadonly` directive solves this problem for the `readonly` attribute. * The `ngReadonly` directive solves this problem for the `readonly` attribute.
* This complementary directive is not removed by the browser and so provides
* a permanent reliable place to store the binding information.
* @example * @example
<doc:example> <doc:example>
<doc:source> <doc:source>
...@@ -14774,8 +14969,11 @@ var htmlAnchorDirective = valueFn({ ...@@ -14774,8 +14969,11 @@ var htmlAnchorDirective = valueFn({
* @description * @description
* The HTML specification does not require browsers to preserve the values of boolean attributes * The HTML specification does not require browsers to preserve the values of boolean attributes
* such as selected. (Their presence means true and their absence means false.) * such as selected. (Their presence means true and their absence means false.)
* This prevents the Angular compiler from retrieving the binding expression. * If we put an Angular interpolation expression into such an attribute then the
* binding information would be lost when the browser removes the attribute.
* The `ngSelected` directive solves this problem for the `selected` atttribute. * The `ngSelected` directive solves this problem for the `selected` atttribute.
* This complementary directive is not removed by the browser and so provides
* a permanent reliable place to store the binding information.
* @example * @example
<doc:example> <doc:example>
<doc:source> <doc:source>
...@@ -14807,8 +15005,12 @@ var htmlAnchorDirective = valueFn({ ...@@ -14807,8 +15005,12 @@ var htmlAnchorDirective = valueFn({
* @description * @description
* The HTML specification does not require browsers to preserve the values of boolean attributes * The HTML specification does not require browsers to preserve the values of boolean attributes
* such as open. (Their presence means true and their absence means false.) * such as open. (Their presence means true and their absence means false.)
* This prevents the Angular compiler from retrieving the binding expression. * If we put an Angular interpolation expression into such an attribute then the
* binding information would be lost when the browser removes the attribute.
* The `ngOpen` directive solves this problem for the `open` attribute. * The `ngOpen` directive solves this problem for the `open` attribute.
* This complementary directive is not removed by the browser and so provides
* a permanent reliable place to store the binding information.
* *
* @example * @example
<doc:example> <doc:example>
...@@ -14901,8 +15103,21 @@ var nullFormCtrl = { ...@@ -14901,8 +15103,21 @@ var nullFormCtrl = {
* @property {Object} $error Is an object hash, containing references to all invalid controls or * @property {Object} $error Is an object hash, containing references to all invalid controls or
* forms, where: * forms, where:
* *
* - keys are validation tokens (error names) — such as `required`, `url` or `email`, * - keys are validation tokens (error names),
* - values are arrays of controls or forms that are invalid with given error. * - values are arrays of controls or forms that are invalid for given error name.
*
*
* Built-in validation tokens:
*
* - `email`
* - `max`
* - `maxlength`
* - `min`
* - `minlength`
* - `number`
* - `pattern`
* - `required`
* - `url`
* *
* @description * @description
* `FormController` keeps track of all its controls and nested forms as well as state of them, * `FormController` keeps track of all its controls and nested forms as well as state of them,
...@@ -15638,8 +15853,21 @@ var inputType = { ...@@ -15638,8 +15853,21 @@ var inputType = {
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// In composition mode, users are still inputing intermediate text buffer,
// hold the listener until composition is done.
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
var composing = false;
element.on('compositionstart', function() {
composing = true;
});
element.on('compositionend', function() {
composing = false;
});
var listener = function() { var listener = function() {
if (composing) return;
var value = element.val(); var value = element.val();
// By default we will trim the value // By default we will trim the value
...@@ -15682,15 +15910,15 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { ...@@ -15682,15 +15910,15 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
deferListener(); deferListener();
}); });
// if user paste into input using mouse, we need "change" event to catch it
element.on('change', listener);
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
if ($sniffer.hasEvent('paste')) { if ($sniffer.hasEvent('paste')) {
element.on('paste cut', deferListener); element.on('paste cut', deferListener);
} }
} }
// if user paste into input using mouse on older browser
// or form autocomplete on newer browser, we need "change" event to catch it
element.on('change', listener);
ctrl.$render = function() { ctrl.$render = function() {
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
...@@ -16184,39 +16412,6 @@ var VALID_CLASS = 'ng-valid', ...@@ -16184,39 +16412,6 @@ var VALID_CLASS = 'ng-valid',
</file> </file>
* </example> * </example>
* *
* ## Isolated Scope Pitfall
*
* Note that if you have a directive with an isolated scope, you cannot require `ngModel`
* since the model value will be looked up on the isolated scope rather than the outer scope.
* When the directive updates the model value, calling `ngModel.$setViewValue()` the property
* on the outer scope will not be updated. However you can get around this by using $parent.
*
* Here is an example of this situation. You'll notice that the first div is not updating the input.
* However the second div can update the input properly.
*
* <example module="badIsolatedDirective">
<file name="script.js">
angular.module('badIsolatedDirective', []).directive('isolate', function() {
return {
require: 'ngModel',
scope: { },
template: '<input ng-model="innerModel">',
link: function(scope, element, attrs, ngModel) {
scope.$watch('innerModel', function(value) {
console.log(value);
ngModel.$setViewValue(value);
});
}
};
});
</file>
<file name="index.html">
<input ng-model="someModel"/>
<div isolate ng-model="someModel"></div>
<div isolate ng-model="$parent.someModel"></div>
</file>
* </example>
*
* *
*/ */
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
...@@ -16420,6 +16615,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ...@@ -16420,6 +16615,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$render(); ctrl.$render();
} }
} }
return value;
}); });
}]; }];
...@@ -16696,7 +16893,6 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; ...@@ -16696,7 +16893,6 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
id="{{name}}" id="{{name}}"
name="favorite"> name="favorite">
</label> </label>
</span>
<div>You chose {{my.favorite}}</div> <div>You chose {{my.favorite}}</div>
</form> </form>
</doc:source> </doc:source>
...@@ -16929,11 +17125,10 @@ function classDirective(name, selector) { ...@@ -16929,11 +17125,10 @@ function classDirective(name, selector) {
// jshint bitwise: false // jshint bitwise: false
var mod = $index & 1; var mod = $index & 1;
if (mod !== old$index & 1) { if (mod !== old$index & 1) {
if (mod === selector) { var classes = flattenClasses(scope.$eval(attr[name]));
addClass(scope.$eval(attr[name])); mod === selector ?
} else { attr.$addClass(classes) :
removeClass(scope.$eval(attr[name])); attr.$removeClass(classes);
}
} }
}); });
} }
...@@ -16941,24 +17136,17 @@ function classDirective(name, selector) { ...@@ -16941,24 +17136,17 @@ function classDirective(name, selector) {
function ngClassWatchAction(newVal) { function ngClassWatchAction(newVal) {
if (selector === true || scope.$index % 2 === selector) { if (selector === true || scope.$index % 2 === selector) {
if (oldVal && !equals(newVal,oldVal)) { var newClasses = flattenClasses(newVal || '');
removeClass(oldVal); if(!oldVal) {
attr.$addClass(newClasses);
} else if(!equals(newVal,oldVal)) {
attr.$updateClass(newClasses, flattenClasses(oldVal));
} }
addClass(newVal);
} }
oldVal = copy(newVal); oldVal = copy(newVal);
} }
function removeClass(classVal) {
attr.$removeClass(flattenClasses(classVal));
}
function addClass(classVal) {
attr.$addClass(flattenClasses(classVal));
}
function flattenClasses(classVal) { function flattenClasses(classVal) {
if(isArray(classVal)) { if(isArray(classVal)) {
return classVal.join(' '); return classVal.join(' ');
...@@ -17436,7 +17624,8 @@ var ngCloakDirective = ngDirective({ ...@@ -17436,7 +17624,8 @@ var ngCloakDirective = ngDirective({
var ngControllerDirective = [function() { var ngControllerDirective = [function() {
return { return {
scope: true, scope: true,
controller: '@' controller: '@',
priority: 500
}; };
}]; }];
...@@ -17920,9 +18109,12 @@ var ngIfDirective = ['$animate', function($animate) { ...@@ -17920,9 +18109,12 @@ var ngIfDirective = ['$animate', function($animate) {
if (!childScope) { if (!childScope) {
childScope = $scope.$new(); childScope = $scope.$new();
$transclude(childScope, function (clone) { $transclude(childScope, function (clone) {
clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when it's template arrives.
block = { block = {
startNode: clone[0], clone: clone
endNode: clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ')
}; };
$animate.enter(clone, $element.parent(), $element); $animate.enter(clone, $element.parent(), $element);
}); });
...@@ -17935,7 +18127,7 @@ var ngIfDirective = ['$animate', function($animate) { ...@@ -17935,7 +18127,7 @@ var ngIfDirective = ['$animate', function($animate) {
} }
if (block) { if (block) {
$animate.leave(getBlockElements(block)); $animate.leave(getBlockElements(block.clone));
block = null; block = null;
} }
} }
...@@ -18091,13 +18283,14 @@ var ngIfDirective = ['$animate', function($animate) { ...@@ -18091,13 +18283,14 @@ var ngIfDirective = ['$animate', function($animate) {
* @description * @description
* Emitted every time the ngInclude content is reloaded. * Emitted every time the ngInclude content is reloaded.
*/ */
var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animate', '$sce', var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce',
function($http, $templateCache, $anchorScroll, $compile, $animate, $sce) { function($http, $templateCache, $anchorScroll, $animate, $sce) {
return { return {
restrict: 'ECA', restrict: 'ECA',
priority: 400, priority: 400,
terminal: true, terminal: true,
transclude: 'element', transclude: 'element',
controller: angular.noop,
compile: function(element, attr) { compile: function(element, attr) {
var srcExp = attr.ngInclude || attr.src, var srcExp = attr.ngInclude || attr.src,
onloadExp = attr.onload || '', onloadExp = attr.onload || '',
...@@ -18131,25 +18324,31 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' ...@@ -18131,25 +18324,31 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
$http.get(src, {cache: $templateCache}).success(function(response) { $http.get(src, {cache: $templateCache}).success(function(response) {
if (thisChangeId !== changeCounter) return; if (thisChangeId !== changeCounter) return;
var newScope = scope.$new(); var newScope = scope.$new();
ctrl.template = response;
$transclude(newScope, function(clone) {
// Note: This will also link all children of ng-include that were contained in the original
// html. If that content contains controllers, ... they could pollute/change the scope.
// However, using ng-include on an element with additional content does not make sense...
// Note: We can't remove them in the cloneAttchFn of $transclude as that
// function is called before linking the content, which would apply child
// directives to non existing elements.
var clone = $transclude(newScope, function(clone) {
cleanupLastIncludeContent(); cleanupLastIncludeContent();
$animate.enter(clone, null, $element, afterAnimation);
});
currentScope = newScope; currentScope = newScope;
currentElement = clone; currentElement = clone;
currentElement.html(response);
$animate.enter(currentElement, null, $element, afterAnimation);
$compile(currentElement.contents())(currentScope);
currentScope.$emit('$includeContentLoaded'); currentScope.$emit('$includeContentLoaded');
scope.$eval(onloadExp); scope.$eval(onloadExp);
});
}).error(function() { }).error(function() {
if (thisChangeId === changeCounter) cleanupLastIncludeContent(); if (thisChangeId === changeCounter) cleanupLastIncludeContent();
}); });
scope.$emit('$includeContentRequested'); scope.$emit('$includeContentRequested');
} else { } else {
cleanupLastIncludeContent(); cleanupLastIncludeContent();
ctrl.template = null;
} }
}); });
}; };
...@@ -18157,6 +18356,24 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' ...@@ -18157,6 +18356,24 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
}; };
}]; }];
// This directive is called during the $transclude call of the first `ngInclude` directive.
// It will replace and compile the content of the element with the loaded template.
// We need this directive so that the element content is already filled when
// the link function of another directive on the same element as ngInclude
// is called.
var ngIncludeFillContentDirective = ['$compile',
function($compile) {
return {
restrict: 'ECA',
priority: -400,
require: 'ngInclude',
link: function(scope, $element, $attr, ctrl) {
$element.html(ctrl.template);
$compile($element.contents())(scope);
}
};
}];
/** /**
* @ngdoc directive * @ngdoc directive
* @name ng.directive:ngInit * @name ng.directive:ngInit
...@@ -18173,6 +18390,8 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' ...@@ -18173,6 +18390,8 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
* to initialize values on a scope. * to initialize values on a scope.
* </div> * </div>
* *
* @priority 450
*
* @element ANY * @element ANY
* @param {expression} ngInit {@link guide/expression Expression} to eval. * @param {expression} ngInit {@link guide/expression Expression} to eval.
* *
...@@ -18204,6 +18423,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' ...@@ -18204,6 +18423,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
</doc:example> </doc:example>
*/ */
var ngInitDirective = ngDirective({ var ngInitDirective = ngDirective({
priority: 450,
compile: function() { compile: function() {
return { return {
pre: function(scope, element, attrs) { pre: function(scope, element, attrs) {
...@@ -18298,7 +18518,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); ...@@ -18298,7 +18518,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
* other numbers, for example 12, so that instead of showing "12 people are viewing", you can * other numbers, for example 12, so that instead of showing "12 people are viewing", you can
* show "a dozen people are viewing". * show "a dozen people are viewing".
* *
* You can use a set of closed braces(`{}`) as a placeholder for the number that you want substituted * You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted
* into pluralized strings. In the previous example, Angular will replace `{}` with * into pluralized strings. In the previous example, Angular will replace `{}` with
* <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder * <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder
* for <span ng-non-bindable>{{numberExpression}}</span>. * for <span ng-non-bindable>{{numberExpression}}</span>.
...@@ -18559,7 +18779,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp ...@@ -18559,7 +18779,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
* `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
* with the corresponding item in the array by identity. Moving the same object in array would move the DOM * with the corresponding item in the array by identity. Moving the same object in array would move the DOM
* element in the same way ian the DOM. * element in the same way in the DOM.
* *
* For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
* case the object identity does not matter. Two objects are considered equivalent as long as their `id` * case the object identity does not matter. Two objects are considered equivalent as long as their `id`
...@@ -18761,7 +18981,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { ...@@ -18761,7 +18981,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
} else if (nextBlockMap.hasOwnProperty(trackById)) { } else if (nextBlockMap.hasOwnProperty(trackById)) {
// restore lastBlockMap // restore lastBlockMap
forEach(nextBlockOrder, function(block) { forEach(nextBlockOrder, function(block) {
if (block && block.startNode) lastBlockMap[block.id] = block; if (block && block.scope) lastBlockMap[block.id] = block;
}); });
// This is a duplicate and we need to throw an error // This is a duplicate and we need to throw an error
throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}",
...@@ -18778,7 +18998,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { ...@@ -18778,7 +18998,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
// lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn
if (lastBlockMap.hasOwnProperty(key)) { if (lastBlockMap.hasOwnProperty(key)) {
block = lastBlockMap[key]; block = lastBlockMap[key];
elementsToRemove = getBlockElements(block); elementsToRemove = getBlockElements(block.clone);
$animate.leave(elementsToRemove); $animate.leave(elementsToRemove);
forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; });
block.scope.$destroy(); block.scope.$destroy();
...@@ -18790,9 +19010,9 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { ...@@ -18790,9 +19010,9 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
key = (collection === collectionKeys) ? index : collectionKeys[index]; key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key]; value = collection[key];
block = nextBlockOrder[index]; block = nextBlockOrder[index];
if (nextBlockOrder[index - 1]) previousNode = nextBlockOrder[index - 1].endNode; if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]);
if (block.startNode) { if (block.scope) {
// if we have already seen this object, then we need to reuse the // if we have already seen this object, then we need to reuse the
// associated scope/element // associated scope/element
childScope = block.scope; childScope = block.scope;
...@@ -18802,11 +19022,11 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { ...@@ -18802,11 +19022,11 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
nextNode = nextNode.nextSibling; nextNode = nextNode.nextSibling;
} while(nextNode && nextNode[NG_REMOVED]); } while(nextNode && nextNode[NG_REMOVED]);
if (block.startNode != nextNode) { if (getBlockStart(block) != nextNode) {
// existing item which got moved // existing item which got moved
$animate.move(getBlockElements(block), null, jqLite(previousNode)); $animate.move(getBlockElements(block.clone), null, jqLite(previousNode));
} }
previousNode = block.endNode; previousNode = getBlockEnd(block);
} else { } else {
// new item which we don't know about // new item which we don't know about
childScope = $scope.$new(); childScope = $scope.$new();
...@@ -18822,14 +19042,16 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { ...@@ -18822,14 +19042,16 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
childScope.$odd = !(childScope.$even = (index&1) === 0); childScope.$odd = !(childScope.$even = (index&1) === 0);
// jshint bitwise: true // jshint bitwise: true
if (!block.startNode) { if (!block.scope) {
$transclude(childScope, function(clone) { $transclude(childScope, function(clone) {
clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' ');
$animate.enter(clone, null, jqLite(previousNode)); $animate.enter(clone, null, jqLite(previousNode));
previousNode = clone; previousNode = clone;
block.scope = childScope; block.scope = childScope;
block.startNode = previousNode && previousNode.endNode ? previousNode.endNode : clone[0]; // Note: We only need the first/last node of the cloned nodes.
block.endNode = clone[clone.length - 1]; // However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when it's template arrives.
block.clone = clone;
nextBlockMap[block.id] = block; nextBlockMap[block.id] = block;
}); });
} }
...@@ -18838,6 +19060,14 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { ...@@ -18838,6 +19060,14 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
}); });
} }
}; };
function getBlockStart(block) {
return block.clone[0];
}
function getBlockEnd(block) {
return block.clone[block.clone.length - 1];
}
}]; }];
/** /**
...@@ -19190,19 +19420,26 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { ...@@ -19190,19 +19420,26 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
* @restrict EA * @restrict EA
* *
* @description * @description
* The ngSwitch directive is used to conditionally swap DOM structure on your template based on a scope expression. * 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 * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location
* as specified in the template. * as specified in the template.
* *
* The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it * 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 * 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 * 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** * (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 * (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 * 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 * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default
* attribute is displayed. * attribute is displayed.
* *
* <div class="alert alert-info">
* Be aware that the attribute values to match against cannot be expressions. They are interpreted
* as literal string values to match against.
* For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the
* value of the expression `$scope.someVal`.
* </div>
* @animations * @animations
* enter - happens after the ngSwitch contents change and the matched child element is placed inside the container * enter - happens after the ngSwitch 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 * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM
...@@ -19214,6 +19451,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { ...@@ -19214,6 +19451,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
* <ANY ng-switch-default>...</ANY> * <ANY ng-switch-default>...</ANY>
* </ANY> * </ANY>
* *
*
* @scope * @scope
* @priority 800 * @priority 800
* @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>. * @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>.
...@@ -19431,7 +19669,7 @@ var ngTranscludeDirective = ngDirective({ ...@@ -19431,7 +19669,7 @@ var ngTranscludeDirective = ngDirective({
link: function($scope, $element, $attrs, controller) { link: function($scope, $element, $attrs, controller) {
controller.$transclude(function(clone) { controller.$transclude(function(clone) {
$element.html(''); $element.empty();
$element.append(clone); $element.append(clone);
}); });
} }
...@@ -19815,13 +20053,13 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { ...@@ -19815,13 +20053,13 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
// becomes the compilation root // becomes the compilation root
nullOption.removeClass('ng-scope'); nullOption.removeClass('ng-scope');
// we need to remove it before calling selectElement.html('') because otherwise IE will // we need to remove it before calling selectElement.empty() because otherwise IE will
// remove the label from the element. wtf? // remove the label from the element. wtf?
nullOption.remove(); nullOption.remove();
} }
// clear contents, we'll add what's needed based on the model // clear contents, we'll add what's needed based on the model
selectElement.html(''); selectElement.empty();
selectElement.on('change', function() { selectElement.on('change', function() {
scope.$apply(function() { scope.$apply(function() {
...@@ -20128,4 +20366,4 @@ var styleDirective = valueFn({ ...@@ -20128,4 +20366,4 @@ var styleDirective = valueFn({
})(window, document); })(window, document);
!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}.ng-animate-start{clip:rect(0,auto,auto,0);-ms-zoom:1.0001;}.ng-animate-active{clip:rect(-1px,auto,auto,0);-ms-zoom:1;}</style>'); !angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}.ng-animate-start{border-spacing:1px 1px;-ms-zoom:1.0001;}.ng-animate-active{border-spacing:0px 0px;-ms-zoom:1;}</style>');
\ No newline at end of file \ No newline at end of file
<!doctype html> <!doctype html>
<html lang="en" ng-app="todomvc" data-framework="angularjs"> <html lang="en" data-framework="angularjs">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
...@@ -7,7 +7,10 @@ ...@@ -7,7 +7,10 @@
<link rel="stylesheet" href="bower_components/todomvc-common/base.css"> <link rel="stylesheet" href="bower_components/todomvc-common/base.css">
<style>[ng-cloak] { display: none; }</style> <style>[ng-cloak] { display: none; }</style>
</head> </head>
<body> <body ng-app="todomvc">
<ng-view />
<script type="text/ng-template" id="todomvc-index.html">
<section id="todoapp" ng-controller="TodoCtrl"> <section id="todoapp" ng-controller="TodoCtrl">
<header id="header"> <header id="header">
<h1>todos</h1> <h1>todos</h1>
...@@ -37,13 +40,13 @@ ...@@ -37,13 +40,13 @@
</span> </span>
<ul id="filters"> <ul id="filters">
<li> <li>
<a ng-class="{selected: location.path() == '/'} " href="#/">All</a> <a ng-class="{selected: status == ''} " href="#/">All</a>
</li> </li>
<li> <li>
<a ng-class="{selected: location.path() == '/active'}" href="#/active">Active</a> <a ng-class="{selected: status == 'active'}" href="#/active">Active</a>
</li> </li>
<li> <li>
<a ng-class="{selected: location.path() == '/completed'}" href="#/completed">Completed</a> <a ng-class="{selected: status == 'completed'}" href="#/completed">Completed</a>
</li> </li>
</ul> </ul>
<button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</button> <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</button>
...@@ -59,8 +62,10 @@ ...@@ -59,8 +62,10 @@
</p> </p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
</script>
<script src="bower_components/todomvc-common/base.js"></script> <script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/angular/angular.js"></script> <script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
<script src="js/controllers/todoCtrl.js"></script> <script src="js/controllers/todoCtrl.js"></script>
<script src="js/services/todoStorage.js"></script> <script src="js/services/todoStorage.js"></script>
......
...@@ -7,4 +7,15 @@ ...@@ -7,4 +7,15 @@
* *
* @type {angular.Module} * @type {angular.Module}
*/ */
var todomvc = angular.module('todomvc', []); var todomvc = angular.module('todomvc', ['ngRoute'])
.config(function ($routeProvider) {
$routeProvider.when('/', {
controller: 'TodoCtrl',
templateUrl: 'todomvc-index.html'
}).when('/:status', {
controller: 'TodoCtrl',
templateUrl: 'todomvc-index.html'
}).otherwise({
redirectTo: '/'
});
});
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
* - retrieves and persists the model via the todoStorage service * - retrieves and persists the model via the todoStorage service
* - exposes the model to the template and provides event handlers * - exposes the model to the template and provides event handlers
*/ */
todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, todoStorage, filterFilter) { todomvc.controller('TodoCtrl', function TodoCtrl($scope, $routeParams, todoStorage, filterFilter) {
var todos = $scope.todos = todoStorage.get(); var todos = $scope.todos = todoStorage.get();
$scope.newTodo = ''; $scope.newTodo = '';
...@@ -21,15 +21,12 @@ todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, todoStorage, ...@@ -21,15 +21,12 @@ todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, todoStorage,
} }
}, true); }, true);
if ($location.path() === '') { // Monitor the current route for changes and adjust the filter accordingly.
$location.path('/'); $scope.$on('$routeChangeSuccess', function () {
} var status = $scope.status = $routeParams.status || '';
$scope.location = $location;
$scope.$watch('location.path()', function (path) { $scope.statusFilter = (status === 'active') ?
$scope.statusFilter = (path === '/active') ? { completed: false } : (status === 'completed') ?
{ completed: false } : (path === '/completed') ?
{ completed: true } : null; { completed: true } : null;
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment