r218 - in sandbox/nuiton-js-angular: . src/main/resources/META-INF/nuiton-js src/main/resources/nuiton-js-angular src/main/resources/nuiton-js-angular/extra
Author: echatellier Date: 2013-10-15 10:29:48 +0200 (Tue, 15 Oct 2013) New Revision: 218 Url: http://nuiton.org/projects/nuiton-js/repository/revisions/218 Log: Angular js 1.2.0-rc3 (version bump) Modified: sandbox/nuiton-js-angular/pom.xml sandbox/nuiton-js-angular/src/main/resources/META-INF/nuiton-js/wro-angular.xml sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/angular.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-animate.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-cookies.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-loader.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-mocks.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-resource.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-route.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-sanitize.js sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-touch.js Modified: sandbox/nuiton-js-angular/pom.xml =================================================================== --- sandbox/nuiton-js-angular/pom.xml 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/pom.xml 2013-10-15 08:29:48 UTC (rev 218) @@ -14,7 +14,7 @@ </parent> <artifactId>nuiton-js-angular</artifactId> - <version>1.2.0-rc2-2-SNAPSHOT</version> + <version>1.2.0-rc3-1-SNAPSHOT</version> <name>Nuiton JS :: Angular</name> <description>Angular jar packaging</description> @@ -29,9 +29,9 @@ </licenses> <scm> - <connection>scm:svn:http://svn.nuiton.org/svn/nuiton-js/tags/nuiton-js-angular-1.2.0-rc2-2</connection> - <developerConnection>scm:svn:http://svn.nuiton.org/svn/nuiton-js/tags/nuiton-js-angular-1.2.0-rc2-2</developerConnection> - <url>http://www.nuiton.org/repositories/browse/nuiton-js/tags/nuiton-js-angular-1.2.0-rc2-2</url> + <connection>scm:svn:http://svn.nuiton.org/svn/nuiton-js/tags/nuiton-js-angular-1.2.0-rc3-1</connection> + <developerConnection>scm:svn:http://svn.nuiton.org/svn/nuiton-js/tags/nuiton-js-angular-1.2.0-rc3-1</developerConnection> + <url>http://www.nuiton.org/repositories/browse/nuiton-js/tags/nuiton-js-angular-1.2.0-rc3-1</url> </scm> </project> Modified: sandbox/nuiton-js-angular/src/main/resources/META-INF/nuiton-js/wro-angular.xml =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/META-INF/nuiton-js/wro-angular.xml 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/META-INF/nuiton-js/wro-angular.xml 2013-10-15 08:29:48 UTC (rev 218) @@ -1,6 +1,6 @@ <!-- #%L - Nuiton JS :: Mustache + Nuiton JS :: AngularJS $Id$ $HeadURL$ %% @@ -26,7 +26,7 @@ <js>classpath:nuiton-js-angular/angular.js</js> </group> - <group name='angular-animate'> + <group name='angular-animate'> <js>classpath:nuiton-js-angular/extra/angular-animate.js</js> </group> Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/angular.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/angular.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/angular.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -82,16 +82,6 @@ //////////////////////////////////// /** - * hasOwnProperty may be overwritten by a property of the same name, or entirely - * absent from an object that does not inherit Object.prototype; this copy is - * used instead - */ -var hasOwnPropertyFn = Object.prototype.hasOwnProperty; -var hasOwnPropertyLocal = function(obj, key) { - return hasOwnPropertyFn.call(obj, key); -}; - -/** * @ngdoc function * @name angular.lowercase * @function @@ -166,22 +156,21 @@ /** * @private * @param {*} obj - * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, ...) + * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, String ...) */ function isArrayLike(obj) { if (obj == null || isWindow(obj)) { return false; } - + var length = obj.length; if (obj.nodeType === 1 && length) { return true; } - return isArray(obj) || !isFunction(obj) && ( - length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj - ); + return isString(obj) || isArray(obj) || length === 0 || + typeof length === 'number' && length > 0 && (length - 1) in obj; } /** @@ -677,7 +666,8 @@ * * If no destination is supplied, a copy of the object or array is created. * * If a destination is provided, all of its elements (for array) or properties (for objects) * are deleted and then all elements/properties from the source are copied to it. - * * If `source` is not an object or array, `source` is returned. + * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. + * * If `source` is identical to 'destination' an exception will be thrown. * * Note: this function is used to augment the Object type in Angular expressions. See * {@link ng.$filter} for more information about Angular arrays. @@ -687,6 +677,42 @@ * @param {(Object|Array)=} destination Destination into which the source is copied. If * provided, must be of the same type as `source`. * @returns {*} The copy or updated `destination`, if `destination` was specified. + * + * @example + <doc:example> + <doc:source> + <div ng-controller="Controller"> + <form novalidate class="simple-form"> + Name: <input type="text" ng-model="user.name" /><br /> + E-mail: <input type="email" ng-model="user.email" /><br /> + Gender: <input type="radio" ng-model="user.gender" value="male" />male + <input type="radio" ng-model="user.gender" value="female" />female<br /> + <button ng-click="reset()">RESET</button> + <button ng-click="update(user)">SAVE</button> + </form> + <pre>form = {{user | json}}</pre> + <pre>master = {{master | json}}</pre> + </div> + + <script> + function Controller($scope) { + $scope.master= {}; + + $scope.update = function(user) { + // Example with 1 argument + $scope.master= angular.copy(user); + }; + + $scope.reset = function() { + // Example with 2 arguments + angular.copy($scope.master, $scope.user); + }; + + $scope.reset(); + } + </script> + </doc:source> + </doc:example> */ function copy(source, destination){ if (isWindow(source) || isScope(source)) { @@ -734,6 +760,8 @@ dst = dst || {}; for(var key in src) { + // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src + // so we don't need to worry hasOwnProperty here if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { dst[key] = src[key]; } @@ -756,7 +784,7 @@ * * * Both objects or values pass `===` comparison. * * Both objects or values are of the same type and all of their properties pass `===` comparison. - * * Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal) + * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) * * Both values represent the same regular expression (In JavasScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). @@ -828,7 +856,8 @@ * @description * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for * `fn`). You can supply optional `args` that are prebound to the function. This feature is also - * known as [function currying](http://en.wikipedia.org/wiki/Currying). + * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as distinguished + * from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_applica...). * * @param {Object} self Context which `fn` should be evaluated in. * @param {function()} fn Function to be bound. @@ -859,7 +888,7 @@ function toJsonReplacer(key, value) { var val = value; - if (/^\$+/.test(key)) { + if (typeof key === 'string' && key.charAt(0) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; @@ -1063,7 +1092,7 @@ * HTML document you must manually bootstrap them using {@link angular.bootstrap}. * Applications cannot be nested. * - * In the example below if the `ngApp` directive would not be placed + * In the example below if the `ngApp` directive were not placed * on the `html` element then the document would not be compiled * and the `{{ 1+2 }}` would not be resolved to `3`. * @@ -1132,7 +1161,9 @@ * They must use {@link api/ng.directive:ngApp ngApp}. * * @param {Element} element DOM element which is the root of angular application. - * @param {Array<String|Function>=} modules an array of module declarations. See: {@link angular.module modules} + * @param {Array<String|Function|Array>=} modules an array of modules to load into the application. + * Each item in the array should be the name of a predefined module or a (DI annotated) + * function that will be invoked by the injector as a run block. See: {@link angular.module modules} * @returns {AUTO.$injector} Returns the newly created injector for this app. */ function bootstrap(element, modules) { @@ -1228,6 +1259,17 @@ } /** + * throw error if the name given is hasOwnProperty + * @param {String} name the name to test + * @param {String} context the context in which the name is used, such as module or directive + */ +function assertNotHasOwnProperty(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + } +} + +/** * Return the value accessible from the object by path. Any undefined traversals are ignored * @param {Object} obj starting object * @param {string} path path to traverse @@ -1264,6 +1306,8 @@ function setupModuleLoader(window) { + var $injectorMinErr = minErr('$injector'); + function ensure(obj, name, factory) { return obj[name] || (obj[name] = factory()); } @@ -1322,12 +1366,13 @@ * @returns {module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + assertNotHasOwnProperty(name, 'module'); if (requires && modules.hasOwnProperty(name)) { modules[name] = null; } return ensure(modules, name, function() { if (!requires) { - throw minErr('$injector')('nomod', "Module '{0}' is not available! You either misspelled the module name " + + throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled the module name " + "or forgot to load it. If registering a module ensure that you specify the dependencies as the second " + "argument.", name); } @@ -1430,7 +1475,7 @@ * @param {Function} animationFactory Factory function for creating new instance of an animation. * @description * - * **NOTE**: animations are take effect only if the **ngAnimate** module is loaded. + * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. * * * Defines an animation hook that can be later used with {@link ngAnimate.$animate $animate} service and @@ -1470,7 +1515,8 @@ * @ngdoc method * @name angular.Module#controller * @methodOf angular.Module - * @param {string} name Controller name. + * @param {string|Object} name Controller name, or an object map of controllers where the + * keys are the names and the values are the constructors. * @param {Function} constructor Controller constructor function. * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. @@ -1481,7 +1527,8 @@ * @ngdoc method * @name angular.Module#directive * @methodOf angular.Module - * @param {string} name directive name + * @param {string|Object} name Directive name, or an object map of directives where the + * keys are the names and the values are the factories. * @param {Function} directiveFactory Factory function for creating new instance of * directives. * @description @@ -1554,11 +1601,11 @@ * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.2.0-rc.2', // all of these placeholder strings will be replaced by grunt's + full: '1.2.0-rc.3', // all of these placeholder strings will be replaced by grunt's major: 1, // package task minor: 2, dot: 0, - codeName: 'barehand-atomsplitting' + codeName: 'ferocious-twitch' }; @@ -1654,6 +1701,7 @@ $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $interpolate: $InterpolateProvider, + $interval: $IntervalProvider, $http: $HttpProvider, $httpBackend: $HttpBackendProvider, $location: $LocationProvider, @@ -1666,8 +1714,7 @@ $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, - $window: $WindowProvider, - $$urlUtils: $$UrlUtilsProvider + $window: $WindowProvider }); } ]); @@ -1684,64 +1731,64 @@ * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. - * `angular.element` can be either an alias for [jQuery](http://api.jquery.com/jQuery/) function, if - * jQuery is available, or a function that wraps the element or string in Angular's jQuery lite - * implementation (commonly referred to as jqLite). * - * Real jQuery always takes precedence over jqLite, provided it was loaded before `DOMContentLoaded` - * event fired. + * If jQuery is available, `angular.element` is an alias for the + * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` + * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." * - * jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM. jqLite implements only the most commonly needed functionality - * within a very small footprint, so only a subset of the jQuery API - methods, arguments and - * invocation styles - are supported. + * <div class="alert alert-success">jqLite is a tiny, API-compatible subset of jQuery that allows + * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most + * commonly needed functionality with the goal of having a very small footprint.</div> * - * Note: All element references in Angular are always wrapped with jQuery or jqLite; they are never - * raw DOM references. + * To use jQuery, simply load it before `DOMContentLoaded` event fired. * + * <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or + * jqLite; they are never raw DOM references.</div> + * * ## Angular's jqLite - * Angular's lite version of jQuery provides only the following jQuery methods: + * jqLite provides only the following jQuery methods: * - * - [addClass()](http://api.jquery.com/addClass/) - * - [after()](http://api.jquery.com/after/) - * - [append()](http://api.jquery.com/append/) - * - [attr()](http://api.jquery.com/attr/) - * - [bind()](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [children()](http://api.jquery.com/children/) - Does not support selectors - * - [clone()](http://api.jquery.com/clone/) - * - [contents()](http://api.jquery.com/contents/) - * - [css()](http://api.jquery.com/css/) - * - [data()](http://api.jquery.com/data/) - * - [eq()](http://api.jquery.com/eq/) - * - [find()](http://api.jquery.com/find/) - Limited to lookups by tag name - * - [hasClass()](http://api.jquery.com/hasClass/) - * - [html()](http://api.jquery.com/html/) - * - [next()](http://api.jquery.com/next/) - Does not support selectors - * - [on()](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [off()](http://api.jquery.com/off/) - Does not support namespaces or selectors - * - [parent()](http://api.jquery.com/parent/) - Does not support selectors - * - [prepend()](http://api.jquery.com/prepend/) - * - [prop()](http://api.jquery.com/prop/) - * - [ready()](http://api.jquery.com/ready/) - * - [remove()](http://api.jquery.com/remove/) - * - [removeAttr()](http://api.jquery.com/removeAttr/) - * - [removeClass()](http://api.jquery.com/removeClass/) - * - [removeData()](http://api.jquery.com/removeData/) - * - [replaceWith()](http://api.jquery.com/replaceWith/) - * - [text()](http://api.jquery.com/text/) - * - [toggleClass()](http://api.jquery.com/toggleClass/) - * - [triggerHandler()](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [unbind()](http://api.jquery.com/off/) - Does not support namespaces - * - [val()](http://api.jquery.com/val/) - * - [wrap()](http://api.jquery.com/wrap/) + * - [`addClass()`](http://api.jquery.com/addClass/) + * - [`after()`](http://api.jquery.com/after/) + * - [`append()`](http://api.jquery.com/append/) + * - [`attr()`](http://api.jquery.com/attr/) + * - [`bind()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData + * - [`children()`](http://api.jquery.com/children/) - Does not support selectors + * - [`clone()`](http://api.jquery.com/clone/) + * - [`contents()`](http://api.jquery.com/contents/) + * - [`css()`](http://api.jquery.com/css/) + * - [`data()`](http://api.jquery.com/data/) + * - [`eq()`](http://api.jquery.com/eq/) + * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name + * - [`hasClass()`](http://api.jquery.com/hasClass/) + * - [`html()`](http://api.jquery.com/html/) + * - [`next()`](http://api.jquery.com/next/) - Does not support selectors + * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors + * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors + * - [`prepend()`](http://api.jquery.com/prepend/) + * - [`prop()`](http://api.jquery.com/prop/) + * - [`ready()`](http://api.jquery.com/ready/) + * - [`remove()`](http://api.jquery.com/remove/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) + * - [`removeClass()`](http://api.jquery.com/removeClass/) + * - [`removeData()`](http://api.jquery.com/removeData/) + * - [`replaceWith()`](http://api.jquery.com/replaceWith/) + * - [`text()`](http://api.jquery.com/text/) + * - [`toggleClass()`](http://api.jquery.com/toggleClass/) + * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. + * - [`unbind()`](http://api.jquery.com/off/) - Does not support namespaces + * - [`val()`](http://api.jquery.com/val/) + * - [`wrap()`](http://api.jquery.com/wrap/) * * ## jQuery/jqLite Extras * Angular also provides the following additional methods and events to both jQuery and jqLite: * * ### Events * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event - * on all DOM nodes being removed. This can be used to clean up and 3rd party bindings to the DOM + * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM * element before it is removed. + * * ### Methods * - `controller(name)` - retrieves the controller of the current element or its parent. By default * retrieves controller associated with the `ngController` directive. If `name` is provided as @@ -1952,29 +1999,36 @@ } function JQLiteHasClass(element, selector) { - return ((" " + element.className + " ").replace(/[\n\t]/g, " "). + if (!element.getAttribute) return false; + return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). indexOf( " " + selector + " " ) > -1); } function JQLiteRemoveClass(element, cssClasses) { - if (cssClasses) { + if (cssClasses && element.setAttribute) { forEach(cssClasses.split(' '), function(cssClass) { - element.className = trim( - (" " + element.className + " ") + element.setAttribute('class', trim( + (" " + (element.getAttribute('class') || '') + " ") .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ") + .replace(" " + trim(cssClass) + " ", " ")) ); }); } } function JQLiteAddClass(element, cssClasses) { - if (cssClasses) { + if (cssClasses && element.setAttribute) { + var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, " "); + forEach(cssClasses.split(' '), function(cssClass) { - if (!JQLiteHasClass(element, cssClass)) { - element.className = trim(element.className + ' ' + trim(cssClass)); + cssClass = trim(cssClass); + if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { + existingClasses += cssClass + ' '; } }); + + element.setAttribute('class', trim(existingClasses)); } } @@ -2470,13 +2524,16 @@ triggerHandler: function(element, eventName, eventData) { var eventFns = (JQLiteExpandoStore(element, 'events') || {})[eventName]; - eventData = eventData || { + + eventData = eventData || []; + + var event = [{ preventDefault: noop, stopPropagation: noop - }; + }]; forEach(eventFns, function(fn) { - fn.call(element, eventData); + fn.apply(element, event.concat(eventData)); }); } }, function(fn, name){ @@ -2716,7 +2773,8 @@ * @description * Invoke the method and supply the method arguments from the `$injector`. * - * @param {!function} fn The function to invoke. The function arguments come form the function annotation. + * @param {!function} fn The function to invoke. Function parameters are injected according to the + * {@link guide/di $inject Annotation} rules. * @param {Object=} self The `this` for the invoked method. * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before * the `$injector` is consulted. @@ -2837,46 +2895,37 @@ * * @description * - * Use `$provide` to register new providers with the `$injector`. The providers are the factories for the instance. - * The providers share the same name as the instance they create with `Provider` suffixed to them. + * The {@link AUTO.$provide $provide} service has a number of methods for registering components with + * the {@link AUTO.$injector $injector}. Many of these functions are also exposed on {@link angular.Module}. * - * A provider is an object with a `$get()` method. The injector calls the `$get` method to create a new instance of - * a service. The Provider can have additional methods which would allow for configuration of the provider. + * An Angular **service** is a singleton object created by a **service factory**. These **service + * factories** are functions which, in turn, are created by a **service provider**. + * The **service providers** are constructor functions. When instantiated they must contain a property + * called `$get`, which holds the **service factory** function. + * + * When you request a service, the {@link AUTO.$injector $injector} is responsible for finding the + * correct **service provider**, instantiating it and then calling its `$get` **service factory** + * function to get the instance of the **service**. + * + * Often services have no configuration options and there is no need to add methods to the service + * provider. The provider will be no more than a constructor function with a `$get` property. For + * these cases the {@link AUTO.$provide $provide} service has additional helper methods to register + * services without specifying a provider. * - * <pre> - * function GreetProvider() { - * var salutation = 'Hello'; + * * {@link AUTO.$provide#provider provider(provider)} - registers a **service provider** with the + * {@link AUTO.$injector $injector} + * * {@link AUTO.$provide#constant constant(obj)} - registers a value/object that can be accessed by + * providers and services. + * * {@link AUTO.$provide#value value(obj)} - registers a value/object that can only be accessed by + * services, not providers. + * * {@link AUTO.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, that + * will be wrapped in a **service provider** object, whose `$get` property will contain the given + * factory function. + * * {@link AUTO.$provide#service service(class)} - registers a **constructor function**, `class` that + * will be wrapped in a **service provider** object, whose `$get` property will instantiate a new + * object using the given constructor function. * - * this.salutation = function(text) { - * salutation = text; - * }; - * - * this.$get = function() { - * return function (name) { - * return salutation + ' ' + name + '!'; - * }; - * }; - * } - * - * describe('Greeter', function(){ - * - * beforeEach(module(function($provide) { - * $provide.provider('greet', GreetProvider); - * })); - * - * it('should greet', inject(function(greet) { - * expect(greet('angular')).toEqual('Hello angular!'); - * })); - * - * it('should allow configuration of salutation', function() { - * module(function(greetProvider) { - * greetProvider.salutation('Ahoj'); - * }); - * inject(function(greet) { - * expect(greet('angular')).toEqual('Ahoj angular!'); - * }); - * }); - * </pre> + * See the individual methods for more information and examples. */ /** @@ -2885,8 +2934,19 @@ * @methodOf AUTO.$provide * @description * - * Register a provider for a service. The providers can be retrieved and can have additional configuration methods. + * Register a **provider function** with the {@link AUTO.$injector $injector}. Provider functions are + * constructor functions, whose instances are responsible for "providing" a factory for a service. + * + * Service provider names start with the name of the service they provide followed by `Provider`. + * For example, the {@link ng.$log $log} service has a provider called {@link ng.$logProvider $logProvider}. * + * Service provider objects can have additional methods which allow configuration of the provider and + * its service. Importantly, you can configure what kind of service is created by the `$get` method, + * or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a method + * {@link ng.$logProvider#debugEnabled debugEnabled} + * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the + * console or not. + * * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provider'` key. * @param {(Object|function())} provider If the provider is: * @@ -2896,6 +2956,70 @@ * {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. * * @returns {Object} registered provider instance + + * @example + * + * The following example shows how to create a simple event tracking service and register it using + * {@link AUTO.$provide#provider $provide.provider()}. + * + * <pre> + * // Define the eventTracker provider + * function EventTrackerProvider() { + * var trackingUrl = '/track'; + * + * // A provider method for configuring where the tracked events should been saved + * this.setTrackingUrl = function(url) { + * trackingUrl = url; + * }; + * + * // The service factory function + * this.$get = ['$http', function($http) { + * var trackedEvents = {}; + * return { + * // Call this to track an event + * event: function(event) { + * var count = trackedEvents[event] || 0; + * count += 1; + * trackedEvents[event] = count; + * return count; + * }, + * // Call this to save the tracked events to the trackingUrl + * save: function() { + * $http.post(trackingUrl, trackedEvents); + * } + * }; + * }]; + * } + * + * describe('eventTracker', function() { + * var postSpy; + * + * beforeEach(module(function($provide) { + * // Register the eventTracker provider + * $provide.provider('eventTracker', EventTrackerProvider); + * })); + * + * beforeEach(module(function(eventTrackerProvider) { + * // Configure eventTracker provider + * eventTrackerProvider.setTrackingUrl('/custom-track'); + * })); + * + * it('tracks events', inject(function(eventTracker) { + * expect(eventTracker.event('login')).toEqual(1); + * expect(eventTracker.event('login')).toEqual(2); + * })); + * + * it('saves to the tracking url', inject(function(eventTracker, $http) { + * postSpy = spyOn($http, 'post'); + * eventTracker.event('login'); + * eventTracker.save(); + * expect(postSpy).toHaveBeenCalled(); + * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); + * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); + * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); + * })); + * }); + * </pre> */ /** @@ -2904,12 +3028,32 @@ * @methodOf AUTO.$provide * @description * - * A short hand for configuring services if only `$get` method is required. + * Register a **service factory**, which will be called to return the service instance. + * This is short for registering a service where its provider consists of only a `$get` property, + * which is the given service factory function. + * You should use {@link AUTO.$provide#factory $provide.factor(getFn)} if you do not need to configure + * your service in a provider. * * @param {string} name The name of the instance. * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand for * `$provide.provider(name, {$get: $getFn})`. * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service + * <pre> + * $provide.factory('ping', ['$http', function($http) { + * return function ping() { + * return $http.send('/ping'); + * }; + * }]); + * </pre> + * You would then inject and use this service like this: + * <pre> + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping(); + * }]); + * </pre> */ @@ -2919,11 +3063,34 @@ * @methodOf AUTO.$provide * @description * - * A short hand for registering service of given class. + * Register a **service constructor**, which will be invoked with `new` to create the service instance. + * This is short for registering a service where its provider's `$get` property is the service + * constructor function that will be used to instantiate the service instance. * + * You should use {@link AUTO.$provide#service $provide.service(class)} if you define your service + * as a type/class. This is common when using {@link http://coffeescript.org CoffeeScript}. + * * @param {string} name The name of the instance. * @param {Function} constructor A class (constructor function) that will be instantiated. * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service using {@link AUTO.$provide#service $provide.service(class)} + * that is defined as a CoffeeScript class. + * <pre> + * class Ping + * constructor: (@$http)-> + * send: ()=> + * @$http.get('/ping') + * + * $provide.service('ping', ['$http', Ping]) + * </pre> + * You would then inject and use this service like this: + * <pre> + * someModule.controller 'Ctrl', ['ping', (ping)-> + * ping.send() + * ] + * </pre> */ @@ -2933,11 +3100,29 @@ * @methodOf AUTO.$provide * @description * - * A short hand for configuring services if the `$get` method is a constant. + * Register a **value service** with the {@link AUTO.$injector $injector}, such as a string, a number, + * an array, an object or a function. This is short for registering a service where its provider's + * `$get` property is a factory function that takes no arguments and returns the **value service**. * + * Value services are similar to constant services, except that they cannot be injected into a module + * configuration function (see {@link angular.Module#config}) but they can be overridden by an Angular + * {@link AUTO.$provide#decorator decorator}. + * * @param {string} name The name of the instance. * @param {*} value The value. * @returns {Object} registered provider instance + * + * @example + * Here are some examples of creating value services. + * <pre> + * $provide.constant('ADMIN_USER', 'admin'); + * + * $provide.constant('RoleLookup', { admin: 0, writer: 1, reader: 2 }); + * + * $provide.constant('halfOf', function(value) { + * return value / 2; + * }); + * </pre> */ @@ -2947,13 +3132,26 @@ * @methodOf AUTO.$provide * @description * - * A constant value, but unlike {@link AUTO.$provide#value value} it can be injected - * into configuration function (other modules) and it is not interceptable by - * {@link AUTO.$provide#decorator decorator}. + * Register a **constant service**, such as a string, a number, an array, an object or a function, with + * the {@link AUTO.$injector $injector}. Unlike {@link AUTO.$provide#value value} it can be injected + * into a module configuration function (see {@link angular.Module#config}) and it cannot be + * overridden by an Angular {@link AUTO.$provide#decorator decorator}. * * @param {string} name The name of the constant. * @param {*} value The constant value. * @returns {Object} registered instance + * + * @example + * Here a some examples of creating constants: + * <pre> + * $provide.constant('SHARD_HEIGHT', 306); + * + * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); + * + * $provide.constant('double', function(value) { + * return value * 2; + * }); + * </pre> */ @@ -2963,17 +3161,29 @@ * @methodOf AUTO.$provide * @description * - * Decoration of service, allows the decorator to intercept the service instance creation. The - * returned instance may be the original instance, or a new instance which delegates to the - * original instance. + * Register a **service decorator** with the {@link AUTO.$injector $injector}. A service decorator + * intercepts the creation of a service, allowing it to override or modify the behaviour of the + * service. The object returned by the decorator may be the original service, or a new service object + * which replaces or wraps and delegates to the original service. * * @param {string} name The name of the service to decorate. * @param {function()} decorator This function will be invoked when the service needs to be - * instantiated. The function is called using the {@link AUTO.$injector#invoke - * injector.invoke} method and is therefore fully injectable. Local injection arguments: + * instantiated and should return the decorated service instance. The function is called using + * the {@link AUTO.$injector#invoke injector.invoke} method and is therefore fully injectable. + * Local injection arguments: * * * `$delegate` - The original service instance, which can be monkey patched, configured, * decorated or delegated to. + * + * @example + * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting + * calls to {@link ng.$log#error $log.warn()}. + * <pre> + * $provider.decorator('$log', ['$delegate', function($delegate) { + * $delegate.warn = $delegate.error; + * return $delegate; + * }]); + * </pre> */ @@ -3023,6 +3233,7 @@ } function provider(name, provider_) { + assertNotHasOwnProperty(name, 'service'); if (isFunction(provider_) || isArray(provider_)) { provider_ = providerInjector.instantiate(provider_); } @@ -3043,6 +3254,7 @@ function value(name, value) { return factory(name, valueFn(value)); } function constant(name, value) { + assertNotHasOwnProperty(name, 'constant'); providerCache[name] = value; instanceCache[name] = value; } @@ -3188,6 +3400,7 @@ } } + /** * @ngdoc function * @name ng.$anchorScroll @@ -3200,8 +3413,41 @@ * according to rules specified in * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-documen... Html5 spec}. * - * It also watches the `$location.hash()` and scroll whenever it changes to match any anchor. + * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. + * + * @example + <example> + <file name="index.html"> + <div id="scrollArea" ng-controller="ScrollCtrl"> + <a ng-click="gotoBottom()">Go to bottom</a> + <a id="bottom"></a> You're at the bottom! + </div> + </file> + <file name="script.js"> + function ScrollCtrl($scope, $location, $anchorScroll) { + $scope.gotoBottom = function (){ + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + } + } + </file> + <file name="style.css"> + #scrollArea { + height: 350px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + </file> + </example> */ function $AnchorScrollProvider() { @@ -3318,8 +3564,8 @@ * @name ng.$animate * * @description - * The $animate service provides rudimentary DOM manipulation functions to insert, remove, move elements within - * the DOM as well as adding and removing classes. This service is the core service used by the ngAnimate $animator + * The $animate service provides rudimentary DOM manipulation functions to insert, remove and move elements within + * the DOM, as well as adding and removing classes. This service is the core service used by the ngAnimate $animator * service which provides high-level animation hooks for CSS and JavaScript. * * $animate is available in the AngularJS core, however, the ngAnimate module must be included to enable full out @@ -3386,7 +3632,7 @@ * @param {jQuery/jqLite element} element the element which will be moved around within the DOM * @param {jQuery/jqLite element} parent the parent element where the element will be inserted into (if the after element is not present) * @param {jQuery/jqLite element} after the sibling element where the element will be positioned next to - * @param {function=} done the callback function (if provided) that will be fired after the element has been moved to it's new position + * @param {function=} done the callback function (if provided) that will be fired after the element has been moved to its new position */ move : function(element, parent, after, done) { // Do not remove element before insert. Removing will cause data associated with the @@ -3411,7 +3657,9 @@ className = isString(className) ? className : isArray(className) ? className.join(' ') : ''; - element.addClass(className); + forEach(element, function (element) { + JQLiteAddClass(element, className); + }); done && $timeout(done, 0, false); }, @@ -3432,7 +3680,9 @@ className = isString(className) ? className : isArray(className) ? className.join(' ') : ''; - element.removeClass(className); + forEach(element, function (element) { + JQLiteRemoveClass(element, className); + }); done && $timeout(done, 0, false); }, @@ -3566,7 +3816,7 @@ var lastBrowserUrl = location.href, baseElement = document.find('base'), - replacedUrl = null; + newLocation = null; /** * @name ng.$browser#url @@ -3589,6 +3839,9 @@ * @param {boolean=} replace Should new url replace current history record ? */ self.url = function(url, replace) { + // Android Browser BFCache causes location reference to become stale. + if (location !== window.location) location = window.location; + // setter if (url) { if (lastBrowserUrl == url) return; @@ -3601,21 +3854,20 @@ baseElement.attr('href', baseElement.attr('href')); } } else { + newLocation = url; if (replace) { location.replace(url); - replacedUrl = url; } else { location.href = url; - replacedUrl = null; } } return self; // getter } else { - // - the replacedUrl is a workaround for an IE8-9 issue with location.replace method that doesn't update - // location.href synchronously + // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href + // methods not updating location.href synchronously. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return replacedUrl || location.href.replace(/%27/g,"'"); + return newLocation || location.href.replace(/%27/g,"'"); } }; @@ -3623,6 +3875,7 @@ urlChangeInit = false; function fireUrlChange() { + newLocation = null; if (lastBrowserUrl == self.url()) return; lastBrowserUrl = self.url(); @@ -3679,10 +3932,14 @@ ////////////////////////////////////////////////////////////// /** + * @name ng.$browser#baseHref + * @methodOf ng.$browser + * + * @description * Returns current <base href> * (always relative - without domain) * - * @returns {string=} + * @returns {string=} current <base href> */ self.baseHref = function() { var href = baseElement.attr('href'); @@ -4157,10 +4414,10 @@ * (a DOM element/tree) to a scope. Where: * * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. - * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the - * `template` and call the `cloneAttachFn` function allowing the caller to attach the - * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is - * called as: <br> `cloneAttachFn(clonedElement, scope)` where: + * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the `template` + * and call the `cloneAttachFn` function allowing the caller to attach the + * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is + * called as: <br> `cloneAttachFn(clonedElement, scope)` where: * * * `clonedElement` - is a clone of the original `element` passed into the compiler. * * `scope` - is the current scope with which the linking function is working with. @@ -4213,13 +4470,13 @@ Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, - aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|file):/, + 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 // The assumption is that future DOM event attribute names will begin with // 'on' and be composed of only English letters. - var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]*|formaction)$/; + var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; /** * @ngdoc function @@ -4230,13 +4487,15 @@ * @description * Register a new directive with the compiler. * - * @param {string} name Name of the directive in camel-case. (ie <code>ngBind</code> which will match as - * <code>ng-bind</code>). - * @param {function|Array} directiveFactory An injectable directive factory function. See {@link guide/directive} for more - * info. + * @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which + * will match as <code>ng-bind</code>), or an object map of directives where the keys are the + * names and the values are the factories. + * @param {function|Array} directiveFactory An injectable directive factory function. See + * {@link guide/directive} for more info. * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { + assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertArg(directiveFactory, 'directiveFactory'); if (!hasDirectives.hasOwnProperty(name)) { @@ -4244,7 +4503,7 @@ $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', function($injector, $exceptionHandler) { var directives = []; - forEach(hasDirectives[name], function(directiveFactory) { + forEach(hasDirectives[name], function(directiveFactory, index) { try { var directive = $injector.invoke(directiveFactory); if (isFunction(directive)) { @@ -4253,6 +4512,7 @@ directive.compile = valueFn(directive.link); } directive.priority = directive.priority || 0; + directive.index = index; directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); directive.restrict = directive.restrict || 'A'; @@ -4334,9 +4594,9 @@ this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$$urlUtils', '$animate', + '$controller', '$rootScope', '$document', '$sce', '$animate', function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document, $sce, $$urlUtils, $animate) { + $controller, $rootScope, $document, $sce, $animate) { var Attributes = function(element, attr) { this.$$element = element; @@ -4428,9 +4688,9 @@ // sanitize a[href] and img[src] values if ((nodeName === 'A' && key === 'href') || (nodeName === 'IMG' && key === 'src')) { - // NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case. + // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. if (!msie || msie >= 8 ) { - normalizedVal = $$urlUtils.resolve(value); + normalizedVal = urlResolve(value).href; if (normalizedVal !== '') { if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { @@ -4478,12 +4738,23 @@ /** - * Observe an interpolated attribute. - * The observer will never be called, if given attribute is not interpolated. + * @ngdoc function + * @name ng.$compile.directive.Attributes#$observe + * @methodOf ng.$compile.directive.Attributes + * @function * + * @description + * Observes an interpolated attribute. + * + * The observer function will be invoked once during the next `$digest` following + * compilation. The observer is then invoked whenever the interpolated value + * changes. + * * @param {string} key Normalized key. (ie ngAttribute) . - * @param {function(*)} fn Function that will be called whenever the attribute value changes. - * @returns {function(*)} the `fn` Function passed in. + * @param {function(interpolatedValue)} fn Function that will be called whenever + the interpolated value of the attribute changes. + * See the {@link guide/directive#Attributes Directives} guide for more info. + * @returns {function()} the `fn` parameter. */ $observe: function(key, fn) { var attrs = this, @@ -4501,8 +4772,7 @@ } }; - var urlSanitizationNode = $document[0].createElement('a'), - startSymbol = $interpolate.startSymbol(), + var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') ? identity @@ -4516,7 +4786,7 @@ //================================ - function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective) { + function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { if (!($compileNodes instanceof jqLite)) { // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it. $compileNodes = jqLite($compileNodes); @@ -4528,7 +4798,7 @@ $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0]; } }); - var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective); + var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); return function publicLinkFn(scope, cloneConnectFn){ assertArg(scope, 'scope'); // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart @@ -4575,7 +4845,7 @@ * @param {number=} max directive priority * @returns {?function} A composite linking function of all of the matched directives or null. */ - function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective) { + function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], nodeLinkFn, childLinkFn, directives, attrs, linkFnFound; @@ -4586,7 +4856,7 @@ directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined, ignoreDirective); nodeLinkFn = (directives.length) - ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement) + ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext) : null; childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length) @@ -4597,6 +4867,7 @@ linkFns.push(nodeLinkFn); linkFns.push(childLinkFn); linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn); + previousCompileContext = null; //use the previous context only for the first element in the virtual group } // return a linking function if we have found anything, null otherwise @@ -4672,9 +4943,8 @@ // iterate over the attributes for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { - var attrStartName; - var attrEndName; - var index; + var attrStartName = false; + var attrEndName = false; attr = nAttrs[j]; if (!msie || msie >= 8 || attr.specified) { @@ -4682,13 +4952,16 @@ // support ngAttr attribute binding ngAttrName = directiveNormalize(name); if (NG_ATTR_BINDING.test(ngAttrName)) { - name = ngAttrName.substr(6).toLowerCase(); + name = snake_case(ngAttrName.substr(6), '-'); } - if ((index = ngAttrName.lastIndexOf('Start')) != -1 && index == ngAttrName.length - 5) { + + var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); + if (ngAttrName === directiveNName + 'Start') { attrStartName = name; attrEndName = name.substr(0, name.length - 5) + 'end'; name = name.substr(0, name.length - 6); } + nName = directiveNormalize(name.toLowerCase()); attrsMap[nName] = name; attrs[nName] = value = trim((msie && name == 'href') @@ -4763,6 +5036,7 @@ } else { nodes.push(node); } + return jqLite(nodes); } @@ -4794,20 +5068,26 @@ * scope argument is auto-generated to the new child of the transcluded parent scope. * @param {JQLite} jqCollection If we are working on the root of the compile tree then this * argument has the root jqLite array so that we can replace nodes on it. + * @param {Object=} originalReplaceDirective An optional directive that will be ignored when compiling + * the transclusion. + * @param {Array.<Function>} preLinkFns + * @param {Array.<Function>} postLinkFns + * @param {Object} previousCompileContext Context used for previous compilation of the current node * @returns linkFn */ - function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective) { + function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, + originalReplaceDirective, preLinkFns, postLinkFns, previousCompileContext) { + previousCompileContext = previousCompileContext || {}; + var terminalPriority = -Number.MAX_VALUE, - preLinkFns = [], - postLinkFns = [], - newScopeDirective = null, - newIsolateScopeDirective = null, - templateDirective = null, + newScopeDirective, + newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, + templateDirective = previousCompileContext.templateDirective, $compileNode = templateAttrs.$$element = jqLite(compileNode), directive, directiveName, $template, - transcludeDirective, + transcludeDirective = previousCompileContext.transcludeDirective, replaceDirective = originalReplaceDirective, childTranscludeFn = transcludeFn, controllerDirectives, @@ -4831,18 +5111,24 @@ } if (directiveValue = directive.scope) { - assertNoDuplicate('isolated scope', newIsolateScopeDirective, directive, $compileNode); - if (isObject(directiveValue)) { - safeAddClass($compileNode, 'ng-isolate-scope'); - newIsolateScopeDirective = directive; + newScopeDirective = newScopeDirective || directive; + + // skip the check for directives with async templates, we'll check the derived sync directive when + // the template arrives + if (!directive.templateUrl) { + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, $compileNode); + if (isObject(directiveValue)) { + safeAddClass($compileNode, 'ng-isolate-scope'); + newIsolateScopeDirective = directive; + } + safeAddClass($compileNode, 'ng-scope'); } - safeAddClass($compileNode, 'ng-scope'); - newScopeDirective = newScopeDirective || directive; } directiveName = directive.name; - if (directiveValue = directive.controller) { + if (!directive.templateUrl && directive.controller) { + directiveValue = directive.controller; controllerDirectives = controllerDirectives || {}; assertNoDuplicate("'" + directiveName + "' controller", controllerDirectives[directiveName], directive, $compileNode); @@ -4850,18 +5136,27 @@ } if (directiveValue = directive.transclude) { - assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode); - transcludeDirective = directive; - terminalPriority = directive.priority; + // Special case ngRepeat so that we don't complain about duplicate transclusion, ngRepeat knows how to handle + // this on its own. + if (directiveName !== 'ngRepeat') { + assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode); + transcludeDirective = directive; + } + if (directiveValue == 'element') { - $template = groupScan(compileNode, attrStart, attrEnd) + terminalPriority = directive.priority; + $template = groupScan(compileNode, attrStart, attrEnd); $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority, - replaceDirective && replaceDirective.name); + replaceDirective && replaceDirective.name, { + newIsolateScopeDirective: newIsolateScopeDirective, + transcludeDirective: transcludeDirective, + templateDirective: templateDirective + }); } else { $template = jqLite(JQLiteClone(compileNode)).contents(); $compileNode.html(''); // clear contents @@ -4921,8 +5216,13 @@ if (directive.replace) { replaceDirective = directive; } - nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), - nodeLinkFn, $compileNode, templateAttrs, jqCollection, childTranscludeFn); + + nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, + templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { + newIsolateScopeDirective: newIsolateScopeDirective, + transcludeDirective: transcludeDirective, + templateDirective: templateDirective + }); ii = directives.length; } else if (directive.compile) { try { @@ -4976,7 +5276,14 @@ } optional = optional || value == '?'; } + value = $element[retrievalMethod]('$' + require + 'Controller'); + + if ($element[0].nodeType == 8 && $element[0].$$controller) { // Transclusion comment node + value = value || $element[0].$$controller; + $element[0].$$controller = null; + } + if (!value && !optional) { throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); } @@ -5091,9 +5398,16 @@ } controllerInstance = $controller(controller, locals); - $element.data( - '$' + directive.name + 'Controller', - controllerInstance); + + // Directives with element transclusion and a controller need to attach controller + // to the comment node created by the compiler, but jQuery .data doesn't support + // attaching data to comment nodes so instead we set it directly on the element and + // remove it after we read it later. + if ($element[0].nodeType == 8) { // Transclusion comment node + $element[0].$$controller = controllerInstance; + } else { + $element.data('$' + directive.name + 'Controller', controllerInstance); + } if (directive.controllerAs) { locals.$scope[directive.controllerAs] = controllerInstance; } @@ -5115,7 +5429,7 @@ childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn); // POSTLINKING - for(i = 0, ii = postLinkFns.length; i < ii; i++) { + for(i = postLinkFns.length - 1; i >= 0; i--) { try { linkFn = postLinkFns[i]; linkFn(scope, $element, attrs, @@ -5195,6 +5509,9 @@ dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; } else if (key == 'style') { $element.attr('style', $element.attr('style') + ';' + value); + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { dst[key] = value; dstAttr[key] = srcAttr[key]; @@ -5203,8 +5520,8 @@ } - function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs, - $rootElement, childTranscludeFn) { + function compileTemplateUrl(directives, $compileNode, tAttrs, + $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { var linkQueue = [], afterTemplateNodeLinkFn, afterTemplateChildLinkFn, @@ -5212,7 +5529,7 @@ origAsyncDirective = directives.shift(), // The fact that we have to copy and patch the directive seems wrong! derivedSyncDirective = extend({}, origAsyncDirective, { - controller: null, templateUrl: null, transclude: null, scope: null, replace: null + templateUrl: null, transclude: null, replace: null }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) @@ -5246,7 +5563,8 @@ directives.unshift(derivedSyncDirective); - afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, childTranscludeFn, $compileNode, origAsyncDirective); + afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, + childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); forEach($rootElement, function(node, i) { if (node == compileNode) { $rootElement[i] = $compileNode[0]; @@ -5268,10 +5586,7 @@ replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); } - afterTemplateNodeLinkFn( - beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller), - scope, linkNode, $rootElement, controller - ); + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller); } linkQueue = null; }). @@ -5286,9 +5601,7 @@ linkQueue.push(rootElement); linkQueue.push(controller); } else { - afterTemplateNodeLinkFn(function() { - beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller); - }, scope, node, rootElement, controller); + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller); } }; } @@ -5298,7 +5611,10 @@ * Sorting function for bound directives. */ function byPriority(a, b) { - return b.priority - a.priority; + var diff = b.priority - a.priority; + if (diff !== 0) return diff; + if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; + return a.index - b.index; } @@ -5352,7 +5668,7 @@ } directives.push({ - priority: 100, + priority: -100, compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) { var $$observers = (attr.$$observers || (attr.$$observers = {})); @@ -5370,6 +5686,7 @@ // register any observers if (!interpolateFn) return; + // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the actual attr value attr[name] = interpolateFn(scope); ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). @@ -5528,11 +5845,13 @@ * @ngdoc function * @name ng.$controllerProvider#register * @methodOf ng.$controllerProvider - * @param {string} name Controller name + * @param {string|Object} name Controller name, or an object map of controllers where the keys are + * the names and the values are the constructors. * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI * annotations in the array notation). */ this.register = function(name, constructor) { + assertNotHasOwnProperty(name, 'controller'); if (isObject(name)) { extend(controllers, name) } else { @@ -5619,10 +5938,24 @@ * Any uncaught exception in angular expressions is delegated to this service. * The default implementation simply delegates to `$log.error` which logs it into * the browser console. - * + * * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. * + * ## Example: + * + * <pre> + * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { + * return function (exception, cause) { + * exception.message += ' (caused by "' + cause + '")'; + * throw exception; + * }; + * }); + * </pre> + * + * This example will override the normal action of `$exceptionHandler`, to make angular + * exceptions fail hard when they happen, instead of just logging to the console. + * * @param {Error} exception Exception associated with the error. * @param {string=} cause optional information about the context in which * the error was thrown. @@ -5757,18 +6090,19 @@ }; /** - * Are order by request. I.E. they are applied in the same order as - * array on request, but revers order on response. + * Are ordered by request, i.e. they are applied in the same order as the + * array, on request, but reverse order, on response. */ var interceptorFactories = this.interceptors = []; + /** - * For historical reasons, response interceptors ordered by the order in which - * they are applied to response. (This is in revers to interceptorFactories) + * For historical reasons, response interceptors are ordered by the order in which + * they are applied to the response. (This is the opposite of interceptorFactories) */ var responseInterceptorFactories = this.responseInterceptors = []; - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', '$$urlUtils', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector, $$urlUtils) { + this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { var defaultCache = $cacheFactory('$http'); @@ -5857,7 +6191,34 @@ * will result in the success callback being called. Note that if the response is a redirect, * XMLHttpRequest will transparently follow it, meaning that the error callback will not be * called for such responses. + * + * # Calling $http from outside AngularJS + * The `$http` service will not actually send the request until the next `$digest()` is executed. + * Normally this is not an issue, since almost all the time your call to `$http` will be from within + * a `$apply()` block. + * If you are calling `$http` from outside Angular, then you should wrap it in a call to `$apply` + * to cause a $digest to occur and also to handle errors in the block correctly. * + * ``` + * $scope.$apply(function() { + * $http(...); + * }); + * ``` + * + * # Writing Unit Tests that use $http + * When unit testing you are mostly responsible for scheduling the `$digest` cycle. If you do not + * trigger a `$digest` before calling `$httpBackend.flush()` then the request will not have been + * made and `$httpBackend.expect(...)` expectations will fail. The solution is to run the code + * that calls the `$http()` method inside a $apply block as explained in the previous section. + * + * ``` + * $httpBackend.expectGET(...); + * $scope.$apply(function() { + * $http.get(...); + * }); + * $httpBackend.flush(); + * ``` + * * # Shortcut methods * * Since all invocations of the $http service require passing in an HTTP method and URL, and @@ -5895,7 +6256,7 @@ * To add or overwrite these defaults, simply add or remove a property from these configuration * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get['My-Header']='value'`. + * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. * * Additionally, the defaults can be set at runtime via the `$http.defaults` object in the same * fashion. @@ -6257,7 +6618,7 @@ config.headers = headers; config.method = uppercase(config.method); - var xsrfValue = $$urlUtils.isSameOrigin(config.url) + var xsrfValue = urlIsSameOrigin(config.url) ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] : undefined; if (xsrfValue) { @@ -6680,7 +7041,9 @@ var xhr = new XHR(); xhr.open(method, url, true); forEach(headers, function(value, key) { - if (value) xhr.setRequestHeader(key, value); + if (isDefined(value)) { + xhr.setRequestHeader(key, value); + } }); // In IE6 and 7, this might be called synchronously when xhr.send below is called and the @@ -6690,26 +7053,6 @@ if (xhr.readyState == 4) { var responseHeaders = xhr.getAllResponseHeaders(); - // TODO(vojta): remove once Firefox 21 gets released. - // begin: workaround to overcome Firefox CORS http response headers bug - // https://bugzilla.mozilla.org/show_bug.cgi?id=608735 - // Firefox already patched in nightly. Should land in Firefox 21. - - // CORS "simple response headers" http://www.w3.org/TR/cors/ - var value, - simpleHeaders = ["Cache-Control", "Content-Language", "Content-Type", - "Expires", "Last-Modified", "Pragma"]; - if (!responseHeaders) { - responseHeaders = ""; - forEach(simpleHeaders, function (header) { - var value = xhr.getResponseHeader(header); - if (value) { - responseHeaders += header + ": " + value + "\n"; - } - }); - } - // end of the workaround. - // responseText is the old-school way of retrieving response (supported by IE8 & 9) // response and responseType properties were introduced in XHR Level2 spec (supported by IE10) completeRequest(callback, @@ -6727,7 +7070,7 @@ xhr.responseType = responseType; } - xhr.send(post || ''); + xhr.send(post || null); } if (timeout > 0) { @@ -6744,8 +7087,7 @@ } function completeRequest(callback, status, response, headersString) { - // URL_MATCH is defined in src/service/location.js - var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1]; + var protocol = locationProtocol || urlResolve(url).protocol; // cancel timeout and subsequent timeout promise resolution timeoutId && $browserDefer.cancel(timeoutId); @@ -7027,6 +7369,94 @@ }]; } +function $IntervalProvider() { + this.$get = ['$rootScope', '$window', '$q', + function($rootScope, $window, $q) { + var intervals = {}; + + + /** + * @ngdoc function + * @name ng.$interval + * + * @description + * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` + * milliseconds. + * + * The return value of registering an interval function is a promise. This promise will be + * notified upon each tick of the interval, and will be resolved after `count` iterations, or + * run indefinitely if `count` is not defined. The value of the notification will be the + * number of iterations that have run. + * To cancel an interval, call `$interval.cancel(promise)`. + * + * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + */ + function interval(fn, delay, count, invokeApply) { + var setInterval = $window.setInterval, + clearInterval = $window.clearInterval; + + var deferred = $q.defer(), + promise = deferred.promise, + count = (isDefined(count)) ? count : 0, + iteration = 0, + skipApply = (isDefined(invokeApply) && !invokeApply); + + promise.then(null, null, fn); + + promise.$$intervalId = setInterval(function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + deferred.resolve(iteration); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + } + + if (!skipApply) $rootScope.$apply(); + + }, delay); + + intervals[promise.$$intervalId] = deferred; + + return promise; + } + + + /** + * @ngdoc function + * @name ng.$interval#cancel + * @methodOf ng.$interval + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {number} promise Promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ + interval.cancel = function(promise) { + if (promise && promise.$$intervalId in intervals) { + intervals[promise.$$intervalId].reject('canceled'); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + return true; + } + return false; + }; + + return interval; + }]; +} + /** * @ngdoc object * @name ng.$locale @@ -7098,8 +7528,7 @@ }; } -var SERVER_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?(\{?[\w\.-]*\}?)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, - PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -7121,39 +7550,40 @@ return segments.join('/'); } -function matchUrl(url, obj) { - var match = SERVER_MATCH.exec(url); +function parseAbsoluteUrl(absoluteUrl, locationObj) { + var parsedUrl = urlResolve(absoluteUrl); - obj.$$protocol = match[1]; - obj.$$host = match[3]; - obj.$$port = int(match[5]) || DEFAULT_PORTS[match[1]] || null; + locationObj.$$protocol = parsedUrl.protocol; + locationObj.$$host = parsedUrl.hostname; + locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } -function matchAppUrl(url, obj) { - var match = PATH_MATCH.exec(url); - obj.$$path = decodeURIComponent(match[1]); - obj.$$search = parseKeyValue(match[3]); - obj.$$hash = decodeURIComponent(match[5] || ''); +function parseAppUrl(relativeUrl, locationObj) { + var prefixed = (relativeUrl.charAt(0) !== '/'); + if (prefixed) { + relativeUrl = '/' + relativeUrl; + } + var match = urlResolve(relativeUrl); + locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname); + locationObj.$$search = parseKeyValue(match.search); + locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; - if (obj.$$path && obj.$$path.charAt(0) != '/') obj.$$path = '/' + obj.$$path; + if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') locationObj.$$path = '/' + locationObj.$$path; } -function composeProtocolHostPort(protocol, host, port) { - return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port); -} - /** * * @param {string} begin * @param {string} whole - * @param {string} otherwise - * @returns {string} returns text from whole after begin or otherwise if it does not begin with expected string. + * @returns {string} returns text from whole after begin or undefined if it does not begin with expected string. */ -function beginsWith(begin, whole, otherwise) { - return whole.indexOf(begin) == 0 ? whole.substr(begin.length) : otherwise; +function beginsWith(begin, whole) { + if (whole.indexOf(begin) == 0) { + return whole.substr(begin.length); + } } @@ -7185,20 +7615,22 @@ this.$$html5 = true; basePrefix = basePrefix || ''; var appBaseNoFile = stripFile(appBase); + parseAbsoluteUrl(appBase, this); + + /** * Parse given html5 (regular) url string into properties * @param {string} newAbsoluteUrl HTML5 url * @private */ this.$$parse = function(url) { - var parsed = {} - matchUrl(url, parsed); var pathUrl = beginsWith(appBaseNoFile, url); if (!isString(pathUrl)) { throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); } - matchAppUrl(pathUrl, parsed); - extend(this, parsed); + + parseAppUrl(pathUrl, this); + if (!this.$$path) { this.$$path = '/'; } @@ -7249,7 +7681,7 @@ function LocationHashbangUrl(appBase, hashPrefix) { var appBaseNoFile = stripFile(appBase); - matchUrl(appBase, this); + parseAbsoluteUrl(appBase, this); /** @@ -7268,7 +7700,7 @@ if (!isString(withoutHashUrl)) { throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, hashPrefix); } - matchAppUrl(withoutHashUrl, this); + parseAppUrl(withoutHashUrl, this); this.$$compose(); }; @@ -7601,7 +8033,7 @@ * @name ng.$locationProvider#html5Mode * @methodOf ng.$locationProvider * @description - * @param {string=} mode Use HTML5 strategy if available. + * @param {boolean=} mode Use HTML5 strategy if available. * @returns {*} current value if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { @@ -7722,9 +8154,12 @@ * @description * Simple service for logging. Default implementation writes the message * into the browser's console (if present). - * + * * The main purpose of this service is to simplify debugging and troubleshooting. * + * The default is not to log `debug` messages. You can use + * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. + * * @example <example> <file name="script.js"> @@ -7865,13 +8300,15 @@ // we are IE which either doesn't have window.console => this is noop and we do nothing, // or we are IE where console.log doesn't have apply so we log at least first 2 args return function(arg1, arg2) { - logFn(arg1, arg2); + logFn(arg1, arg2 == null ? '' : arg2); } } }]; } var $parseMinErr = minErr('$parse'); +var promiseWarningCache = {}; +var promiseWarning; // Sandboxing Angular Expressions // ------------------------------ @@ -7913,12 +8350,19 @@ if (obj && obj.constructor === obj) { throw $parseMinErr('isecfn', 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); + } else if (// isWindow(obj) + obj && obj.document && obj.location && obj.alert && obj.setInterval) { + throw $parseMinErr('isecwindow', + 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression); + } else if (// isElement(obj) + obj && (obj.nodeName || (obj.on && obj.find))) { + throw $parseMinErr('isecdom', + 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression); } else { return obj; } } - var OPERATORS = { 'null':function(){return null;}, 'true':function(){return true;}, @@ -7956,156 +8400,192 @@ }; var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; -function lex(text, csp){ - var tokens = [], - token, - index = 0, - json = [], - ch, - lastCh = ':'; // can start regexp - while (index < text.length) { - ch = text.charAt(index); - if (is('"\'')) { - readString(ch); - } else if (isNumber(ch) || is('.') && isNumber(peek())) { - readNumber(); - } else if (isIdent(ch)) { - readIdent(); - // identifiers can only be if the preceding char was a { or , - if (was('{,') && json[0]=='{' && - (token=tokens[tokens.length-1])) { - token.json = token.text.indexOf('.') == -1; - } - } else if (is('(){}[].,;:?')) { - tokens.push({ - index:index, - text:ch, - json:(was(':[,') && is('{[')) || is('}]:,') - }); - if (is('{[')) json.unshift(ch); - if (is('}]')) json.shift(); - index++; - } else if (isWhitespace(ch)) { - index++; - continue; - } else { - var ch2 = ch + peek(), - ch3 = ch2 + peek(2), - fn = OPERATORS[ch], - fn2 = OPERATORS[ch2], - fn3 = OPERATORS[ch3]; - if (fn3) { - tokens.push({index:index, text:ch3, fn:fn3}); - index += 3; - } else if (fn2) { - tokens.push({index:index, text:ch2, fn:fn2}); - index += 2; - } else if (fn) { - tokens.push({index:index, text:ch, fn:fn, json: was('[,:') && is('+-')}); - index += 1; +///////////////////////////////////////// + + +/** + * @constructor + */ +var Lexer = function (options) { + this.options = options; +}; + +Lexer.prototype = { + constructor: Lexer, + + lex: function (text) { + this.text = text; + + this.index = 0; + this.ch = undefined; + this.lastCh = ':'; // can start regexp + + this.tokens = []; + + var token; + var json = []; + + while (this.index < this.text.length) { + this.ch = this.text.charAt(this.index); + if (this.is('"\'')) { + this.readString(this.ch); + } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { + this.readNumber(); + } else if (this.isIdent(this.ch)) { + this.readIdent(); + // identifiers can only be if the preceding char was a { or , + if (this.was('{,') && json[0] === '{' && + (token = this.tokens[this.tokens.length - 1])) { + token.json = token.text.indexOf('.') === -1; + } + } else if (this.is('(){}[].,;:?')) { + this.tokens.push({ + index: this.index, + text: this.ch, + json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') + }); + if (this.is('{[')) json.unshift(this.ch); + if (this.is('}]')) json.shift(); + this.index++; + } else if (this.isWhitespace(this.ch)) { + this.index++; + continue; } else { - throwError("Unexpected next character ", index, index+1); + var ch2 = this.ch + this.peek(); + var ch3 = ch2 + this.peek(2); + var fn = OPERATORS[this.ch]; + var fn2 = OPERATORS[ch2]; + var fn3 = OPERATORS[ch3]; + if (fn3) { + this.tokens.push({index: this.index, text: ch3, fn: fn3}); + this.index += 3; + } else if (fn2) { + this.tokens.push({index: this.index, text: ch2, fn: fn2}); + this.index += 2; + } else if (fn) { + this.tokens.push({ + index: this.index, + text: this.ch, + fn: fn, + json: (this.was('[,:') && this.is('+-')) + }); + this.index += 1; + } else { + this.throwError('Unexpected next character ', this.index, this.index + 1); + } } + this.lastCh = this.ch; } - lastCh = ch; - } - return tokens; + return this.tokens; + }, - function is(chars) { - return chars.indexOf(ch) != -1; - } + is: function(chars) { + return chars.indexOf(this.ch) !== -1; + }, - function was(chars) { - return chars.indexOf(lastCh) != -1; - } + was: function(chars) { + return chars.indexOf(this.lastCh) !== -1; + }, - function peek(i) { + peek: function(i) { var num = i || 1; - return index + num < text.length ? text.charAt(index + num) : false; - } - function isNumber(ch) { - return '0' <= ch && ch <= '9'; - } - function isWhitespace(ch) { - return ch == ' ' || ch == '\r' || ch == '\t' || - ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0 - } - function isIdent(ch) { - return 'a' <= ch && ch <= 'z' || - 'A' <= ch && ch <= 'Z' || - '_' == ch || ch == '$'; - } - function isExpOperator(ch) { - return ch == '-' || ch == '+' || isNumber(ch); - } + return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; + }, - function throwError(error, start, end) { - end = end || index; - var colStr = (isDefined(start) ? - "s " + start + "-" + index + " [" + text.substring(start, end) + "]" - : " " + end); - throw $parseMinErr('lexerr', "Lexer Error: {0} at column{1} in expression [{2}].", - error, colStr, text); - } + isNumber: function(ch) { + return ('0' <= ch && ch <= '9'); + }, - function readNumber() { - var number = ""; - var start = index; - while (index < text.length) { - var ch = lowercase(text.charAt(index)); - if (ch == '.' || isNumber(ch)) { + isWhitespace: function(ch) { + return (ch === ' ' || ch === '\r' || ch === '\t' || + ch === '\n' || ch === '\v' || ch === '\u00A0'); // IE treats non-breaking space as \u00A0 + }, + + isIdent: function(ch) { + return ('a' <= ch && ch <= 'z' || + 'A' <= ch && ch <= 'Z' || + '_' === ch || ch === '$'); + }, + + isExpOperator: function(ch) { + return (ch === '-' || ch === '+' || this.isNumber(ch)); + }, + + throwError: function(error, start, end) { + end = end || this.index; + var colStr = (isDefined(start) + ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' + : ' ' + end); + throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', + error, colStr, this.text); + }, + + readNumber: function() { + var number = ''; + var start = this.index; + while (this.index < this.text.length) { + var ch = lowercase(this.text.charAt(this.index)); + if (ch == '.' || this.isNumber(ch)) { number += ch; } else { - var peekCh = peek(); - if (ch == 'e' && isExpOperator(peekCh)) { + var peekCh = this.peek(); + if (ch == 'e' && this.isExpOperator(peekCh)) { number += ch; - } else if (isExpOperator(ch) && - peekCh && isNumber(peekCh) && + } else if (this.isExpOperator(ch) && + peekCh && this.isNumber(peekCh) && number.charAt(number.length - 1) == 'e') { number += ch; - } else if (isExpOperator(ch) && - (!peekCh || !isNumber(peekCh)) && + } else if (this.isExpOperator(ch) && + (!peekCh || !this.isNumber(peekCh)) && number.charAt(number.length - 1) == 'e') { - throwError('Invalid exponent'); + this.throwError('Invalid exponent'); } else { break; } } - index++; + this.index++; } number = 1 * number; - tokens.push({index:start, text:number, json:true, - fn:function() {return number;}}); - } - function readIdent() { - var ident = "", - start = index, - lastDot, peekIndex, methodName, ch; + this.tokens.push({ + index: start, + text: number, + json: true, + fn: function() { return number; } + }); + }, - while (index < text.length) { - ch = text.charAt(index); - if (ch == '.' || isIdent(ch) || isNumber(ch)) { - if (ch == '.') lastDot = index; + readIdent: function() { + var parser = this; + + var ident = ''; + var start = this.index; + + var lastDot, peekIndex, methodName, ch; + + while (this.index < this.text.length) { + ch = this.text.charAt(this.index); + if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { + if (ch === '.') lastDot = this.index; ident += ch; } else { break; } - index++; + this.index++; } //check if this is not a method invocation and if it is back out to last dot if (lastDot) { - peekIndex = index; - while(peekIndex < text.length) { - ch = text.charAt(peekIndex); - if (ch == '(') { + peekIndex = this.index; + while (peekIndex < this.text.length) { + ch = this.text.charAt(peekIndex); + if (ch === '(') { methodName = ident.substr(lastDot - start + 1); ident = ident.substr(0, lastDot - start); - index = peekIndex; + this.index = peekIndex; break; } - if(isWhitespace(ch)) { + if (this.isWhitespace(ch)) { peekIndex++; } else { break; @@ -8115,54 +8595,56 @@ var token = { - index:start, - text:ident + index: start, + text: ident }; + // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn if (OPERATORS.hasOwnProperty(ident)) { - token.fn = token.json = OPERATORS[ident]; + token.fn = OPERATORS[ident]; + token.json = OPERATORS[ident]; } else { - var getter = getterFn(ident, csp, text); + var getter = getterFn(ident, this.options, this.text); token.fn = extend(function(self, locals) { return (getter(self, locals)); }, { assign: function(self, value) { - return setter(self, ident, value, text); + return setter(self, ident, value, parser.text, parser.options); } }); } - tokens.push(token); + this.tokens.push(token); if (methodName) { - tokens.push({ + this.tokens.push({ index:lastDot, text: '.', json: false }); - tokens.push({ + this.tokens.push({ index: lastDot + 1, text: methodName, json: false }); } - } + }, - function readString(quote) { - var start = index; - index++; - var string = ""; + readString: function(quote) { + var start = this.index; + this.index++; + var string = ''; var rawString = quote; var escape = false; - while (index < text.length) { - var ch = text.charAt(index); + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); rawString += ch; if (escape) { - if (ch == 'u') { - var hex = text.substring(index + 1, index + 5); + if (ch === 'u') { + var hex = this.text.substring(this.index + 1, this.index + 5); if (!hex.match(/[\da-f]{4}/i)) - throwError( "Invalid unicode escape [\\u" + hex + "]"); - index += 4; + this.throwError('Invalid unicode escape [\\u' + hex + ']'); + this.index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; @@ -8173,172 +8655,227 @@ } } escape = false; - } else if (ch == '\\') { + } else if (ch === '\\') { escape = true; - } else if (ch == quote) { - index++; - tokens.push({ - index:start, - text:rawString, - string:string, - json:true, - fn:function() { return string; } + } else if (ch === quote) { + this.index++; + this.tokens.push({ + index: start, + text: rawString, + string: string, + json: true, + fn: function() { return string; } }); return; } else { string += ch; } - index++; + this.index++; } - throwError("Unterminated quote", start); + this.throwError('Unterminated quote', start); } -} +}; -///////////////////////////////////////// -function parser(text, json, $filter, csp){ - var ZERO = valueFn(0), - value, - tokens = lex(text, csp), - assignment = _assignment, - functionCall = _functionCall, - fieldAccess = _fieldAccess, - objectIndex = _objectIndex, - filterChain = _filterChain; +/** + * @constructor + */ +var Parser = function (lexer, $filter, options) { + this.lexer = lexer; + this.$filter = $filter; + this.options = options; +}; - if(json){ - // The extra level of aliasing is here, just in case the lexer misses something, so that - // we prevent any accidental execution in JSON. - assignment = logicalOR; - functionCall = - fieldAccess = - objectIndex = - filterChain = - function() { throwError("is not valid json", {text:text, index:0}); }; - value = primary(); - } else { - value = statements(); - } - if (tokens.length !== 0) { - throwError("is an unexpected token", tokens[0]); - } - value.literal = !!value.literal; - value.constant = !!value.constant; - return value; +Parser.ZERO = function () { return 0; }; - /////////////////////////////////// - function throwError(msg, token) { +Parser.prototype = { + constructor: Parser, + + parse: function (text, json) { + this.text = text; + + //TODO(i): strip all the obsolte json stuff from this file + this.json = json; + + this.tokens = this.lexer.lex(text); + + if (json) { + // The extra level of aliasing is here, just in case the lexer misses something, so that + // we prevent any accidental execution in JSON. + this.assignment = this.logicalOR; + + this.functionCall = + this.fieldAccess = + this.objectIndex = + this.filterChain = function() { + this.throwError('is not valid json', {text: text, index: 0}); + }; + } + + var value = json ? this.primary() : this.statements(); + + if (this.tokens.length !== 0) { + this.throwError('is an unexpected token', this.tokens[0]); + } + + value.literal = !!value.literal; + value.constant = !!value.constant; + + return value; + }, + + primary: function () { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else { + var token = this.expect(); + primary = token.fn; + if (!primary) { + this.throwError('not a primary expression', token); + } + if (token.json) { + primary.constant = true; + primary.literal = true; + } + } + + var next, context; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = this.functionCall(primary, context); + context = null; + } else if (next.text === '[') { + context = primary; + primary = this.objectIndex(primary); + } else if (next.text === '.') { + context = primary; + primary = this.fieldAccess(primary); + } else { + this.throwError('IMPOSSIBLE'); + } + } + return primary; + }, + + throwError: function(msg, token) { throw $parseMinErr('syntax', - "Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].", - token.text, msg, (token.index + 1), text, text.substring(token.index)); - } + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, - function peekToken() { - if (tokens.length === 0) - throw $parseMinErr('ueoe', "Unexpected end of expression: {0}", text); - return tokens[0]; - } + peekToken: function() { + if (this.tokens.length === 0) + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + return this.tokens[0]; + }, - function peek(e1, e2, e3, e4) { - if (tokens.length > 0) { - var token = tokens[0]; + peek: function(e1, e2, e3, e4) { + if (this.tokens.length > 0) { + var token = this.tokens[0]; var t = token.text; - if (t==e1 || t==e2 || t==e3 || t==e4 || + if (t === e1 || t === e2 || t === e3 || t === e4 || (!e1 && !e2 && !e3 && !e4)) { return token; } } return false; - } + }, - function expect(e1, e2, e3, e4){ - var token = peek(e1, e2, e3, e4); + expect: function(e1, e2, e3, e4){ + var token = this.peek(e1, e2, e3, e4); if (token) { - if (json && !token.json) { - throwError("is not valid json", token); + if (this.json && !token.json) { + this.throwError('is not valid json', token); } - tokens.shift(); + this.tokens.shift(); return token; } return false; - } + }, - function consume(e1){ - if (!expect(e1)) { - throwError("is unexpected, expecting [" + e1 + "]", peek()); + consume: function(e1){ + if (!this.expect(e1)) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); } - } + }, - function unaryFn(fn, right) { + unaryFn: function(fn, right) { return extend(function(self, locals) { return fn(self, locals, right); }, { constant:right.constant }); - } + }, - function ternaryFn(left, middle, right){ + ternaryFn: function(left, middle, right){ return extend(function(self, locals){ return left(self, locals) ? middle(self, locals) : right(self, locals); }, { constant: left.constant && middle.constant && right.constant }); - } + }, - function binaryFn(left, fn, right) { + binaryFn: function(left, fn, right) { return extend(function(self, locals) { return fn(self, locals, left, right); }, { constant:left.constant && right.constant }); - } + }, - function statements() { + statements: function() { var statements = []; - while(true) { - if (tokens.length > 0 && !peek('}', ')', ';', ']')) - statements.push(filterChain()); - if (!expect(';')) { + while (true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + statements.push(this.filterChain()); + if (!this.expect(';')) { // optimize for the common case where there is only one statement. // TODO(size): maybe we should not support multiple statements? - return statements.length == 1 - ? statements[0] - : function(self, locals){ - var value; - for ( var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) - value = statement(self, locals); - } - return value; - }; + return (statements.length === 1) + ? statements[0] + : function(self, locals) { + var value; + for (var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement) { + value = statement(self, locals); + } + } + return value; + }; } } - } + }, - function _filterChain() { - var left = expression(); + filterChain: function() { + var left = this.expression(); var token; - while(true) { - if ((token = expect('|'))) { - left = binaryFn(left, token.fn, filter()); + while (true) { + if ((token = this.expect('|'))) { + left = this.binaryFn(left, token.fn, this.filter()); } else { return left; } } - } + }, - function filter() { - var token = expect(); - var fn = $filter(token.text); + filter: function() { + var token = this.expect(); + var fn = this.$filter(token.text); var argsFn = []; - while(true) { - if ((token = expect(':'))) { - argsFn.push(expression()); + while (true) { + if ((token = this.expect(':'))) { + argsFn.push(this.expression()); } else { - var fnInvoke = function(self, locals, input){ + var fnInvoke = function(self, locals, input) { var args = [input]; - for ( var i = 0; i < argsFn.length; i++) { + for (var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self, locals)); } return fn.apply(self, args); @@ -8348,297 +8885,259 @@ }; } } - } + }, - function expression() { - return assignment(); - } + expression: function() { + return this.assignment(); + }, - function _assignment() { - var left = ternary(); + assignment: function() { + var left = this.ternary(); var right; var token; - if ((token = expect('='))) { + if ((token = this.expect('='))) { if (!left.assign) { - throwError("implies assignment but [" + - text.substring(0, token.index) + "] can not be assigned to", token); + this.throwError('implies assignment but [' + + this.text.substring(0, token.index) + '] can not be assigned to', token); } - right = ternary(); - return function(scope, locals){ + right = this.ternary(); + return function(scope, locals) { return left.assign(scope, right(scope, locals), locals); }; - } else { - return left; } - } + return left; + }, - function ternary() { - var left = logicalOR(); + ternary: function() { + var left = this.logicalOR(); var middle; var token; - if((token = expect('?'))){ - middle = ternary(); - if((token = expect(':'))){ - return ternaryFn(left, middle, ternary()); + if ((token = this.expect('?'))) { + middle = this.ternary(); + if ((token = this.expect(':'))) { + return this.ternaryFn(left, middle, this.ternary()); + } else { + this.throwError('expected :', token); } - else { - throwError('expected :', token); - } - } - else { + } else { return left; } - } + }, - function logicalOR() { - var left = logicalAND(); + logicalOR: function() { + var left = this.logicalAND(); var token; - while(true) { - if ((token = expect('||'))) { - left = binaryFn(left, token.fn, logicalAND()); + while (true) { + if ((token = this.expect('||'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } else { return left; } } - } + }, - function logicalAND() { - var left = equality(); + logicalAND: function() { + var left = this.equality(); var token; - if ((token = expect('&&'))) { - left = binaryFn(left, token.fn, logicalAND()); + if ((token = this.expect('&&'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } return left; - } + }, - function equality() { - var left = relational(); + equality: function() { + var left = this.relational(); var token; - if ((token = expect('==','!=','===','!=='))) { - left = binaryFn(left, token.fn, equality()); + if ((token = this.expect('==','!=','===','!=='))) { + left = this.binaryFn(left, token.fn, this.equality()); } return left; - } + }, - function relational() { - var left = additive(); + relational: function() { + var left = this.additive(); var token; - if ((token = expect('<', '>', '<=', '>='))) { - left = binaryFn(left, token.fn, relational()); + if ((token = this.expect('<', '>', '<=', '>='))) { + left = this.binaryFn(left, token.fn, this.relational()); } return left; - } + }, - function additive() { - var left = multiplicative(); + additive: function() { + var left = this.multiplicative(); var token; - while ((token = expect('+','-'))) { - left = binaryFn(left, token.fn, multiplicative()); + while ((token = this.expect('+','-'))) { + left = this.binaryFn(left, token.fn, this.multiplicative()); } return left; - } + }, - function multiplicative() { - var left = unary(); + multiplicative: function() { + var left = this.unary(); var token; - while ((token = expect('*','/','%'))) { - left = binaryFn(left, token.fn, unary()); + while ((token = this.expect('*','/','%'))) { + left = this.binaryFn(left, token.fn, this.unary()); } return left; - } + }, - function unary() { + unary: function() { var token; - if (expect('+')) { - return primary(); - } else if ((token = expect('-'))) { - return binaryFn(ZERO, token.fn, unary()); - } else if ((token = expect('!'))) { - return unaryFn(token.fn, unary()); + if (this.expect('+')) { + return this.primary(); + } else if ((token = this.expect('-'))) { + return this.binaryFn(Parser.ZERO, token.fn, this.unary()); + } else if ((token = this.expect('!'))) { + return this.unaryFn(token.fn, this.unary()); } else { - return primary(); + return this.primary(); } - } + }, + fieldAccess: function(object) { + var parser = this; + var field = this.expect().text; + var getter = getterFn(field, this.options, this.text); - function primary() { - var primary; - if (expect('(')) { - primary = filterChain(); - consume(')'); - } else if (expect('[')) { - primary = arrayDeclaration(); - } else if (expect('{')) { - primary = object(); - } else { - var token = expect(); - primary = token.fn; - if (!primary) { - throwError("not a primary expression", token); + return extend(function(scope, locals, self) { + return getter(self || object(scope, locals), locals); + }, { + assign: function(scope, value, locals) { + return setter(object(scope, locals), field, value, parser.text, parser.options); } - if (token.json) { - primary.constant = primary.literal = true; - } - } + }); + }, - var next, context; - while ((next = expect('(', '[', '.'))) { - if (next.text === '(') { - primary = functionCall(primary, context); - context = null; - } else if (next.text === '[') { - context = primary; - primary = objectIndex(primary); - } else if (next.text === '.') { - context = primary; - primary = fieldAccess(primary); - } else { - throwError("IMPOSSIBLE"); - } - } - return primary; - } + objectIndex: function(obj) { + var parser = this; - function _fieldAccess(object) { - var field = expect().text; - var getter = getterFn(field, csp, text); - return extend( - function(scope, locals, self) { - return getter(self || object(scope, locals), locals); - }, - { - assign:function(scope, value, locals) { - return setter(object(scope, locals), field, value, text); - } - } - ); - } + var indexFn = this.expression(); + this.consume(']'); - function _objectIndex(obj) { - var indexFn = expression(); - consume(']'); - return extend( - function(self, locals){ - var o = obj(self, locals), - i = indexFn(self, locals), - v, p; + return extend(function(self, locals) { + var o = obj(self, locals), + i = indexFn(self, locals), + v, p; - if (!o) return undefined; - v = ensureSafeObject(o[i], text); - if (v && v.then) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; + if (!o) return undefined; + v = ensureSafeObject(o[i], parser.text); + if (v && v.then && parser.options.unwrapPromises) { + p = v; + if (!('$$v' in v)) { + p.$$v = undefined; + p.then(function(val) { p.$$v = val; }); } - return v; - }, { - assign:function(self, value, locals){ - var key = indexFn(self, locals); - // prevent overwriting of Function.constructor which would break ensureSafeObject check - return ensureSafeObject(obj(self, locals), text)[key] = value; - } - }); - } + v = v.$$v; + } + return v; + }, { + assign: function(self, value, locals) { + var key = indexFn(self, locals); + // prevent overwriting of Function.constructor which would break ensureSafeObject check + var safe = ensureSafeObject(obj(self, locals), parser.text); + return safe[key] = value; + } + }); + }, - function _functionCall(fn, contextGetter) { + functionCall: function(fn, contextGetter) { var argsFn = []; - if (peekToken().text != ')') { + if (this.peekToken().text !== ')') { do { - argsFn.push(expression()); - } while (expect(',')); + argsFn.push(this.expression()); + } while (this.expect(',')); } - consume(')'); - return function(scope, locals){ - var args = [], - context = contextGetter ? contextGetter(scope, locals) : scope; + this.consume(')'); - for ( var i = 0; i < argsFn.length; i++) { + var parser = this; + + return function(scope, locals) { + var args = []; + var context = contextGetter ? contextGetter(scope, locals) : scope; + + for (var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](scope, locals)); } var fnPtr = fn(scope, locals, context) || noop; - // IE stupidity! + + ensureSafeObject(fnPtr, parser.text); + + // IE stupidity! (IE doesn't have apply for some native functions) var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); + ? fnPtr.apply(context, args) + : fnPtr(args[0], args[1], args[2], args[3], args[4]); - // Check for promise - if (v && v.then) { - var p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } - - return v; + return ensureSafeObject(v, parser.text); }; - } + }, // This is used with json array declaration - function arrayDeclaration () { + arrayDeclaration: function () { var elementFns = []; var allConstant = true; - if (peekToken().text != ']') { + if (this.peekToken().text !== ']') { do { - var elementFn = expression(); + var elementFn = this.expression(); elementFns.push(elementFn); if (!elementFn.constant) { allConstant = false; } - } while (expect(',')); + } while (this.expect(',')); } - consume(']'); - return extend(function(self, locals){ + this.consume(']'); + + return extend(function(self, locals) { var array = []; - for ( var i = 0; i < elementFns.length; i++) { + for (var i = 0; i < elementFns.length; i++) { array.push(elementFns[i](self, locals)); } return array; }, { - literal:true, - constant:allConstant + literal: true, + constant: allConstant }); - } + }, - function object () { + object: function () { var keyValues = []; var allConstant = true; - if (peekToken().text != '}') { + if (this.peekToken().text !== '}') { do { - var token = expect(), + var token = this.expect(), key = token.string || token.text; - consume(":"); - var value = expression(); - keyValues.push({key:key, value:value}); + this.consume(':'); + var value = this.expression(); + keyValues.push({key: key, value: value}); if (!value.constant) { allConstant = false; } - } while (expect(',')); + } while (this.expect(',')); } - consume('}'); - return extend(function(self, locals){ + this.consume('}'); + + return extend(function(self, locals) { var object = {}; - for ( var i = 0; i < keyValues.length; i++) { + for (var i = 0; i < keyValues.length; i++) { var keyValue = keyValues[i]; object[keyValue.key] = keyValue.value(self, locals); } return object; }, { - literal:true, - constant:allConstant + literal: true, + constant: allConstant }); } -} +}; + ////////////////////////////////////////////////// // Parser helper functions ////////////////////////////////////////////////// -function setter(obj, path, setValue, fullExp) { +function setter(obj, path, setValue, fullExp, options) { + //needed? + options = options || {}; + var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { key = ensureSafeMemberName(element.shift(), fullExp); @@ -8648,7 +9147,8 @@ obj[key] = propertyObj; } obj = propertyObj; - if (obj.then) { + if (obj.then && options.unwrapPromises) { + promiseWarning(fullExp); if (!("$$v" in obj)) { (function(promise) { promise.then(function(val) { promise.$$v = val; }); } @@ -8672,76 +9172,106 @@ * - http://jsperf.com/angularjs-parse-getter/4 * - http://jsperf.com/path-evaluation-simplified/7 */ -function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { +function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { ensureSafeMemberName(key0, fullExp); ensureSafeMemberName(key1, fullExp); ensureSafeMemberName(key2, fullExp); ensureSafeMemberName(key3, fullExp); ensureSafeMemberName(key4, fullExp); - return function(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - if (pathVal === null || pathVal === undefined) return pathVal; + return !options.unwrapPromises + ? function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key1 || pathVal === null || pathVal === undefined) return pathVal; + if (pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key0]; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key2 || pathVal === null || pathVal === undefined) return pathVal; + if (!key1 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key1]; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key3 || pathVal === null || pathVal === undefined) return pathVal; + if (!key2 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key2]; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key4 || pathVal === null || pathVal === undefined) return pathVal; + if (!key3 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key3]; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; + if (!key4 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key4]; + + return pathVal; + } + : function cspSafePromiseEnabledGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, + promise; + + if (pathVal === null || pathVal === undefined) return pathVal; + + pathVal = pathVal[key0]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + if (!key1 || pathVal === null || pathVal === undefined) return pathVal; + + pathVal = pathVal[key1]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + if (!key2 || pathVal === null || pathVal === undefined) return pathVal; + + pathVal = pathVal[key2]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + if (!key3 || pathVal === null || pathVal === undefined) return pathVal; + + pathVal = pathVal[key3]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + if (!key4 || pathVal === null || pathVal === undefined) return pathVal; + + pathVal = pathVal[key4]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + return pathVal; + } } -function getterFn(path, csp, fullExp) { +function getterFn(path, options, fullExp) { + // Check whether the cache has this getter already. + // We can use hasOwnProperty directly on the cache because we ensure, + // see below, that the cache never stores a path called 'hasOwnProperty' if (getterFnCache.hasOwnProperty(path)) { return getterFnCache[path]; } @@ -8750,14 +9280,14 @@ pathKeysLength = pathKeys.length, fn; - if (csp) { + if (options.csp) { fn = (pathKeysLength < 6) - ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp) + ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, options) : function(scope, locals) { var i = 0, val; do { val = cspSafeGetterFn( - pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp + pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp, options )(scope, locals); locals = undefined; // clear after first iteration @@ -8776,21 +9306,33 @@ ? 's' // but if we are first then we check locals first, and if so read it first : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - 'if (s && s.then) {\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n'; + (options.unwrapPromises + ? 'if (s && s.then) {\n' + + ' pw("' + fullExp.replace(/\"/g, '\\"') + '");\n' + + ' if (!("$$v" in s)) {\n' + + ' p=s;\n' + + ' p.$$v = undefined;\n' + + ' p.then(function(v) {p.$$v=v;});\n' + + '}\n' + + ' s=s.$$v\n' + + '}\n' + : ''); }); code += 'return s;'; - fn = Function('s', 'k', code); // s=scope, k=locals - fn.toString = function() { return code; }; + + var evaledFnGetter = Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + evaledFnGetter.toString = function() { return code; }; + fn = function(scope, locals) { + return evaledFnGetter(scope, locals, promiseWarning); + }; } - return getterFnCache[path] = fn; + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + if (path !== 'hasOwnProperty') { + getterFnCache[path] = fn; + } + return fn; } /////////////////////////////////// @@ -8834,17 +9376,138 @@ * set to a function to change its value on the given context. * */ + + +/** + * @ngdoc object + * @name ng.$parseProvider + * @function + * + * @description + * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} service. + */ function $ParseProvider() { var cache = {}; - this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + + var $parseOptions = { + csp: false, + unwrapPromises: false, + logPromiseWarnings: true + }; + + + /** + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name ng.$parseProvider#unwrapPromises + * @methodOf ng.$parseProvider + * @description + * + * **This feature is deprecated, see deprecation notes below for more info** + * + * If set to true (default is false), $parse will unwrap promises automatically when a promise is found at any part of + * the expression. In other words, if set to true, the expression will always result in a non-promise value. + * + * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, the fulfillment value + * is used in place of the promise while evaluating the expression. + * + * **Deprecation notice** + * + * This is a feature that didn't prove to be wildly useful or popular, primarily because of the dichotomy between data + * access in templates (accessed as raw values) and controller code (accessed as promises). + * + * In most code we ended up resolving promises manually in controllers anyway and thus unifying the model access there. + * + * Other downsides of automatic promise unwrapping: + * + * - when building components it's often desirable to receive the raw promises + * - adds complexity and slows down expression evaluation + * - makes expression code pre-generation unattractive due to the amount of code that needs to be generated + * - makes IDE auto-completion and tool support hard + * + * **Warning Logs** + * + * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a promise (to reduce + * the noise, each expression is logged only once). To disable this logging use + * `$parseProvider.logPromiseWarnings(false)` api. + * + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as setter. + */ + this.unwrapPromises = function(value) { + if (isDefined(value)) { + $parseOptions.unwrapPromises = !!value; + return this; + } else { + return $parseOptions.unwrapPromises; + } + }; + + + /** + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name ng.$parseProvider#logPromiseWarnings + * @methodOf ng.$parseProvider + * @description + * + * Controls whether Angular should log a warning on any encounter of a promise in an expression. + * + * The default is set to `true`. + * + * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as setter. + */ + this.logPromiseWarnings = function(value) { + if (isDefined(value)) { + $parseOptions.logPromiseWarnings = value; + return this; + } else { + return $parseOptions.logPromiseWarnings; + } + }; + + + this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { + $parseOptions.csp = $sniffer.csp; + + promiseWarning = function promiseWarningFn(fullExp) { + if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; + promiseWarningCache[fullExp] = true; + $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + + 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + }; + return function(exp) { - switch(typeof exp) { + var parsedExpression; + + switch (typeof exp) { case 'string': - return cache.hasOwnProperty(exp) - ? cache[exp] - : cache[exp] = parser(exp, false, $filter, $sniffer.csp); + + if (cache.hasOwnProperty(exp)) { + return cache[exp]; + } + + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + parsedExpression = parser.parse(exp, false); + + if (exp !== 'hasOwnProperty') { + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + cache[exp] = parsedExpression; + } + + return parsedExpression; + case 'function': return exp; + default: return noop; } @@ -9332,8 +9995,8 @@ * @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises. * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, * each value corresponding to the promise at the same index/key in the `promises` array/hash. If any of - * the promises is resolved with a rejection, this resulting promise will be resolved with the - * same rejection. + * the promises is resolved with a rejection, this resulting promise will be rejected with the + * same rejection value. */ function all(promises) { var deferred = defer(), @@ -9422,8 +10085,10 @@ * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. - * All other scopes are child scopes of the root scope. Scopes provide mechanism for watching the model and provide - * event processing life-cycle. See {@link guide/scope developer guide on scopes}. + * All other scopes are descendant scopes of the root scope. Scopes provide separation + * between the model and the view, via a mechanism for watching the model for changes. + * They also provide an event emission/broadcast and subscription facility. See the + * {@link guide/scope developer guide on scopes}. */ function $RootScopeProvider(){ var TTL = 10; @@ -9519,9 +10184,9 @@ * 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. * - * @param {boolean} isolate if true then the scope does not prototypically inherit from the + * @param {boolean} isolate If true, then the scope does not prototypically inherit from the * parent scope. The scope is isolated, as it can not see parent scope properties. - * When creating widgets it is useful for the widget to not accidentally read parent + * When creating widgets, it is useful for the widget to not accidentally read parent * state. * * @returns {Object} The newly created child scope. @@ -9534,12 +10199,12 @@ if (isolate) { child = new Scope(); child.$root = this.$root; - // ensure that there is just one async queue per $rootScope and it's children + // ensure that there is just one async queue per $rootScope and its children child.$$asyncQueue = this.$$asyncQueue; child.$$postDigestQueue = this.$$postDigestQueue; } else { Child = function() {}; // should be anonymous; This is so that when the minifier munges - // the name it does not become random set of chars. These 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. Child.prototype = this; child = new Child(); @@ -9569,7 +10234,7 @@ * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest $digest()} and - * should return the value which will be watched. (Since {@link ng.$rootScope.Scope#$digest $digest()} + * should return the value that will be watched. (Since {@link ng.$rootScope.Scope#$digest $digest()} * reruns when it detects changes the `watchExpression` can execute multiple times per * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) * - The `listener` is called only when the value from the current `watchExpression` and the @@ -9680,13 +10345,13 @@ * * @description * Shallow watches the properties of an object and fires whenever any of the properties change - * (for arrays this implies watching the array items, for object maps this implies watching the properties). - * If a change is detected the `listener` callback is fired. + * (for arrays, this implies watching the array items; for object maps, this implies watching the properties). + * If a change is detected, the `listener` callback is fired. * * - The `obj` collection is observed via standard $watch operation and is examined on every call to $digest() to * see if any items have been added, removed, or moved. - * - The `listener` is called whenever anything within the `obj` has changed. Examples include adding new items - * into the object or array, removing and moving items around. + * - The `listener` is called whenever anything within the `obj` has changed. Examples include adding, removing, + * and moving items belonging to an object or array. * * * # Example @@ -9723,8 +10388,8 @@ * `oldCollection` object is a copy of the former collection data. * The `scope` refers to the current scope. * - * @returns {function()} Returns a de-registration function for this listener. When the de-registration function is executed - * then the internal watch operation is terminated. + * @returns {function()} Returns a de-registration function for this listener. When the de-registration function + * is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { var self = this; @@ -9825,18 +10490,17 @@ * 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 iterations exceeds 10. * - * 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.$compileProvider#directive directives}. - * Instead a call to {@link ng.$rootScope.Scope#$apply $apply()} (typically from within a - * {@link ng.$compileProvider#directive directives}) will force a `$digest()`. + * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within a + * {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, - * you can register a `watchExpression` function with {@link ng.$rootScope.Scope#$watch $watch()} + * you can register a `watchExpression` function with {@link ng.$rootScope.Scope#$watch $watch()} * with no `listener`. * - * You may have a need to call `$digest()` from within unit-tests, to simulate the scope - * life-cycle. + * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * * # Example * <pre> @@ -9869,7 +10533,7 @@ dirty, ttl = TTL, next, current, target = this, watchLog = [], - logIdx, logMsg; + logIdx, logMsg, asyncTask; beginPhase('$digest'); @@ -9879,7 +10543,8 @@ while(asyncQueue.length) { try { - current.$eval(asyncQueue.shift()); + asyncTask = asyncQueue.shift(); + asyncTask.scope.$eval(asyncTask.expression); } catch (e) { $exceptionHandler(e); } @@ -9977,8 +10642,8 @@ * {@link ng.directive:ngRepeat ngRepeat} for managing the * unrolling of the loop. * - * Just before a scope is destroyed a `$destroy` event is broadcasted on this scope. - * Application code can register a `$destroy` event handler that will give it chance to + * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. + * Application code can register a `$destroy` event handler that will give it a chance to * perform any necessary cleanup. * * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to @@ -10010,7 +10675,7 @@ * @function * * @description - * Executes the `expression` on the current scope returning the result. Any exceptions in the + * Executes the `expression` on the current scope and returns the result. Any exceptions in the * expression are propagated (uncaught). This is useful when evaluating Angular expressions. * * # Example @@ -10045,19 +10710,19 @@ * * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that: * - * - it will execute after the function that schedule the evaluation is done running (preferably before DOM rendering). + * - it will execute after the function that scheduled the evaluation (preferably before DOM rendering). * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after `expression` execution. * * Any exceptions from the execution of the expression are forwarded to the * {@link ng.$exceptionHandler $exceptionHandler} service. * - * __Note:__ if this function is called outside of `$digest` cycle, a new $digest cycle will be scheduled. - * It is however encouraged to always call code that changes the model from withing an `$apply` call. + * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle will be scheduled. + * However, it is encouraged to always call code that changes the model from within an `$apply` call. * That includes code evaluated via `$evalAsync`. * * @param {(string|function())=} expression An angular expression to be executed. * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * */ @@ -10072,11 +10737,11 @@ }); } - this.$$asyncQueue.push(expr); + this.$$asyncQueue.push({scope: this, expression: expr}); }, - $$postDigest : function(expr) { - this.$$postDigestQueue.push(expr); + $$postDigest : function(fn) { + this.$$postDigestQueue.push(fn); }, /** @@ -10088,7 +10753,7 @@ * @description * `$apply()` is used to execute an expression in angular from outside of the angular 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-cycle + * Because we are calling into the angular framework we need to perform proper scope life cycle * of {@link ng.$exceptionHandler exception handling}, * {@link ng.$rootScope.Scope#$digest executing watches}. * @@ -10157,7 +10822,7 @@ * * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or `$broadcast`-ed. * - `currentScope` - `{Scope}`: the current scope which is handling the event. - * - `name` - `{string}`: Name of the event. + * - `name` - `{string}`: name of the event. * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel further event * propagation (available only for events that were `$emit`-ed). * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag to true. @@ -10200,7 +10865,7 @@ * * @param {string} name Event name to emit. * @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#$on}). */ $emit: function(name, args) { var empty = [], @@ -10232,12 +10897,14 @@ continue; } try { + //allow all listeners attached to the current scope to run namedListeners[i].apply(null, listenerArgs); - if (stopPropagation) return event; } catch (e) { $exceptionHandler(e); } } + //if any listener on the current scope stops propagation, prevent bubbling + if (stopPropagation) return event; //traverse upwards scope = scope.$parent; } while (scope); @@ -10361,7 +11028,56 @@ JS: 'js' }; +// Helper functions follow. +// Copied from: +// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js... +// Prereq: s is a string. +function escapeForRegexp(s) { + return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). + replace(/\x08/g, '\\x08'); +}; + + +function adjustMatcher(matcher) { + if (matcher === 'self') { + return matcher; + } else if (isString(matcher)) { + // Strings match exactly except for 2 wildcards - '*' and '**'. + // '*' matches any character except those from the set ':/.?&'. + // '**' matches any character (like .* in a RegExp). + // More than 2 *'s raises an error as it's ill defined. + if (matcher.indexOf('***') > -1) { + throw $sceMinErr('iwcard', + 'Illegal sequence *** in string matcher. String: {0}', matcher); + } + matcher = escapeForRegexp(matcher). + replace('\\*\\*', '.*'). + replace('\\*', '[^:/.?&;]*'); + return new RegExp('^' + matcher + '$'); + } else if (isRegExp(matcher)) { + // The only other type of matcher allowed is a Regexp. + // Match entire URL / disallow partial matches. + // Flags are reset (i.e. no global, ignoreCase or multiline) + return new RegExp('^' + matcher.source + '$'); + } else { + throw $sceMinErr('imatcher', + 'Matchers may only be "self", string patterns or RegExp objects'); + } +} + + +function adjustMatchers(matchers) { + var adjustedMatchers = []; + if (isDefined(matchers)) { + forEach(matchers, function(matcher) { + adjustedMatchers.push(adjustMatcher(matcher)); + }); + } + return adjustedMatchers; +} + + /** * @ngdoc service * @name ng.$sceDelegate @@ -10394,13 +11110,37 @@ * @name ng.$sceDelegateProvider * @description * - * The $sceDelegateProvider provider allows developers to configure the {@link ng.$sceDelegate + * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that URLs used for sourcing Angular templates are safe. Refer {@link + * that the URLs used for sourcing Angular templates are safe. Refer {@link * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} * - * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. + * For the general details about this service in Angular, read the main page for {@link ng.$sce + * Strict Contextual Escaping (SCE)}. + * + * **Example**: Consider the following case. <a name="example"></a> + * + * - your app is hosted at url `http://myapp.example.com/` + * - but some of your templates are hosted on other domains you control such as + * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. + * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. + * + * Here is what a secure configuration for this scenario might look like: + * + * <pre class="prettyprint"> + * angular.module('myApp', []).config(function($sceDelegateProvider) { + * $sceDelegateProvider.resourceUrlWhitelist([ + * // Allow same origin resource loads. + * 'self', + * // Allow loading from our assets domain. Notice the difference between * and **. + * 'http://srv*.assets.example.com/**']); + * + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**']); + * }); + * </pre> */ function $SceDelegateProvider() { @@ -10417,28 +11157,25 @@ * @function * * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array. + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. * - * Each element of this array must either be a regex or the special string `'self'`. + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items allowed in + * this array. * - * When a regex is used, it is matched against the normalized / absolute URL of the resource - * being tested. + * Note: **an empty whitelist array will block all URLs**! * - * The **special string** `'self'` can be used to match against all URLs of the same domain as the - * application document with the same protocol (allows sourcing https resources from http documents.) - * - * Please note that **an empty whitelist array will block all URLs**! - * * @return {Array} the currently set whitelist array. * - * The **default value** when no whitelist has been explicitly set is `['self']`. + * The **default value** when no whitelist has been explicitly set is `['self']` allowing only + * same origin resource requests. * * @description * Sets/Gets the whitelist of trusted resource URLs. */ this.resourceUrlWhitelist = function (value) { if (arguments.length) { - resourceUrlWhitelist = value; + resourceUrlWhitelist = adjustMatchers(value); } return resourceUrlWhitelist; }; @@ -10450,14 +11187,12 @@ * @function * * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array. + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. * - * Each element of this array must either be a regex or the special string `'self'` (see - * `resourceUrlWhitelist` for meaning - it's only really useful there.) + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items allowed in + * this array. * - * When a regex is used, it is matched against the normalized / absolute URL of the resource - * being tested. - * * The typical usage for the blacklist is to **block [open redirects](http://cwe.mitre.org/data/definitions/601.html)** * served by your domain as these would otherwise be trusted but actually return content from the redirected * domain. @@ -10475,20 +11210,14 @@ this.resourceUrlBlacklist = function (value) { if (arguments.length) { - resourceUrlBlacklist = value; + resourceUrlBlacklist = adjustMatchers(value); } return resourceUrlBlacklist; }; - // Helper functions for matching resource urls by policy. - function isCompatibleProtocol(documentProtocol, resourceProtocol) { - return ((documentProtocol === resourceProtocol) || - (documentProtocol === "http:" && resourceProtocol === "https:")); - } + this.$get = ['$log', '$document', '$injector', function( + $log, $document, $injector) { - this.$get = ['$log', '$document', '$injector', '$$urlUtils', function( - $log, $document, $injector, $$urlUtils) { - var htmlSanitizer = function htmlSanitizer(html) { throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); }; @@ -10500,14 +11229,15 @@ function matchUrl(matcher, parsedUrl) { if (matcher === 'self') { - return $$urlUtils.isSameOrigin(parsedUrl); + return urlIsSameOrigin(parsedUrl); } else { - return !!parsedUrl.href.match(matcher); + // definitely a regex. See adjustMatchers() + return !!matcher.exec(parsedUrl.href); } } function isResourceUrlAllowedByPolicy(url) { - var parsedUrl = $$urlUtils.resolve(url.toString(), true); + var parsedUrl = urlResolve(url.toString()); var i, n, allowed = false; // Ensure that at least one item from the whitelist allows this url. for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { @@ -10690,9 +11420,9 @@ * # Strict Contextual Escaping * * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context One example of such - * a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer to these - * contexts as privileged or SCE contexts. + * contexts to result in a value that is marked as safe to use for that context. One example of + * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer + * to these contexts as privileged or SCE contexts. * * As of version 1.2, Angular ships with SCE enabled by default. * @@ -10812,10 +11542,55 @@ * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contens are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | * - * ## Show me an example. + * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a> * + * Each element in these arrays must be one of the following: * + * - **'self'** + * - The special **string**, `'self'`, can be used to match against all URLs of the **same + * domain** as the application document using the **same protocol**. + * - **String** (except the special value `'self'`) + * - The string is matched against the full *normalized / absolute URL* of the resource + * being tested (substring matches are not good enough.) + * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters + * match themselves. + * - `*`: matches zero or more occurances of any character other than one of the following 6 + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use + * in a whitelist. + * - `**`: matches zero or more occurances of *any* character. As such, it's not + * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. + * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might + * not have been the intention.) It's usage at the very end of the path is ok. (e.g. + * http://foo.example.com/templates/**). + * - **RegExp** (*see caveat below*) + * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax + * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to + * accidentally introduce a bug when one updates a complex expression (imho, all regexes should + * have good test coverage.). For instance, the use of `.` in the regex is correct only in a + * small number of cases. A `.` character in the regex used when matching the scheme or a + * subdomain could be matched against a `:` or literal `.` that was likely not intended. It + * is highly recommended to use the string patterns and only fall back to regular expressions + * if they as a last resort. + * - The regular expression must be an instance of RegExp (i.e. not a string.) It is + * matched against the **entire** *normalized / absolute URL* of the resource being tested + * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags + * present on the RegExp (such as multiline, global, ignoreCase) are ignored. + * - If you are generating your Javascript from some other templating engine (not + * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), + * remember to escape your regular expression (and be aware that you might need more than + * one level of escaping depending on your templating engine and the way you interpolated + * the value.) Do make use of your platform's escaping mechanism as it might be good + * enough before coding your own. e.g. Ruby has + * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) + * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). + * Javascript lacks a similar built in function for escaping. Take a look at Google + * Closure library's [goog.string.regExpEscape(s)]( + * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js...). * + * Refer {@link ng.$sceDelegateProvider#example $sceDelegateProvider} for an example. + * + * ## Show me an example using SCE. + * * @example <example module="mySceApp"> <file name="index.html"> @@ -11280,7 +12055,7 @@ getTrusted = sce.getTrusted, trustAs = sce.trustAs; - angular.forEach(SCE_CONTEXTS, function (enumValue, name) { + forEach(SCE_CONTEXTS, function (enumValue, name) { var lName = lowercase(name); sce[camelCase("parse_as_" + lName)] = function (expr) { return parse(enumValue, expr); @@ -11411,6 +12186,94 @@ * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this * promise will be resolved with is the return value of the `fn` function. + * + * @example + <doc:example module="time"> + <doc:source> + <script> + function Ctrl2($scope,$timeout) { + $scope.format = 'M/d/yy h:mm:ss a'; + $scope.blood_1 = 100; + $scope.blood_2 = 120; + + var stop; + $scope.fight = function() { + stop = $timeout(function() { + if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { + $scope.blood_1 = $scope.blood_1 - 3; + $scope.blood_2 = $scope.blood_2 - 4; + $scope.fight(); + } else { + $timeout.cancel(stop); + } + }, 100); + }; + + $scope.stopFight = function() { + $timeout.cancel(stop); + }; + + $scope.resetFight = function() { + $scope.blood_1 = 100; + $scope.blood_2 = 120; + } + } + + angular.module('time', []) + // Register the 'myCurrentTime' directive factory method. + // We inject $timeout and dateFilter service since the factory method is DI. + .directive('myCurrentTime', function($timeout, dateFilter) { + // return the directive link function. (compile function not needed) + return function(scope, element, attrs) { + var format, // date format + timeoutId; // timeoutId, so that we can cancel the time updates + + // used to update the UI + function updateTime() { + element.text(dateFilter(new Date(), format)); + } + + // watch the expression, and update the UI on change. + scope.$watch(attrs.myCurrentTime, function(value) { + format = value; + updateTime(); + }); + + // schedule update in one second + function updateLater() { + // save the timeoutId for canceling + timeoutId = $timeout(function() { + updateTime(); // update DOM + updateLater(); // schedule another update + }, 1000); + } + + // listen on DOM destroy (removal) event, and cancel the next UI update + // to prevent updating time ofter the DOM element was removed. + element.bind('$destroy', function() { + $timeout.cancel(timeoutId); + }); + + updateLater(); // kick off the UI update process. + } + }); + </script> + + <div> + <div ng-controller="Ctrl2"> + Date format: <input ng-model="format"> <hr/> + Current time is: <span my-current-time="format"></span> + <hr/> + Blood 1 : <font color='red'>{{blood_1}}</font> + Blood 2 : <font color='red'>{{blood_2}}</font> + <button type="button" data-ng-click="fight()">Fight</button> + <button type="button" data-ng-click="stopFight()">StopFight</button> + <button type="button" data-ng-click="resetFight()">resetFight</button> + </div> + </div> + + </doc:source> + </doc:example> */ function timeout(fn, delay, invokeApply) { var deferred = $q.defer(), @@ -11465,125 +12328,107 @@ }]; } -function $$UrlUtilsProvider() { - this.$get = [function() { - var urlParsingNode = document.createElement("a"), - // NOTE: The usage of window and document instead of $window and $document here is - // deliberate. This service depends on the specific behavior of anchor nodes created by the - // browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and - // cause us to break tests. In addition, when the browser resolves a URL for XHR, it - // doesn't know about mocked locations and resolves URLs to the real document - which is - // exactly the behavior needed here. There is little value is mocking these our for this - // service. - originUrl = resolve(window.location.href, true); +// NOTE: The usage of window and document instead of $window and $document here is +// deliberate. This service depends on the specific behavior of anchor nodes created by the +// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and +// cause us to break tests. In addition, when the browser resolves a URL for XHR, it +// doesn't know about mocked locations and resolves URLs to the real document - which is +// exactly the behavior needed here. There is little value is mocking these out for this +// service. +var urlParsingNode = document.createElement("a"); +var originUrl = urlResolve(window.location.href, true); - /** - * @description - * Normalizes and optionally parses a URL. - * - * NOTE: This is a private service. The API is subject to change unpredictably in any commit. - * - * Implementation Notes for non-IE browsers - * ---------------------------------------- - * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, - * results both in the normalizing and parsing of the URL. Normalizing means that a relative - * URL will be resolved into an absolute URL in the context of the application document. - * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related - * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * - * Implementation Notes for IE - * --------------------------- - * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other - * browsers. However, the parsed components will not be set if the URL assigned did not specify - * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We - * work around that by performing the parsing in a 2nd step by taking a previously normalized - * URL (e.g. by assining to a.href) and assigning it a.href again. This correctly populates the - * properties such as protocol, hostname, port, etc. - * - * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one - * uses the inner HTML approach to assign the URL as part of an HTML snippet - - * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. - * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. - * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that - * method and IE < 8 is unsupported. - * - * References: - * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * http://url.spec.whatwg.org/#urlutils - * https://github.com/angular/angular.js/pull/2902 - * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ - * - * @param {string} url The URL to be parsed. - * @param {boolean=} parse When true, returns an object for the parsed URL. Otherwise, returns - * a single string that is the normalized URL. - * @returns {object|string} When parse is true, returns the normalized URL as a string. - * Otherwise, returns an object with the following members. - * - * | member name | Description | - * |---------------|----------------| - * | href | A normalized version of the provided URL if it was not an absolute URL | - * | protocol | The protocol including the trailing colon | - * | host | The host and port (if the port is non-default) of the normalizedUrl | - * - * These fields from the UrlUtils interface are currently not needed and hence not returned. - * - * | member name | Description | - * |---------------|----------------| - * | hostname | The host without the port of the normalizedUrl | - * | pathname | The path following the host in the normalizedUrl | - * | hash | The URL hash if present | - * | search | The query string | - * - */ - function resolve(url, parse) { - var href = url; - if (msie <= 11) { - // Normalize before parse. Refer Implementation Notes on why this is - // done in two steps on IE. - urlParsingNode.setAttribute("href", href); - href = urlParsingNode.href; - } - urlParsingNode.setAttribute('href', href); +/** + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * URL will be resolved into an absolute URL in the context of the application document. + * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related + * properties are all populated to reflect the normalized URL. This approach has wide + * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * + * Implementation Notes for IE + * --------------------------- + * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other + * browsers. However, the parsed components will not be set if the URL assigned did not specify + * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We + * work around that by performing the parsing in a 2nd step by taking a previously normalized + * URL (e.g. by assining to a.href) and assigning it a.href again. This correctly populates the + * properties such as protocol, hostname, port, etc. + * + * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one + * uses the inner HTML approach to assign the URL as part of an HTML snippet - + * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. + * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. + * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that + * method and IE < 8 is unsupported. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * http://url.spec.whatwg.org/#urlutils + * https://github.com/angular/angular.js/pull/2902 + * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * + * @function + * @param {string} url The URL to be parsed. + * @description Normalizes and parses a URL. + * @returns {object} Returns the normalized URL as a dictionary. + * + * | member name | Description | + * |---------------|----------------| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * | search | The search params, minus the question mark | + * | hash | The hash string, minus the hash symbol + * | hostname | The hostname + * | port | The port, without ":" + * | pathname | The pathname, beginning with "/" + * + */ +function urlResolve(url) { + var href = url; + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + urlParsingNode.setAttribute("href", href); + href = urlParsingNode.href; + } - if (!parse) { - return urlParsingNode.href; - } - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils - return { - href: urlParsingNode.href, - protocol: urlParsingNode.protocol, - host: urlParsingNode.host - // Currently unused and hence commented out. - // hostname: urlParsingNode.hostname, - // port: urlParsingNode.port, - // pathname: urlParsingNode.pathname, - // hash: urlParsingNode.hash, - // search: urlParsingNode.search - }; - } + urlParsingNode.setAttribute('href', href); - return { - resolve: resolve, - /** - * Parse a request URL and determine whether this is a same-origin request as the application document. - * - * @param {string|object} requestUrl The url of the request as a string that will be resolved - * or a parsed URL object. - * @returns {boolean} Whether the request is for the same origin as the application document. - */ - isSameOrigin: function isSameOrigin(requestUrl) { - var parsed = (typeof requestUrl === 'string') ? resolve(requestUrl, true) : requestUrl; - return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); - } - }; - }]; + // $$urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: urlParsingNode.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. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ +function urlIsSameOrigin(requestUrl) { + var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; + return (parsed.protocol === originUrl.protocol && + parsed.host === originUrl.host); +} + +/** * @ngdoc object * @name ng.$window * @@ -11703,15 +12548,32 @@ function $FilterProvider($provide) { var suffix = 'Filter'; + /** + * @ngdoc function + * @name ng.$controllerProvider#register + * @methodOf ng.$controllerProvider + * @param {string|Object} name Name of the filter function, or an object map of filters where + * the keys are the filter names and the values are the filter factories. + * @returns {Object} Registered filter instance, or if a map of filters was provided then a map + * of the registered filter instances. + */ function register(name, factory) { - return $provide.factory(name + suffix, factory); + if(isObject(name)) { + var filters = {}; + forEach(name, function(filter, key) { + filters[key] = register(key, filter); + }); + return filters; + } else { + return $provide.factory(name + suffix, factory); + } } this.register = register; this.$get = ['$injector', function($injector) { return function(name) { return $injector.get(name + suffix); - } + }; }]; //////////////////////////////////////// @@ -11856,7 +12718,7 @@ default: comperator = function(obj, text) { text = (''+text).toLowerCase(); - return (''+obj).toLowerCase().indexOf(text) > -1 + return (''+obj).toLowerCase().indexOf(text) > -1; }; } var search = function(obj, text){ @@ -12196,7 +13058,7 @@ }; var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - NUMBER_STRING = /^\d+$/; + NUMBER_STRING = /^\-?\d+$/; /** * @ngdoc filter @@ -12563,7 +13425,7 @@ <table class="friend"> <tr> <th><a href="" ng-click="predicate = 'name'; reverse=false">Name</a> - (<a href ng-click="predicate = '-name'; reverse=false">^</a>)</th> + (<a href="" ng-click="predicate = '-name'; reverse=false">^</a>)</th> <th><a href="" ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th> <th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</a></th> </tr> @@ -12668,12 +13530,12 @@ * @restrict E * * @description - * Modifies the default behavior of html A tag, so that the default action is prevented when href - * attribute is empty. + * Modifies the default behavior of the html A tag so that the default action is prevented when + * the href attribute is empty. * - * The reasoning for this change is to allow easy creation of action links with `ngClick` directive + * This change permits the easy creation of action links with the `ngClick` directive * without changing the location or causing page reloads, e.g.: - * `<a href="" ng-click="model.$save()">Save</a>` + * `<a href="" ng-click="list.addItem()">Add Item</a>` */ var htmlAnchorDirective = valueFn({ restrict: 'E', @@ -12711,13 +13573,15 @@ * @restrict A * * @description - * Using Angular markup like {{hash}} in an href attribute makes - * the page open to a wrong URL, if the user clicks that link before - * angular has a chance to replace the {{hash}} with actual URL, the - * link will be broken and will most likely return a 404 error. + * Using Angular markup like `{{hash}}` in an href attribute will + * make the link go to the wrong URL if the user clicks it before + * Angular has a chance to replace the `{{hash}}` markup with its + * value. Until Angular replaces the markup the link will be broken + * and will most likely return a 404 error. + * * The `ngHref` directive solves this problem. * - * The buggy way to write it: + * The wrong way to write it: * <pre> * <a href="http://www.gravatar.com/avatar/{{hash}}"/> * </pre> @@ -12731,7 +13595,8 @@ * @param {template} ngHref any string which can contain `{{}}` markup. * * @example - * This example uses `link` variable inside `href` attribute: + * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes + * in links and their different behaviors: <doc:example> <doc:source> <input ng-model="value" /><br /> @@ -12849,10 +13714,10 @@ * </div> * </pre> * - * The HTML specs do not require browsers to preserve the special attributes such as disabled. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngDisabled` directive. + * 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.) + * This prevents the Angular compiler from retrieving the binding expression. + * The `ngDisabled` directive solves this problem for the `disabled` attribute. * * @example <doc:example> @@ -12870,7 +13735,8 @@ </doc:example> * * @element INPUT - * @param {expression} ngDisabled Angular expression that will be evaluated. + * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, + * then special attribute "disabled" will be set on the element */ @@ -12880,10 +13746,10 @@ * @restrict A * * @description - * The HTML specs do not require browsers to preserve the special attributes such as checked. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngChecked` directive. + * 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.) + * This prevents the Angular compiler from retrieving the binding expression. + * The `ngChecked` directive solves this problem for the `checked` attribute. * @example <doc:example> <doc:source> @@ -12900,7 +13766,8 @@ </doc:example> * * @element INPUT - * @param {expression} ngChecked Angular expression that will be evaluated. + * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, + * then special attribute "checked" will be set on the element */ @@ -12910,10 +13777,10 @@ * @restrict A * * @description - * The HTML specs do not require browsers to preserve the special attributes such as readonly. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngReadonly` directive. + * 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.) + * This prevents the Angular compiler from retrieving the binding expression. + * The `ngReadonly` directive solves this problem for the `readonly` attribute. * @example <doc:example> <doc:source> @@ -12930,7 +13797,8 @@ </doc:example> * * @element INPUT - * @param {string} expression Angular expression that will be evaluated. + * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, + * then special attribute "readonly" will be set on the element */ @@ -12940,10 +13808,10 @@ * @restrict A * * @description - * The HTML specs do not require browsers to preserve the special attributes such as selected. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduced the `ngSelected` directive. + * 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.) + * This prevents the Angular compiler from retrieving the binding expression. + * The `ngSelected` directive solves this problem for the `selected` atttribute. * @example <doc:example> <doc:source> @@ -12963,7 +13831,8 @@ </doc:example> * * @element OPTION - * @param {string} expression Angular expression that will be evaluated. + * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, + * then special attribute "selected" will be set on the element */ /** @@ -12972,10 +13841,10 @@ * @restrict A * * @description - * The HTML specs do not require browsers to preserve the special attributes such as open. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngOpen` directive. + * 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.) + * This prevents the Angular compiler from retrieving the binding expression. + * The `ngOpen` directive solves this problem for the `open` attribute. * * @example <doc:example> @@ -12995,7 +13864,8 @@ </doc:example> * * @element DETAILS - * @param {string} expression Angular expression that will be evaluated. + * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, + * then special attribute "open" will be set on the element */ var ngAttributeAliasDirectives = {}; @@ -13118,9 +13988,12 @@ * Input elements using ngModelController do this automatically when they are linked. */ form.$addControl = function(control) { + // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored + // and not added to the scope. Now we throw an error. + assertNotHasOwnProperty(control.$name, 'input'); controls.push(control); - if (control.$name && !form.hasOwnProperty(control.$name)) { + if (control.$name) { form[control.$name] = control; } }; @@ -13262,15 +14135,19 @@ * Directive that instantiates * {@link ng.directive:form.FormController FormController}. * - * If `name` attribute is specified, the form controller is published onto the current scope under + * If the `name` attribute is specified, the form controller is published onto the current scope under * this name. * * # Alias: {@link ng.directive:ngForm `ngForm`} * - * In angular forms can be nested. This means that the outer form is valid when all of the child - * forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this - * reason angular provides {@link ng.directive:ngForm `ngForm`} alias - * which behaves identical to `<form>` but allows form nesting. + * In Angular forms can be nested. This means that the outer form is valid when all of the child + * forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so + * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to + * `<form>` but can be nested. This allows you to have nested forms, which is very useful when + * using Angular validation directives in forms that are dynamically generated using the + * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` + * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an + * `ngForm` directive and nest these in an outer `form` element. * * * # CSS classes @@ -13280,12 +14157,12 @@ * - `ng-dirty` Is set if the form is dirty. * * - * # Submitting a form and preventing default action + * # Submitting a form and preventing the default action * * Since the role of forms in client-side Angular applications is different than in classical * roundtrip apps, it is desirable for the browser not to translate the form submission into a full * page reload that sends the data to the server. Instead some javascript logic should be triggered - * to handle the form submission in application specific way. + * to handle the form submission in an application-specific way. * * For this reason, Angular prevents the default action (form submission to the server) unless the * `<form>` element has an `action` attribute specified. @@ -13297,8 +14174,9 @@ * - {@link ng.directive:ngClick ngClick} directive on the first * button or input field of type submit (input[type=submit]) * - * To prevent double execution of the handler, use only one of ngSubmit or ngClick directives. This - * is because of the following form submission rules coming from the html spec: + * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} + * or {@link ng.directive:ngClick ngClick} directives. + * This is because of the following form submission rules in the HTML specification: * * - If a form has only one input field then hitting enter in this field triggers form submit * (`ngSubmit`) @@ -13347,7 +14225,7 @@ return ['$timeout', function($timeout) { var formDirective = { name: 'form', - restrict: 'E', + restrict: isNgForm ? 'EAC' : 'E', controller: FormController, compile: function() { return { @@ -13396,7 +14274,7 @@ } }; - return isNgForm ? extend(copy(formDirective), {restrict: 'EAC'}) : formDirective; + return formDirective; }]; }; @@ -13787,11 +14665,6 @@ }; -function isEmpty(value) { - return isUndefined(value) || value === '' || value === null || value !== value; -} - - function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { var listener = function() { @@ -13848,7 +14721,7 @@ ctrl.$render = function() { - element.val(isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); + element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; // pattern validator @@ -13857,7 +14730,7 @@ match; var validate = function(regexp, value) { - if (isEmpty(value) || regexp.test(value)) { + if (ctrl.$isEmpty(value) || regexp.test(value)) { ctrl.$setValidity('pattern', true); return value; } else { @@ -13871,7 +14744,7 @@ if (match) { pattern = new RegExp(match[1], match[2]); patternValidator = function(value) { - return validate(pattern, value) + return validate(pattern, value); }; } else { patternValidator = function(value) { @@ -13894,7 +14767,7 @@ if (attr.ngMinlength) { var minlength = int(attr.ngMinlength); var minLengthValidator = function(value) { - if (!isEmpty(value) && value.length < minlength) { + if (!ctrl.$isEmpty(value) && value.length < minlength) { ctrl.$setValidity('minlength', false); return undefined; } else { @@ -13911,7 +14784,7 @@ if (attr.ngMaxlength) { var maxlength = int(attr.ngMaxlength); var maxLengthValidator = function(value) { - if (!isEmpty(value) && value.length > maxlength) { + if (!ctrl.$isEmpty(value) && value.length > maxlength) { ctrl.$setValidity('maxlength', false); return undefined; } else { @@ -13929,7 +14802,7 @@ textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { - var empty = isEmpty(value); + var empty = ctrl.$isEmpty(value); if (empty || NUMBER_REGEXP.test(value)) { ctrl.$setValidity('number', true); return value === '' ? null : (empty ? value : parseFloat(value)); @@ -13940,13 +14813,13 @@ }); ctrl.$formatters.push(function(value) { - return isEmpty(value) ? '' : '' + value; + return ctrl.$isEmpty(value) ? '' : '' + value; }); if (attr.min) { var min = parseFloat(attr.min); var minValidator = function(value) { - if (!isEmpty(value) && value < min) { + if (!ctrl.$isEmpty(value) && value < min) { ctrl.$setValidity('min', false); return undefined; } else { @@ -13962,7 +14835,7 @@ if (attr.max) { var max = parseFloat(attr.max); var maxValidator = function(value) { - if (!isEmpty(value) && value > max) { + if (!ctrl.$isEmpty(value) && value > max) { ctrl.$setValidity('max', false); return undefined; } else { @@ -13977,7 +14850,7 @@ ctrl.$formatters.push(function(value) { - if (isEmpty(value) || isNumber(value)) { + if (ctrl.$isEmpty(value) || isNumber(value)) { ctrl.$setValidity('number', true); return value; } else { @@ -13991,7 +14864,7 @@ textInputType(scope, element, attr, ctrl, $sniffer, $browser); var urlValidator = function(value) { - if (isEmpty(value) || URL_REGEXP.test(value)) { + if (ctrl.$isEmpty(value) || URL_REGEXP.test(value)) { ctrl.$setValidity('url', true); return value; } else { @@ -14008,7 +14881,7 @@ textInputType(scope, element, attr, ctrl, $sniffer, $browser); var emailValidator = function(value) { - if (isEmpty(value) || EMAIL_REGEXP.test(value)) { + if (ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value)) { ctrl.$setValidity('email', true); return value; } else { @@ -14060,6 +14933,11 @@ element[0].checked = ctrl.$viewValue; }; + // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. + ctrl.$isEmpty = function(value) { + return value !== trueValue; + }; + ctrl.$formatters.push(function(value) { return value === trueValue; }); @@ -14246,19 +15124,19 @@ * @description * * `NgModelController` provides API for the `ng-model` directive. The controller contains - * services for data-binding, validation, CSS update, value formatting and parsing. It - * specifically does not contain any logic which deals with DOM rendering or listening to - * DOM events. The `NgModelController` is meant to be extended by other directives where, the - * directive provides DOM manipulation and the `NgModelController` provides the data-binding. - * Note that you cannot use `NgModelController` in a directive with an isolated scope, - * as, in that case, the `ng-model` value gets put into the isolated scope and does not get - * propogated to the parent scope. + * services for data-binding, validation, CSS updates, and value formatting and parsing. It + * purposefully does not contain any logic which deals with DOM rendering or listening to + * DOM events. Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding. * - * + * ## Custom Control Example * This example shows how to use `NgModelController` with a custom control to achieve * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) * collaborate together to achieve the desired result. * + * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element + * contents be edited in place by the user. This will not work on older browsers. + * * <example module="customControl"> <file name="style.css"> [contenteditable] { @@ -14329,6 +15207,39 @@ </file> * </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. + * + * Here is an example of this situation. You'll notice that even though both 'input' and 'div' + * seem to be attached to the same model, they are not kept in synch. + * + * <example module="badIsolatedDirective"> + <file name="script.js"> + angular.module('badIsolatedDirective', []).directive('bad', 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 bad ng-model="someModel"></div> + </file> + * </example> + * + * */ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', function($scope, $exceptionHandler, $attr, $element, $parse) { @@ -14362,6 +15273,25 @@ */ this.$render = noop; + /** + * @ngdoc function + * @name { ng.directive:ngModel.NgModelController#$isEmpty + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * This is called when we need to determine if the value of the input is empty. + * + * For instance, the required directive does this to work out if the input has data or not. + * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. + * + * You can override this for input directives whose concept of being empty is different to the + * default. The `checkboxInputType` directive does this because in its case a value of `false` + * implies empty. + */ + this.$isEmpty = function(value) { + return isUndefined(value) || value === '' || value === null || value !== value; + }; + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid $error = this.$error = {}; // keep invalid keys here @@ -14515,23 +15445,27 @@ * @element input * * @description - * Is a directive that tells Angular to do two-way data binding. It works together with `input`, - * `select`, `textarea` and even custom form controls that use {@link ng.directive:ngModel.NgModelController - * NgModelController} exposed by this directive. + * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a + * property on the scope using {@link ng.directive:ngModel.NgModelController NgModelController}, + * which is created and exposed by this directive. * * `ngModel` is responsible for: * - * - binding the view into the model, which other directives such as `input`, `textarea` or `select` - * require, - * - providing validation behavior (i.e. required, number, email, url), - * - keeping state of the control (valid/invalid, dirty/pristine, validation errors), - * - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`), - * - register the control with parent {@link ng.directive:form form}. + * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` + * require. + * - Providing validation behavior (i.e. required, number, email, url). + * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`). + * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the * current scope. If the property doesn't already exist on this scope, it will be created * implicitly and added to the scope. * + * For best practices on using `ngModel`, see: + * + * - {@link https://github.com/angular/angular.js/wiki/Understanding-Scopes} + * * For basic examples, how to use `ngModel`, see: * * - {@link ng.directive:input input} @@ -14568,7 +15502,6 @@ /** * @ngdoc directive * @name ng.directive:ngChange - * @restrict E * * @description * Evaluate given expression when user changes the input. @@ -14577,6 +15510,8 @@ * Note, this directive requires `ngModel` to be present. * * @element input + * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change + * in input value. * * @example * <doc:example> @@ -14631,7 +15566,7 @@ attr.required = true; // force truthy in case we are on non input element var validator = function(value) { - if (attr.required && (isEmpty(value) || value === false)) { + if (attr.required && ctrl.$isEmpty(value)) { ctrl.$setValidity('required', false); return; } else { @@ -14656,7 +15591,8 @@ * @name ng.directive:ngList * * @description - * Text input that converts between comma-separated string into an array of strings. + * Text input that converts between a delimited string and an array of strings. The delimiter + * can be a fixed string (by default a comma) or a regular expression. * * @element input * @param {string=} ngList optional delimiter that should be used to split the value. If @@ -14691,7 +15627,7 @@ it('should be invalid if empty', function() { input('names').enter(''); - expect(binding('names')).toEqual('[]'); + expect(binding('names')).toEqual(''); expect(binding('myForm.namesInput.$valid')).toEqual('false'); expect(element('span.error').css('display')).not().toBe('none'); }); @@ -14706,6 +15642,9 @@ separator = match && new RegExp(match[1]) || attr.ngList || ','; var parse = function(viewValue) { + // If the viewValue is invalid (say required but empty) it will be `undefined` + if (isUndefined(viewValue)) return; + var list = []; if (viewValue) { @@ -14725,23 +15664,77 @@ return undefined; }); + + // Override the standard $isEmpty because an empty array means the input is empty. + ctrl.$isEmpty = function(value) { + return !value || !value.length; + }; } }; }; var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; - +/** + * @ngdoc directive + * @name ng.directive:ngValue + * + * @description + * Binds the given expression to the value of `input[select]` or `input[radio]`, so + * that when the element is selected, the `ngModel` of that element is set to the + * bound value. + * + * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as + * shown below. + * + * @element input + * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute + * of the `input` element + * + * @example + <doc:example> + <doc:source> + <script> + function Ctrl($scope) { + $scope.names = ['pizza', 'unicorns', 'robots']; + $scope.my = { favorite: 'unicorns' }; + } + </script> + <form ng-controller="Ctrl"> + <h2>Which is your favorite?</h2> + <label ng-repeat="name in names" for="{{name}}"> + {{name}} + <input type="radio" + ng-model="my.favorite" + ng-value="name" + id="{{name}}" + name="favorite"> + </label> + </span> + <div>You chose {{my.favorite}}</div> + </form> + </doc:source> + <doc:scenario> + it('should initialize to model', function() { + expect(binding('my.favorite')).toEqual('unicorns'); + }); + it('should bind the values to the inputs', function() { + input('my.favorite').select('pizza'); + expect(binding('my.favorite')).toEqual('pizza'); + }); + </doc:scenario> + </doc:example> + */ var ngValueDirective = function() { return { priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { - return function(scope, elm, attr) { + return function ngValueConstantLink(scope, elm, attr) { attr.$set('value', scope.$eval(attr.ngValue)); }; } else { - return function(scope, elm, attr) { + return function ngValueLink(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { attr.$set('value', value); }); @@ -14754,6 +15747,7 @@ /** * @ngdoc directive * @name ng.directive:ngBind + * @restrict AC * * @description * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element @@ -14884,11 +15878,15 @@ * @element ANY * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. */ -var ngBindHtmlDirective = ['$sce', function($sce) { +var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { return function(scope, element, attr) { element.addClass('ng-binding').data('$binding', attr.ngBindHtml); - scope.$watch(attr.ngBindHtml, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(value) || ''); + + var parsed = $parse(attr.ngBindHtml); + function getStringValue() { return (parsed(scope) || '').toString(); } + + scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { + element.html($sce.getTrustedHtml(parsed(scope)) || ''); }); }; }]; @@ -14965,9 +15963,10 @@ /** * @ngdoc directive * @name ng.directive:ngClass + * @restrict AC * * @description - * The `ngClass` allows you to set CSS classes on HTML an element, dynamically, by databinding + * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding * an expression that represents all classes to be added. * * The directive won't add duplicate classes if a particular class was already set. @@ -15103,13 +16102,14 @@ /** * @ngdoc directive * @name ng.directive:ngClassOdd + * @restrict AC * * @description * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except it works in - * conjunction with `ngRepeat` and takes affect only on odd (even) rows. + * {@link ng.directive:ngClass ngClass}, except they work in + * conjunction with `ngRepeat` and take effect only on odd (even) rows. * - * This directive can be applied only within a scope of an + * This directive can be applied only within the scope of an * {@link ng.directive:ngRepeat ngRepeat}. * * @element ANY @@ -15150,13 +16150,14 @@ /** * @ngdoc directive * @name ng.directive:ngClassEven + * @restrict AC * * @description * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except it works in - * conjunction with `ngRepeat` and takes affect only on odd (even) rows. + * {@link ng.directive:ngClass ngClass}, except they work in + * conjunction with `ngRepeat` and take effect only on odd (even) rows. * - * This directive can be applied only within a scope of an + * This directive can be applied only within the scope of an * {@link ng.directive:ngRepeat ngRepeat}. * * @element ANY @@ -15197,17 +16198,19 @@ /** * @ngdoc directive * @name ng.directive:ngCloak + * @restrict AC * * @description * The `ngCloak` directive is used to prevent the Angular html template from being briefly * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this * directive to avoid the undesirable flicker effect caused by the html template display. * - * The directive can be applied to the `<body>` element, but typically a fine-grained application is - * preferred in order to benefit from progressive rendering of the browser view. + * The directive can be applied to the `<body>` element, but the preferred usage is to apply + * multiple `ngCloak` directives to small portions of the page to permit progressive rendering + * of the browser view. * - * `ngCloak` works in cooperation with a css rule that is embedded within `angular.js` and - * `angular.min.js` files. Following is the css rule: + * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and + * `angular.min.js`: * * <pre> * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { @@ -15216,17 +16219,17 @@ * </pre> * * When this css rule is loaded by the browser, all html elements (including their children) that - * are tagged with the `ng-cloak` directive are hidden. When Angular comes across this directive - * during the compilation of the template it deletes the `ngCloak` element attribute, which - * makes the compiled element visible. + * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive + * during the compilation of the template it deletes the `ngCloak` element attribute, making + * the compiled element visible. * - * For the best result, `angular.js` script must be loaded in the head section of the html file; - * alternatively, the css rule (above) must be included in the external stylesheet of the + * For the best result, the `angular.js` script must be loaded in the head section of the html + * document; alternatively, the css rule above must be included in the external stylesheet of the * application. * * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css - * class `ngCloak` in addition to `ngCloak` directive as shown in the example below. + * class `ngCloak` in addition to the `ngCloak` directive as shown in the example below. * * @element ANY * @@ -15259,15 +16262,16 @@ * @name ng.directive:ngController * * @description - * The `ngController` directive assigns behavior to a scope. This is a key aspect of how angular + * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular * supports the principles behind the Model-View-Controller design pattern. * * MVC components in angular: * - * * Model — The Model is data in scope properties; scopes are attached to the DOM. - * * View — The template (HTML with data bindings) is rendered into the View. - * * Controller — The `ngController` directive specifies a Controller class; the class has - * methods that typically express the business logic behind the application. + * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties + * are accessed through bindings. + * * View — The template (HTML with data bindings) that is rendered into the View. + * * Controller — The `ngController` directive specifies a Controller class; the class contains business + * logic behind the application to decorate the scope with functions and values * * Note that an alternative way to define controllers is via the {@link ngRoute.$route $route} service. * @@ -15275,8 +16279,8 @@ * @scope * @param {expression} ngController Name of a globally accessible constructor function or an * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. The controller instance can further be published into the scope - * by adding `as localName` the controller name attribute. + * constructor function. The controller instance can be published into a scope property + * by specifying `as propertyName`. * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and @@ -15284,8 +16288,8 @@ * easily be called from the angular markup. Notice that the scope becomes the `this` for the * controller's instance. This allows for easy access to the view data from the controller. Also * notice that any changes to the data are automatically reflected in the View without the need - * for a manual update. The example is included in two different declaration styles based on - * your style preferences. + * for a manual update. The example is shown in two different declaration styles you may use + * according to preference. <doc:example> <doc:source> <script> @@ -15436,12 +16440,12 @@ * For us to be compatible, we just need to implement the "getterFn" in $parse without violating * any of these restrictions. * - * AngularJS uses `Function(string)` generated functions as a speed optimization. By applying `ngCsp` - * it is be possible to opt into the CSP compatible mode. When this mode is on AngularJS will + * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` + * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will * be raised. * - * In order to use this feature put `ngCsp` directive on the root element of the application. + * In order to use this feature put the `ngCsp` directive on the root element of the application. * * @example * This example shows how to apply the `ngCsp` directive to the `html` tag. @@ -15468,8 +16472,8 @@ * @name ng.directive:ngClick * * @description - * The ngClick allows you to specify custom behavior when - * element is clicked. + * The ngClick directive allows you to specify custom behavior when + * an element is clicked. * * @element ANY * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon @@ -15500,7 +16504,7 @@ */ var ngEventDirectives = {}; forEach( - 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur'.split(' '), + 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), function(name) { var directiveName = directiveNormalize('ng-' + name); ngEventDirectives[directiveName] = ['$parse', function($parse) { @@ -15521,11 +16525,11 @@ * @name ng.directive:ngDblclick * * @description - * The `ngDblclick` directive allows you to specify custom behavior on dblclick event. + * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event. * * @element ANY * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon - * dblclick. (Event object is available as `$event`) + * a dblclick. (The Event object is available as `$event`) * * @example * See {@link ng.directive:ngClick ngClick} @@ -15760,36 +16764,80 @@ /** * @ngdoc directive + * @name ng.directive:ngCopy + * + * @description + * Specify custom behavior on copy event. + * + * @element window, input, select, textarea, a + * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon + * copy. (Event object is available as `$event`) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive + * @name ng.directive:ngCut + * + * @description + * Specify custom behavior on cut event. + * + * @element window, input, select, textarea, a + * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon + * cut. (Event object is available as `$event`) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive + * @name ng.directive:ngPaste + * + * @description + * Specify custom behavior on paste event. + * + * @element window, input, select, textarea, a + * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon + * paste. (Event object is available as `$event`) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive * @name ng.directive:ngIf * @restrict A * * @description - * The `ngIf` directive removes and recreates a portion of the DOM tree (HTML) - * conditionally based on **"falsy"** and **"truthy"** values, respectively, evaluated within - * an {expression}. In other words, if the expression assigned to **ngIf evaluates to a false - * value** then **the element is removed from the DOM** and **if true** then **a clone of the - * element is reinserted into the DOM**. + * The `ngIf` directive removes or recreates a portion of the DOM tree based on an + * {expression}. If the expression assigned to `ngIf` evaluates to a false + * value then the element is removed from the DOM, otherwise a clone of the + * element is reinserted into the DOM. * * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the * element in the DOM rather than changing its visibility via the `display` css property. A common * case when this difference is significant is when using css selectors that rely on an element's - * position within the DOM (HTML), such as the `:first-child` or `:last-child` pseudo-classes. + * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. * - * Note that **when an element is removed using ngIf its scope is destroyed** and **a new scope - * is created when the element is restored**. The scope created within `ngIf` inherits from + * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope + * is created when the element is restored. The scope created within `ngIf` inherits from * its parent scope using * {@link https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-I... prototypal inheritance}. * An important implication of this is if `ngModel` is used within `ngIf` to bind to * a javascript primitive defined in the parent scope. In this case any modifications made to the * variable within the child scope will override (hide) the value in the parent scope. * - * Also, `ngIf` recreates elements using their compiled state. An example scenario of this behavior - * is if an element's class attribute is directly modified after it's compiled, using something like + * Also, `ngIf` recreates elements using their compiled state. An example of this behavior + * is if an element's class attribute is directly modified after it's compiled, using something like * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element * the added class will be lost because the original compiled state is used to regenerate the element. * - * Additionally, you can provide animations via the ngAnimate module to animate the **enter** - * and **leave** effects. + * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` + * and `leave` effects. * * @animations * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container @@ -15797,8 +16845,10 @@ * * @element ANY * @scope + * @priority 600 * @param {expression} ngIf If the {@link guide/expression expression} is falsy then - * the element is removed from the DOM tree (HTML). + * the element is removed from the DOM tree. If it is truthy a copy of the compiled + * eleent is added to the DOM tree. * * @example <example animations="true"> @@ -15838,7 +16888,7 @@ var ngIfDirective = ['$animate', function($animate) { return { transclude: 'element', - priority: 1000, + priority: 600, terminal: true, restrict: 'A', compile: function (element, attr, transclude) { @@ -15874,20 +16924,19 @@ * @description * Fetches, compiles and includes an external HTML fragment. * - * Keep in mind that: + * By default, the template URL is restricted to the same domain and protocol as the + * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols + * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or + * {@link ng.$sce#trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link + * ng.$sce Strict Contextual Escaping}. * - * - by default, the template URL is restricted to the same domain and protocol as the - * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl} on it. To load templates from other domains and/or protocols, - * you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or - * {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. Refer Angular's {@link - * ng.$sce Strict Contextual Escaping}. - * - in addition, the browser's - * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHt... - * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing - * (CORS)} policy apply that may further restrict whether the template is successfully loaded. - * (e.g. ngInclude won't work for cross-domain requests on all browsers and for `file://` - * access on some browsers) + * In addition, the browser's + * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHt... + * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing + * (CORS)} policy may further restrict whether the template is successfully loaded. + * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` + * access on some browsers. * * @animations * enter - animation is used to bring new content into the browser. @@ -15896,6 +16945,7 @@ * The enter and leave animation occur concurrently. * * @scope + * @priority 400 * * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, * make sure you wrap it in quotes, e.g. `src="'myPartialTemplate.html'"`. @@ -16019,6 +17069,7 @@ function($http, $templateCache, $anchorScroll, $compile, $animate, $sce) { return { restrict: 'ECA', + priority: 400, terminal: true, transclude: 'element', compile: function(element, attr, transclusion) { @@ -16083,25 +17134,45 @@ /** * @ngdoc directive * @name ng.directive:ngInit + * @restrict AC * * @description - * The `ngInit` directive specifies initialization tasks to be executed - * before the template enters execution mode during bootstrap. + * The `ngInit` directive allows you to evaluate an expression in the + * current scope. * + * <div class="alert alert-error"> + * The only appropriate use of `ngInit` for aliasing special properties of + * {@link api/ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you + * should use {@link guide/dev_guide.mvc.understanding_controller controllers} rather than `ngInit` + * to initialize values on a scope. + * </div> + * * @element ANY * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example <doc:example> <doc:source> - <div ng-init="greeting='Hello'; person='World'"> - {{greeting}} {{person}}! - </div> + <script> + function Ctrl($scope) { + $scope.list = [['a', 'b'], ['c', 'd']]; + } + </script> + <div ng-controller="Ctrl"> + <div ng-repeat="innerList in list" ng-init="outerIndex = $index"> + <div ng-repeat="value in innerList" ng-init="innerIndex = $index"> + <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span> + </div> + </div> + </div> </doc:source> <doc:scenario> - it('should check greeting', function() { - expect(binding('greeting')).toBe('Hello'); - expect(binding('person')).toBe('World'); + it('should alias index positions', function() { + expect(element('.example-init').text()) + .toBe('list[ 0 ][ 0 ] = a;' + + 'list[ 0 ][ 1 ] = b;' + + 'list[ 1 ][ 0 ] = c;' + + 'list[ 1 ][ 1 ] = d;'); }); </doc:scenario> </doc:example> @@ -16119,17 +17190,20 @@ /** * @ngdoc directive * @name ng.directive:ngNonBindable + * @restrict AC * @priority 1000 * * @description - * Sometimes it is necessary to write code which looks like bindings but which should be left alone - * by angular. Use `ngNonBindable` to make angular ignore a chunk of HTML. + * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current + * DOM element. This is useful if the element contains what appears to be Angular directives and + * bindings but which should be ignored by Angular. This could be the case if you have a site that + * displays snippets of code. for instance. * * @element ANY * * @example - * In this example there are two location where a simple binding (`{{}}`) is present, but the one - * wrapped in `ngNonBindable` is left alone. + * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, + * but the one wrapped in `ngNonBindable` is left alone. * * @example <doc:example> @@ -16578,7 +17652,8 @@ return function($scope, $element, $attr){ var expression = $attr.ngRepeat; var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/), - trackByExp, trackByExpGetter, trackByIdFn, trackByIdArrayFn, trackByIdObjFn, lhs, rhs, valueIdentifier, keyIdentifier, + trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, + lhs, rhs, valueIdentifier, keyIdentifier, hashFnLocals = {$id: hashKey}; if (!match) { @@ -16592,7 +17667,7 @@ if (trackByExp) { trackByExpGetter = $parse(trackByExp); - trackByIdFn = function(key, value, index) { + trackByIdExpFn = function(key, value, index) { // assign key, value, and $index to the locals so that they can be used in hash functions if (keyIdentifier) hashFnLocals[keyIdentifier] = key; hashFnLocals[valueIdentifier] = value; @@ -16635,16 +17710,18 @@ childScope, key, value, // key/value of iteration trackById, + trackByIdFn, collectionKeys, block, // last object information {scope, element, id} - nextBlockOrder = []; + nextBlockOrder = [], + elementsToRemove; if (isArrayLike(collection)) { collectionKeys = collection; - trackByIdFn = trackByIdFn || trackByIdArrayFn; + trackByIdFn = trackByIdExpFn || trackByIdArrayFn; } else { - trackByIdFn = trackByIdFn || trackByIdObjFn; + trackByIdFn = trackByIdExpFn || trackByIdObjFn; // if object, extract keys, sort them and use to determine order of iteration over obj props collectionKeys = []; for (key in collection) { @@ -16663,6 +17740,7 @@ key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; trackById = trackByIdFn(key, value, index); + assertNotHasOwnProperty(trackById, '`track by` id'); if(lastBlockMap.hasOwnProperty(trackById)) { block = lastBlockMap[trackById] delete lastBlockMap[trackById]; @@ -16685,10 +17763,12 @@ // remove existing items for (key in lastBlockMap) { + // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn if (lastBlockMap.hasOwnProperty(key)) { block = lastBlockMap[key]; - $animate.leave(block.elements); - forEach(block.elements, function(element) { element[NG_REMOVED] = true}); + elementsToRemove = getBlockElements(block); + $animate.leave(elementsToRemove); + forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); block.scope.$destroy(); } } @@ -16698,6 +17778,7 @@ key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; + if (nextBlockOrder[index - 1]) previousNode = nextBlockOrder[index - 1].endNode; if (block.startNode) { // if we have already seen this object, then we need to reuse the @@ -16713,7 +17794,7 @@ // do nothing } else { // existing item which got moved - $animate.move(block.elements, null, jqLite(previousNode)); + $animate.move(getBlockElements(block), null, jqLite(previousNode)); } previousNode = block.endNode; } else { @@ -16731,11 +17812,11 @@ if (!block.startNode) { linker(childScope, function(clone) { + clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); $animate.enter(clone, null, jqLite(previousNode)); previousNode = clone; block.scope = childScope; - block.startNode = clone[0]; - block.elements = clone; + block.startNode = previousNode && previousNode.endNode ? previousNode.endNode : clone[0]; block.endNode = clone[clone.length - 1]; nextBlockMap[block.id] = block; }); @@ -16746,6 +17827,23 @@ }; } }; + + function getBlockElements(block) { + if (block.startNode === block.endNode) { + return jqLite(block.startNode); + } + + var element = block.startNode; + var elements = [element]; + + do { + element = element.nextSibling; + if (!element) break; + elements.push(element); + } while (element !== block.endNode); + + return jqLite(elements); + } }]; /** @@ -16753,10 +17851,10 @@ * @name ng.directive:ngShow * * @description - * The `ngShow` directive shows and hides the given HTML element conditionally based on the expression - * provided to the ngShow attribute. The show and hide mechanism is a achieved by removing and adding - * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is a predefined CSS class present - * in AngularJS which sets the display style to none (using an !important flag). + * The `ngShow` directive shows or hides the given HTML element based on the expression + * provided to the ngShow attribute. The element is shown or hidden by removing or adding + * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). * * <pre> * <!-- when $scope.myValue is truthy (element is visible) --> @@ -16802,9 +17900,9 @@ * ## A note about animations with ngShow * * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works similar to the animation system present with ngClass, however, the - * only difference is that you must also include the !important flag to override the display property so - * that you can perform an animation when the element is hidden during the time of the animation. + * is true and false. This system works like the animation system present with ngClass except that + * you must also include the !important flag to override the display property + * so that you can perform an animation when the element is hidden during the time of the animation. * * <pre> * // @@ -16905,10 +18003,10 @@ * @name ng.directive:ngHide * * @description - * The `ngHide` directive shows and hides the given HTML element conditionally based on the expression - * provided to the ngHide attribute. The show and hide mechanism is a achieved by removing and adding - * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is a predefined CSS class present - * in AngularJS which sets the display style to none (using an !important flag). + * The `ngHide` directive shows or hides the given HTML element based on the expression + * provided to the ngHide attribute. The element is shown or hidden by removing or adding + * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). * * <pre> * <!-- when $scope.myValue is truthy (element is hidden) --> @@ -16954,8 +18052,8 @@ * ## A note about animations with ngHide * * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works similar to the animation system present with ngClass, however, the - * only difference is that you must also include the !important flag to override the display property so + * is true and false. This system works like the animation system present with ngClass, except that + * you must also include the !important flag to override the display property so * that you can perform an animation when the element is hidden during the time of the animation. * * <pre> @@ -17054,6 +18152,7 @@ /** * @ngdoc directive * @name ng.directive:ngStyle + * @restrict AC * * @description * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. @@ -17117,7 +18216,7 @@ * attribute is displayed. * * @animations - * enter - happens after the ngSwtich 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 * * @usage @@ -17128,6 +18227,7 @@ * </ANY> * * @scope + * @priority 800 * @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>. * @paramDescription * On child elements add: @@ -17262,7 +18362,7 @@ var ngSwitchWhenDirective = ngDirective({ transclude: 'element', - priority: 500, + priority: 800, require: '^ngSwitch', compile: function(element, attrs, transclude) { return function(scope, element, attr, ctrl) { @@ -17274,7 +18374,7 @@ var ngSwitchDefaultDirective = ngDirective({ transclude: 'element', - priority: 500, + priority: 800, require: '^ngSwitch', compile: function(element, attrs, transclude) { return function(scope, element, attr, ctrl) { @@ -17287,6 +18387,7 @@ /** * @ngdoc directive * @name ng.directive:ngTransclude + * @restrict AC * * @description * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. @@ -17335,7 +18436,15 @@ * */ var ngTranscludeDirective = ngDirective({ - controller: ['$transclude', function($transclude) { + controller: ['$element', '$transclude', function($element, $transclude) { + if (!$transclude) { + throw minErr('ngTransclude')('orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag($element)); + } + // remember the transclusion fn but call it during linking so that we don't process transclusion before directives on // the parent element even when the transclusion replaces the current element. (we can't use priority here because // that applies only to compile fns and not controllers @@ -17353,12 +18462,12 @@ /** * @ngdoc directive * @name ng.directive:script + * @restrict E * * @description * Load content of a script tag, with type `text/ng-template`, into `$templateCache`, so that the * template can be used by `ngInclude`, `ngView` or directive templates. * - * @restrict E * @param {'text/ng-template'} type must be set to `'text/ng-template'` * * @example @@ -17395,6 +18504,7 @@ }; }]; +var ngOptionsMinErr = minErr('ngOptions'); /** * @ngdoc directive * @name ng.directive:select @@ -17405,22 +18515,22 @@ * * # `ngOptions` * - * Optionally `ngOptions` attribute can be used to dynamically generate a list of `<option>` - * elements for a `<select>` element using an array or an object obtained by evaluating the - * `ngOptions` expression. + * The `ngOptions` attribute can be used to dynamically generate a list of `<option>` + * elements for the `<select>` element using the array or object obtained by evaluating the + * `ngOptions` comprehension_expression. * - * When an item in the `<select>` menu is selected, the value of array element or object property + * When an item in the `<select>` menu is selected, the array element or object property * represented by the selected option will be bound to the model identified by the `ngModel` - * directive of the parent select element. + * directive. * * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can - * be nested into the `<select>` element. This element will then represent `null` or "not selected" + * be nested into the `<select>` element. This element will then represent the `null` or "not selected" * option. See example below for demonstration. * * Note: `ngOptions` provides iterator facility for `<option>` element which should be used instead * of {@link ng.directive:ngRepeat ngRepeat} when you want the - * `select` model to be bound to a non-string value. This is because an option element can currently - * be bound to string values only. + * `select` model to be bound to a non-string value. This is because an option element can only + * be bound to string values at present. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -17521,8 +18631,8 @@ var ngOptionsDirective = valueFn({ terminal: true }); var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000007777000000000000000000088888 - var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/, + //0000111110000000000022220000000000000000000000333300000000000000444444444444444000000000555555555555555000000066666666666666600000000000000007777000000000000000000088888 + var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/, nullModelCtrl = {$setViewValue: noop}; return { @@ -17543,10 +18653,11 @@ ngModelCtrl = ngModelCtrl_; nullOption = nullOption_; unknownOption = unknownOption_; - } + }; self.addOption = function(value) { + assertNotHasOwnProperty(value, '"option value"'); optionsMap[value] = true; if (ngModelCtrl.$viewValue == value) { @@ -17572,12 +18683,12 @@ $element.prepend(unknownOption); $element.val(unknownVal); unknownOption.prop('selected', true); // needed for IE - } + }; self.hasOption = function(value) { return optionsMap.hasOwnProperty(value); - } + }; $scope.$on('$destroy', function() { // disable unknown option so that we don't do work when the whole select is being destroyed @@ -17695,7 +18806,7 @@ var match; if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { - throw minErr('ngOptions')('iexp', + throw ngOptionsMinErr('iexp', "Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}", optionsExp, startingTag(selectElement)); } @@ -17734,7 +18845,7 @@ var optionGroup, collection = valuesFn(scope) || [], locals = {}, - key, value, optionElement, index, groupIndex, length, groupLength; + key, value, optionElement, index, groupIndex, length, groupLength, trackIndex; if (multiple) { value = []; @@ -17749,7 +18860,7 @@ key = optionElement.val(); if (keyName) locals[keyName] = key; if (trackFn) { - for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) { + for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { locals[valueName] = collection[trackIndex]; if (trackFn(scope, locals) == key) break; } @@ -17768,7 +18879,7 @@ value = null; } else { if (trackFn) { - for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) { + for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { locals[valueName] = collection[trackIndex]; if (trackFn(scope, locals) == key) { value = valueFn(scope, locals); @@ -17801,6 +18912,7 @@ modelValue = ctrl.$modelValue, values = valuesFn(scope) || [], keys = keyName ? sortedKeys(values) : values, + key, groupLength, length, groupIndex, index, locals = {}, @@ -17824,14 +18936,23 @@ // We now build up the list of options we need (we merge later) for (index = 0; length = keys.length, index < length; index++) { - locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index]; - optionGroupName = groupByFn(scope, locals) || ''; + + key = index; + if (keyName) { + key = keys[index]; + if ( key.charAt(0) === '$' ) continue; + locals[keyName] = key; + } + + locals[valueName] = values[key]; + + optionGroupName = groupByFn(scope, locals) || ''; if (!(optionGroup = optionGroups[optionGroupName])) { optionGroup = optionGroups[optionGroupName] = []; optionGroupNames.push(optionGroupName); } if (multiple) { - selected = selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) != undefined; + selected = selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) !== undefined; } else { if (trackFn) { var modelCast = {}; @@ -17949,7 +19070,7 @@ } } } - } + }; }]; var optionDirective = ['$interpolate', function($interpolate) { @@ -17998,7 +19119,7 @@ }); }; } - } + }; }]; var styleDirective = valueFn({ Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-animate.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-animate.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-animate.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -32,7 +32,7 @@ * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | - * | {@link ng.directive:ngShow#animations ngClass} | add and remove | + * | {@link ng.directive:ngClass#animations ngClass} | add and remove | * | {@link ng.directive:ngShow#animations ngShow & ngHide} | add and remove (the ng-hide class value) | * * You can find out more information about animations upon visiting each directive page. @@ -207,6 +207,7 @@ var selectors = $animateProvider.$$selectors; var NG_ANIMATE_STATE = '$$ngAnimateState'; + var NG_ANIMATE_CLASS_NAME = 'ng-animate'; var rootAnimateState = {running:true}; $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope) { @@ -222,8 +223,11 @@ //the empty string value is the default animation //operation which performs CSS transition and keyframe //animations sniffing. This is always included for each - //element animation procedure - classes.push(''); + //element animation procedure if the browser supports + //transitions and/or keyframe animations + if ($sniffer.transitions || $sniffer.animations) { + classes.push(''); + } for(var i=0; i < classes.length; i++) { var klass = classes[i], @@ -288,6 +292,7 @@ * @param {function()=} done callback function that will be called once the animation is complete */ enter : function(element, parent, after, done) { + this.enabled(false, element); $delegate.enter(element, parent, after); $rootScope.$$postDigest(function() { performAnimation('enter', 'ng-enter', element, parent, after, function() { @@ -324,6 +329,8 @@ * @param {function()=} done callback function that will be called once the animation is complete */ leave : function(element, done) { + cancelChildAnimations(element); + this.enabled(false, element); $rootScope.$$postDigest(function() { performAnimation('leave', 'ng-leave', element, null, null, function() { $delegate.leave(element, done); @@ -362,6 +369,8 @@ * @param {function()=} done callback function that will be called once the animation is complete */ move : function(element, parent, after, done) { + cancelChildAnimations(element); + this.enabled(false, element); $delegate.move(element, parent, after); $rootScope.$$postDigest(function() { performAnimation('move', 'ng-move', element, null, null, function() { @@ -452,12 +461,30 @@ * Globally enables/disables animations. * */ - enabled : function(value) { - if (arguments.length) { - rootAnimateState.running = !value; + enabled : function(value, element) { + switch(arguments.length) { + case 2: + if(value) { + cleanup(element); + } + else { + var data = element.data(NG_ANIMATE_STATE) || {}; + data.structural = true; + data.running = true; + element.data(NG_ANIMATE_STATE, data); + } + break; + + case 1: + rootAnimateState.running = !value; + break; + + default: + value = !rootAnimateState.running + break; } - return !rootAnimateState.running; - } + return !!value; + } }; /* @@ -484,52 +511,52 @@ //skip the animation if animations are disabled, a parent is already being animated //or the element is not currently attached to the document body. - if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running) { - //avoid calling done() since there is no need to remove any - //data or className values since this happens earlier than that - //and also use a timeout so that it won't be asynchronous - $timeout(onComplete || noop, 0, false); + if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running || animations.length == 0) { + done(); return; } var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - //if an animation is currently running on the element then lets take the steps - //to cancel that animation and fire any required callbacks + var isClassBased = event == 'addClass' || event == 'removeClass'; if(ngAnimateState.running) { + if(isClassBased && ngAnimateState.structural) { + onComplete && onComplete(); + return; + } + + //if an animation is currently running on the element then lets take the steps + //to cancel that animation and fire any required callbacks + $timeout.cancel(ngAnimateState.flagTimer); cancelAnimations(ngAnimateState.animations); - ngAnimateState.done(); + (ngAnimateState.done || noop)(); } element.data(NG_ANIMATE_STATE, { running:true, + structural:!isClassBased, animations:animations, done:done }); + //the ng-animate class does nothing, but it's here to allow for + //parent animations to find and cancel child animations when needed + element.addClass(NG_ANIMATE_CLASS_NAME); + forEach(animations, function(animation, index) { var fn = function() { progress(index); }; if(animation.start) { - if(event == 'addClass' || event == 'removeClass') { - animation.endFn = animation.start(element, className, fn); - } else { - animation.endFn = animation.start(element, fn); - } + animation.endFn = isClassBased ? + animation.start(element, className, fn) : + animation.start(element, fn); } else { fn(); } }); - function cancelAnimations(animations) { - var isCancelledFlag = true; - forEach(animations, function(animation) { - (animation.endFn || noop)(isCancelledFlag); - }); - } - function progress(index) { animations[index].done = true; (animations[index].endFn || noop)(); @@ -542,118 +569,218 @@ function done() { if(!done.hasBeenRun) { done.hasBeenRun = true; - element.removeData(NG_ANIMATE_STATE); + var data = element.data(NG_ANIMATE_STATE); + if(data) { + /* only structural animations wait for reflow before removing an + animation, but class-based animations don't. An example of this + failing would be when a parent HTML tag has a ng-class attribute + causing ALL directives below to skip animations during the digest */ + if(isClassBased) { + cleanup(element); + } else { + data.flagTimer = $timeout(function() { + cleanup(element); + }, 0, false); + element.data(NG_ANIMATE_STATE, data); + } + } (onComplete || noop)(); } } } + + function cancelChildAnimations(element) { + angular.forEach(element[0].querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) { + element = angular.element(element); + var data = element.data(NG_ANIMATE_STATE); + if(data) { + cancelAnimations(data.animations); + cleanup(element); + } + }); + } + + function cancelAnimations(animations) { + var isCancelledFlag = true; + forEach(animations, function(animation) { + (animation.endFn || noop)(isCancelledFlag); + }); + } + + function cleanup(element) { + element.removeClass(NG_ANIMATE_CLASS_NAME); + element.removeData(NG_ANIMATE_STATE); + } }]); - $animateProvider.register('', ['$window','$sniffer', '$timeout', function($window, $sniffer, $timeout) { - var noop = angular.noop; + $animateProvider.register('', ['$window', '$sniffer', '$timeout', function($window, $sniffer, $timeout) { var forEach = angular.forEach; - //one day all browsers will have these properties - var w3cAnimationProp = 'animation'; - var w3cTransitionProp = 'transition'; + // Detect proper transitionend/animationend event names. + var transitionProp, transitionendEvent, animationProp, animationendEvent; - //but some still use vendor-prefixed styles - var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; - var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + // If unprefixed events are not supported but webkit-prefixed are, use the latter. + // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. + // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` + // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. + // Register both events in case `window.onanimationend` is not supported because of that, + // do the same for `transitionend` as Safari is likely to exhibit similar behavior. + // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit + // therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition + if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) { + transitionProp = 'WebkitTransition'; + transitionendEvent = 'webkitTransitionEnd transitionend'; + } else { + transitionProp = 'transition'; + transitionendEvent = 'transitionend'; + } + if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { + animationProp = 'WebkitAnimation'; + animationendEvent = 'webkitAnimationEnd animationend'; + } else { + animationProp = 'animation'; + animationendEvent = 'animationend'; + } + var durationKey = 'Duration', + propertyKey = 'Property', delayKey = 'Delay', - propertyKey = 'Property', animationIterationCountKey = 'IterationCount', ELEMENT_NODE = 1; - function animate(element, className, done) { - if (!($sniffer.transitions || $sniffer.animations)) { - done(); - return; - } - else if(['ng-enter','ng-leave','ng-move'].indexOf(className) == -1) { - var existingDuration = 0; + var NG_ANIMATE_PARENT_KEY = '$ngAnimateKey'; + var lookupCache = {}; + var parentCounter = 0; + + var animationReflowQueue = [], animationTimer, timeOut = false; + function afterReflow(callback) { + animationReflowQueue.push(callback); + $timeout.cancel(animationTimer); + animationTimer = $timeout(function() { + angular.forEach(animationReflowQueue, function(fn) { + fn(); + }); + animationReflowQueue = []; + animationTimer = null; + lookupCache = {}; + }, 10, false); + } + + function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) { + var data = lookupCache[cacheKey]; + if(!data) { + var transitionDuration = 0, transitionDelay = 0, + animationDuration = 0, animationDelay = 0; + + //we want all the styles defined before and after forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; - existingDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), - parseMaxTime(elementStyles[vendorTransitionProp + durationKey]), - existingDuration); + + transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration); + + if(!onlyCheckTransition) { + transitionDelay = Math.max(parseMaxTime(elementStyles[transitionProp + delayKey]), transitionDelay); + + animationDelay = Math.max(parseMaxTime(elementStyles[animationProp + delayKey]), animationDelay); + + var aDuration = parseMaxTime(elementStyles[animationProp + durationKey]); + + if(aDuration > 0) { + aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey]) || 1; + } + + animationDuration = Math.max(aDuration, animationDuration); + } } }); - if(existingDuration > 0) { - done(); - return; - } + data = { + transitionDelay : transitionDelay, + animationDelay : animationDelay, + transitionDuration : transitionDuration, + animationDuration : animationDuration + }; + lookupCache[cacheKey] = data; } + return data; + } - element.addClass(className); + function parseMaxTime(str) { + var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; + } - //we want all the styles defined before and after - var duration = 0; - forEach(element, function(element) { - if (element.nodeType == ELEMENT_NODE) { - var elementStyles = $window.getComputedStyle(element) || {}; + function getCacheKey(element) { + var parent = element.parent(); + var parentID = parent.data(NG_ANIMATE_PARENT_KEY); + if(!parentID) { + parent.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); + parentID = parentCounter; + } + return parentID + '-' + element[0].className; + } - var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]), - parseMaxTime(elementStyles[vendorTransitionProp + delayKey])); + function animate(element, className, done) { - var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]), - parseMaxTime(elementStyles[vendorAnimationProp + delayKey])); + var cacheKey = getCacheKey(element); + if(getElementAnimationDetails(element, cacheKey, true).transitionDuration > 0) { - var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), - parseMaxTime(elementStyles[vendorTransitionProp + durationKey])); + done(); + return; + } - var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), - parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); + element.addClass(className); - if(animationDuration > 0) { - animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, - parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, - 1); - } + var timings = getElementAnimationDetails(element, cacheKey + ' ' + className); - duration = Math.max(animationDelay + animationDuration, - transitionDelay + transitionDuration, - duration); - } - }); - /* there is no point in performing a reflow if the animation timeout is empty (this would cause a flicker bug normally - in the page */ - if(duration > 0) { - var node = element[0]; + in the page. There is also no point in performing an animation + that only has a delay and no duration */ + var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); + if(maxDuration > 0) { + var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * 1000, + startTime = Date.now(), + node = element[0]; //temporarily disable the transition so that the enter styles //don't animate twice (this is here to avoid a bug in Chrome/FF). - node.style[w3cTransitionProp + propertyKey] = 'none'; - node.style[vendorTransitionProp + propertyKey] = 'none'; + if(timings.transitionDuration > 0) { + node.style[transitionProp + propertyKey] = 'none'; + } var activeClassName = ''; forEach(className.split(' '), function(klass, i) { activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; }); - //this triggers a reflow which allows for the transition animation to kick in - element.prop('clientWidth'); - node.style[w3cTransitionProp + propertyKey] = ''; - node.style[vendorTransitionProp + propertyKey] = ''; - element.addClass(activeClassName); + // This triggers a reflow which allows for the transition animation to kick in. + var css3AnimationEvents = animationendEvent + ' ' + transitionendEvent; - $timeout(done, duration * 1000, false); + afterReflow(function() { + if(timings.transitionDuration > 0) { + node.style[transitionProp + propertyKey] = ''; + } + element.addClass(activeClassName); + }); - //this will automatically be called by $animate so - //there is no need to attach this internally to the - //timeout done method + element.on(css3AnimationEvents, onAnimationProgress); + + // This will automatically be called by $animate so + // there is no need to attach this internally to the + // timeout done method. return function onEnd(cancelled) { + element.off(css3AnimationEvents, onAnimationProgress); element.removeClass(className); element.removeClass(activeClassName); - //only when the animation is cancelled is the done() - //function not called for this animation therefore - //this must be also called + // Only when the animation is cancelled is the done() + // function not called for this animation therefore + // this must be also called. if(cancelled) { done(); } @@ -664,13 +791,22 @@ done(); } - function parseMaxTime(str) { - var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; - forEach(values, function(value) { - total = Math.max(parseFloat(value) || 0, total); - }); - return total; + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * but we're using elapsedTime instead of the timeStamp on the 2nd + * pre-condition since animations sometimes close off early */ + if(Math.max(timeStamp - startTime, 0) >= maxDelayTime && ev.elapsedTime >= maxDuration) { + done(); + } } + } return { Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-cookies.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-cookies.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-cookies.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-loader.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-loader.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-loader.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -16,6 +16,8 @@ function setupModuleLoader(window) { + var $injectorMinErr = minErr('$injector'); + function ensure(obj, name, factory) { return obj[name] || (obj[name] = factory()); } @@ -74,12 +76,13 @@ * @returns {module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + assertNotHasOwnProperty(name, 'module'); if (requires && modules.hasOwnProperty(name)) { modules[name] = null; } return ensure(modules, name, function() { if (!requires) { - throw minErr('$injector')('nomod', "Module '{0}' is not available! You either misspelled the module name " + + throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled the module name " + "or forgot to load it. If registering a module ensure that you specify the dependencies as the second " + "argument.", name); } @@ -182,7 +185,7 @@ * @param {Function} animationFactory Factory function for creating new instance of an animation. * @description * - * **NOTE**: animations are take effect only if the **ngAnimate** module is loaded. + * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. * * * Defines an animation hook that can be later used with {@link ngAnimate.$animate $animate} service and @@ -222,7 +225,8 @@ * @ngdoc method * @name angular.Module#controller * @methodOf angular.Module - * @param {string} name Controller name. + * @param {string|Object} name Controller name, or an object map of controllers where the + * keys are the names and the values are the constructors. * @param {Function} constructor Controller constructor function. * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. @@ -233,7 +237,8 @@ * @ngdoc method * @name angular.Module#directive * @methodOf angular.Module - * @param {string} name directive name + * @param {string|Object} name Directive name, or an object map of directives where the + * keys are the names and the values are the factories. * @param {Function} directiveFactory Factory function for creating new instance of * directives. * @description Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-mocks.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-mocks.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-mocks.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT * @@ -75,6 +75,13 @@ }; + /** + * @name ngMock.$browser#defer.now + * @propertyOf ngMock.$browser + * + * @description + * Current milliseconds mock time. + */ self.defer.now = 0; @@ -119,29 +126,6 @@ } }; - /** - * @name ngMock.$browser#defer.flushNext - * @methodOf ngMock.$browser - * - * @description - * Flushes next pending request and compares it to the provided delay - * - * @param {number=} expectedDelay the delay value that will be asserted against the delay of the next timeout function - */ - self.defer.flushNext = function(expectedDelay) { - var tick = self.deferredFns.shift(); - expect(tick.time).toEqual(expectedDelay); - tick.fn(); - }; - - /** - * @name ngMock.$browser#defer.now - * @propertyOf ngMock.$browser - * - * @description - * Current milliseconds mock time. - */ - self.$$baseHref = ''; self.baseHref = function() { return this.$$baseHref; @@ -454,6 +438,119 @@ }; +/** + * @ngdoc service + * @name ngMock.$interval + * + * @description + * Mock implementation of the $interval service. + * + * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + */ +angular.mock.$IntervalProvider = function() { + this.$get = ['$rootScope', '$q', + function($rootScope, $q) { + var repeatFns = [], + nextRepeatId = 0, + now = 0; + + var $interval = function(fn, delay, count, invokeApply) { + var deferred = $q.defer(), + promise = deferred.promise, + count = (angular.isDefined(count)) ? count : 0, + iteration = 0, + skipApply = (angular.isDefined(invokeApply) && !invokeApply); + + promise.then(null, null, fn); + + promise.$$intervalId = nextRepeatId; + + function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + var fnIndex; + deferred.resolve(iteration); + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + repeatFns.splice(fnIndex, 1); + } + } + + if (!skipApply) $rootScope.$apply(); + }; + + repeatFns.push({ + nextTime:(now + delay), + delay: delay, + fn: tick, + id: nextRepeatId, + deferred: deferred + }); + repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + + nextRepeatId++; + return promise; + }; + + $interval.cancel = function(promise) { + var fnIndex; + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + repeatFns[fnIndex].deferred.reject('canceled'); + repeatFns.splice(fnIndex, 1); + return true; + } + + return false; + }; + + /** + * @ngdoc method + * @name ngMock.$interval#flush + * @methodOf ngMock.$interval + * @description + * + * Runs interval tasks scheduled to be run in the next `millis` milliseconds. + * + * @param {number=} millis maximum timeout amount to flush up until. + * + * @return {number} The amount of time moved forward. + */ + $interval.flush = function(millis) { + now += millis; + while (repeatFns.length && repeatFns[0].nextTime <= now) { + var task = repeatFns[0]; + task.fn(); + task.nextTime += task.delay; + repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + } + return millis; + }; + + return $interval; + }]; +}; + + (function() { var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; @@ -672,7 +769,7 @@ } }; - forEach(['enter','leave','move','addClass','removeClass'], function(method) { + angular.forEach(['enter','leave','move','addClass','removeClass'], function(method) { animate[method] = function() { var params = arguments; animate.queue.push({ @@ -747,7 +844,7 @@ offset = offset || ' '; var log = [offset + 'Scope(' + scope.$id + '): {']; for ( var key in scope ) { - if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { + if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { log.push(' ' + key + ': ' + angular.toJson(scope[key])); } } @@ -1597,6 +1694,7 @@ $browser: angular.mock.$BrowserProvider, $exceptionHandler: angular.mock.$ExceptionHandlerProvider, $log: angular.mock.$LogProvider, + $interval: angular.mock.$IntervalProvider, $httpBackend: angular.mock.$HttpBackendProvider, $rootElement: angular.mock.$RootElementProvider }).config(function($provide) { @@ -1789,7 +1887,7 @@ cache = angular.element.cache; for(key in cache) { - if (cache.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(cache,key)) { var handle = cache[key].handle; handle && angular.element(handle.elem).off(); @@ -1892,8 +1990,40 @@ * instance of {@link AUTO.$injector $injector} per test, which is then used for * resolving references. * - * See also {@link angular.mock.module module} * + * ## Resolving References (Underscore Wrapping) + * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this + * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable + * that is declared in the scope of the `describe()` block. Since we would, most likely, want + * the variable to have the same name of the reference we have a problem, since the parameter + * to the `inject()` function would hide the outer variable. + * + * To help with this, the injected parameters can, optionally, be enclosed with underscores. + * These are ignored by the injector when the reference name is resolved. + * + * For example, the parameter `_myService_` would be resolved as the reference `myService`. + * Since it is available in the function body as _myService_, we can then assign it to a variable + * defined in an outer scope. + * + * ``` + * // Defined out reference variable outside + * var myService; + * + * // Wrap the parameter in underscores + * beforeEach( inject( function(_myService_){ + * myService = _myService_; + * })); + * + * // Use myService in a series of tests. + * it('makes use of myService', function() { + * myService.doStuff(); + * }); + * + * ``` + * + * See also {@link angular.mock.module angular.mock.module} + * + * ## Example * Example of what a typical jasmine tests looks like with the inject method. * <pre> * @@ -1926,11 +2056,11 @@ * inject(function(version) { * expect(version).toEqual('overridden'); * }); - * )); + * }); * }); * * </pre> - * + * * @param {...Function} fns any number of functions which will be injected using the injector. */ window.inject = angular.mock.inject = function() { Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-resource.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-resource.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-resource.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -16,7 +16,7 @@ * * `ngResource` is the name of the optional Angular module that adds support for interacting with * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. - * `ngReource` provides the {@link ngResource.$resource `$resource`} serivce. + * `ngResource` provides the {@link ngResource.$resource `$resource`} service. * * {@installModule resource} * @@ -94,7 +94,7 @@ * caching. * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that * should abort the request when resolved. - * - **`withCredentials`** - `{boolean}` - whether to to set the `withCredentials` flag on the + * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 * requests with credentials} for more information. * - **`responseType`** - `{string}` - see {@link @@ -352,6 +352,9 @@ var urlParams = self.urlParams = {}; forEach(url.split(/\W/), function(param){ + if (param === 'hasOwnProperty') { + throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); + } if (!(new RegExp("^\\d+$").test(param)) && param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { urlParams[param] = true; } @@ -471,7 +474,7 @@ } }); - httpConfig.data = data; + if (hasBody) httpConfig.data = data; route.setUrlParams(httpConfig, extend({}, extractParams(data, action.params || {}), params), action.url); var promise = $http(httpConfig).then(function(response) { @@ -497,8 +500,6 @@ value.$resolved = true; - (success||noop)(value, response.headers); - response.resource = value; return response; @@ -508,8 +509,15 @@ (error||noop)(response); return $q.reject(response); - }).then(responseInterceptor, responseErrorInterceptor); + }); + promise = promise.then( + function(response) { + var value = responseInterceptor(response); + (success||noop)(value, response.headers); + return value; + }, + responseErrorInterceptor); if (!isInstanceCall) { // we are creating instance / collection Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-route.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-route.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-route.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,26 +1,10 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; -var copy = angular.copy, - equals = angular.equals, - extend = angular.extend, - forEach = angular.forEach, - isDefined = angular.isDefined, - isFunction = angular.isFunction, - isString = angular.isString, - jqLite = angular.element, - noop = angular.noop, - toJson = angular.toJson; - - -function inherit(parent, extra) { - return extend(new (extend(function() {}, {prototype:parent}))(), extra); -} - /** * @ngdoc overview * @name ngRoute @@ -49,6 +33,10 @@ * 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 = {}; /** @@ -130,8 +118,8 @@ * 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() - * changes. + * - `[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. @@ -147,7 +135,7 @@ * Adds a new route definition to the `$route` service. */ this.when = function(path, route) { - routes[path] = extend( + routes[path] = angular.extend( {reloadOnSearch: true}, route, path && pathRegExp(path, route) @@ -159,7 +147,7 @@ ? path.substr(0, path.length-1) : path +'/'; - routes[redirectPath] = extend( + routes[redirectPath] = angular.extend( {redirectTo: path}, pathRegExp(redirectPath, route) ); @@ -198,7 +186,9 @@ + (optional ? '' : slash) + '(?:' + (optional ? slash : '') - + (star && '(.+)?' || '([^/]+)?') + ')' + + (star && '(.+?)' || '([^/]+)') + + (optional || '') + + ')' + (optional || ''); }) .replace(/([\/$\*])/g, '\\$1'); @@ -367,6 +357,7 @@ * 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. */ @@ -394,6 +385,7 @@ * @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. @@ -477,9 +469,9 @@ last = $route.current; if (next && last && next.$$route === last.$$route - && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { + && angular.equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { last.params = next.params; - copy(last.params, $routeParams); + angular.copy(last.params, $routeParams); $rootScope.$broadcast('$routeUpdate', last); } else if (next || last) { forceReload = false; @@ -487,7 +479,7 @@ $route.current = next; if (next) { if (next.redirectTo) { - if (isString(next.redirectTo)) { + if (angular.isString(next.redirectTo)) { $location.path(interpolate(next.redirectTo, next.params)).search(next.params) .replace(); } else { @@ -500,29 +492,29 @@ $q.when(next). then(function() { if (next) { - var locals = extend({}, next.resolve), + var locals = angular.extend({}, next.resolve), template, templateUrl; - forEach(locals, function(value, key) { - locals[key] = isString(value) ? $injector.get(value) : $injector.invoke(value); + angular.forEach(locals, function(value, key) { + locals[key] = angular.isString(value) ? $injector.get(value) : $injector.invoke(value); }); - if (isDefined(template = next.template)) { - if (isFunction(template)) { + if (angular.isDefined(template = next.template)) { + if (angular.isFunction(template)) { template = template(next.params); } - } else if (isDefined(templateUrl = next.templateUrl)) { - if (isFunction(templateUrl)) { + } else if (angular.isDefined(templateUrl = next.templateUrl)) { + if (angular.isFunction(templateUrl)) { templateUrl = templateUrl(next.params); } templateUrl = $sce.getTrustedResourceUrl(templateUrl); - if (isDefined(templateUrl)) { + if (angular.isDefined(templateUrl)) { next.loadedTemplateUrl = templateUrl; template = $http.get(templateUrl, {cache: $templateCache}). then(function(response) { return response.data; }); } } - if (isDefined(template)) { + if (angular.isDefined(template)) { locals['$template'] = template; } return $q.all(locals); @@ -533,7 +525,7 @@ if (next == $route.current) { if (next) { next.locals = locals; - copy(next.params, $routeParams); + angular.copy(next.params, $routeParams); } $rootScope.$broadcast('$routeChangeSuccess', next, last); } @@ -552,10 +544,10 @@ function parseRoute() { // Match a route var params, match; - forEach(routes, function(route, path) { + angular.forEach(routes, function(route, path) { if (!match && (params = switchRouteMatcher($location.path(), route))) { match = inherit(route, { - params: extend({}, $location.search(), params), + params: angular.extend({}, $location.search(), params), pathParams: params}); match.$$route = route; } @@ -569,7 +561,7 @@ */ function interpolate(string, params) { var result = []; - forEach((string||'').split(':'), function(segment, i) { + angular.forEach((string||'').split(':'), function(segment, i) { if (i === 0) { result.push(segment); } else { @@ -648,6 +640,7 @@ * The enter and leave animation occur concurrently. * * @scope + * @priority 400 * @example <example module="ngViewExample" deps="angular-route.js" animations="true"> <file name="index.html"> @@ -801,7 +794,7 @@ return { restrict: 'ECA', terminal: true, - priority: 1000, + priority: 400, transclude: 'element', compile: function(element, attr, linker) { return function(scope, $element, attr) { @@ -848,7 +841,7 @@ currentScope[current.controllerAs] = controller; } clone.data('$ngControllerController', controller); - clone.contents().data('$ngControllerController', controller); + clone.children().data('$ngControllerController', controller); } link(currentScope); Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-sanitize.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-sanitize.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-sanitize.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ /** %%Ignore-License - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -140,6 +140,7 @@ BEGIN_TAG_REGEXP = /^</, BEGING_END_TAGE_REGEXP = /^<\s*\//, COMMENT_REGEXP = /<!--(.*?)-->/g, + DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i, CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/i, NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) @@ -215,14 +216,22 @@ // Comment if ( html.indexOf("<!--") === 0 ) { - index = html.indexOf("-->"); + // comments containing -- are not allowed unless they terminate the comment + index = html.indexOf("--", 4); - if ( index >= 0 ) { + if ( index >= 0 && html.lastIndexOf("-->", index) === index) { if (handler.comment) handler.comment( html.substring( 4, index ) ); html = html.substring( index + 3 ); chars = false; } + // DOCTYPE + } else if ( DOCTYPE_REGEXP.test(html) ) { + match = html.match( DOCTYPE_REGEXP ); + if ( match ) { + html = html.replace( match[0] , ''); + chars = false; + } // end tag } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { match = html.match( END_TAG_REGEXP ); Modified: sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-touch.js =================================================================== --- sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-touch.js 2013-09-20 13:29:04 UTC (rev 217) +++ sandbox/nuiton-js-angular/src/main/resources/nuiton-js-angular/extra/angular-touch.js 2013-10-15 08:29:48 UTC (rev 218) @@ -1,5 +1,5 @@ -/** - * @license AngularJS v1.2.0-rc.2 +/** %%Ignore-License + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -108,12 +108,12 @@ totalX = 0; totalY = 0; lastPos = startCoords; - eventHandlers['start'] && eventHandlers['start'](startCoords); + eventHandlers['start'] && eventHandlers['start'](startCoords, event); }); element.on('touchcancel', function(event) { active = false; - eventHandlers['cancel'] && eventHandlers['cancel'](); + eventHandlers['cancel'] && eventHandlers['cancel'](event); }); element.on('touchmove mousemove', function(event) { @@ -141,20 +141,19 @@ if (totalY > totalX) { // Allow native scrolling to take over. active = false; - eventHandlers['cancel'] && eventHandlers['cancel'](); + eventHandlers['cancel'] && eventHandlers['cancel'](event); return; } else { // Prevent the browser from scrolling. event.preventDefault(); - - eventHandlers['move'] && eventHandlers['move'](coords); + eventHandlers['move'] && eventHandlers['move'](coords, event); } }); element.on('touchend mouseup', function(event) { if (!active) return; active = false; - eventHandlers['end'] && eventHandlers['end'](getCoordinates(event)); + eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event); }); } }; @@ -398,7 +397,7 @@ } if (!angular.isDefined(attr.disabled) || attr.disabled === false) { - element.triggerHandler('click', event); + element.triggerHandler('click', [event]); } } @@ -415,9 +414,9 @@ // - On mobile browsers, the simulated "fast" click will call this. // - But the browser's follow-up slow click will be "busted" before it reaches this handler. // Therefore it's safe to use this directive on both mobile and desktop. - element.on('click', function(event) { + element.on('click', function(event, touchend) { scope.$apply(function() { - clickHandler(scope, {$event: event}); + clickHandler(scope, {$event: (touchend || event)}); }); }); @@ -524,18 +523,18 @@ } $swipe.bind(element, { - 'start': function(coords) { + 'start': function(coords, event) { startCoords = coords; valid = true; }, - 'cancel': function() { + 'cancel': function(event) { valid = false; }, - 'end': function(coords) { + 'end': function(coords, event) { if (validSwipe(coords)) { scope.$apply(function() { element.triggerHandler(eventName); - swipeHandler(scope); + swipeHandler(scope, {$event: event}); }); } }
participants (1)
-
echatellier@users.nuiton.org