From 02e4c215ebfb9d5a31a88e13928ac7d84d0c5a69 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 13 Jan 2012 21:04:35 -0800 Subject: [PATCH] upgrade to AnuglarJS 0.10.6 bubblewrap-cape --- .gitignore | 1 + app/index-async.html | 53 + app/index.html | 5 +- app/js/app.js | 17 + app/js/controllers.js | 1 + app/js/filters.js | 8 + app/js/services.js | 21 +- app/js/widgets.js | 10 + app/lib/angular/angular-ie-compat.js | 35 - app/lib/angular/angular-loader.js | 221 + app/lib/angular/angular-loader.min.js | 7 + app/lib/angular/angular.js | 19195 ++++++----- app/lib/angular/angular.min.js | 272 +- app/lib/angular/version.txt | 2 +- app/partials/partial2.html | 4 + test/e2e/scenarios.js | 2 +- test/lib/angular/angular-mocks.js | 1665 +- test/lib/angular/angular-scenario.js | 27266 +++++++++------- test/lib/angular/jstd-scenario-adapter.js | 26 +- test/lib/angular/version.txt | 2 +- .../jasmine-jstd-adapter/JasmineAdapter.js | 73 +- test/lib/jasmine-jstd-adapter/version.txt | 2 +- test/lib/jstestdriver/JsTestDriver.jar | Bin 3827324 -> 4290405 bytes test/lib/jstestdriver/version.txt | 2 +- test/unit/filtersSpec.js | 16 + test/unit/servicesSpec.js | 11 + test/unit/widgetsSpec.js | 16 + 27 files changed, 28535 insertions(+), 20398 deletions(-) create mode 100644 app/index-async.html create mode 100644 app/js/app.js delete mode 100644 app/lib/angular/angular-ie-compat.js create mode 100644 app/lib/angular/angular-loader.js create mode 100644 app/lib/angular/angular-loader.min.js diff --git a/.gitignore b/.gitignore index cb14eaf..b8121a2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build.xml .project .settings +.idea/* diff --git a/app/index-async.html b/app/index-async.html new file mode 100644 index 0000000..ceb24a4 --- /dev/null +++ b/app/index-async.html @@ -0,0 +1,53 @@ + + + + + + my angular app + + + + + + + +
Angular seed app: v
+ + + + diff --git a/app/index.html b/app/index.html index e62bac8..a8ef9b6 100644 --- a/app/index.html +++ b/app/index.html @@ -1,5 +1,5 @@ - + my angular app @@ -13,7 +13,10 @@ +
Angular seed app: v
+ + diff --git a/app/js/app.js b/app/js/app.js new file mode 100644 index 0000000..74ed036 --- /dev/null +++ b/app/js/app.js @@ -0,0 +1,17 @@ +'use strict'; +/* http://docs.angularjs.org/#!angular.service */ + +// Declare app level module which depends on filters, and services +angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.widgets']). + run(['$route', '$window', '$rootScope', function($route, $window, $rootScope) { + + $route.when('/view1', {template: 'partials/partial1.html', controller: MyCtrl1}); + $route.when('/view2', {template: 'partials/partial2.html', controller: MyCtrl2}); + $route.otherwise({redirectTo: '/view1'}); + + var self = this; + + $rootScope.$on('$afterRouteChange', function(){ + $window.scrollTo(0,0); + }); + }]); diff --git a/app/js/controllers.js b/app/js/controllers.js index e7bb8ea..18cb297 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -1,3 +1,4 @@ +'use strict'; /* App Controllers */ diff --git a/app/js/filters.js b/app/js/filters.js index 25be4a4..09f7d77 100644 --- a/app/js/filters.js +++ b/app/js/filters.js @@ -1 +1,9 @@ +'use strict'; /* http://docs.angularjs.org/#!angular.filter */ + +angular.module('myApp.filters', []). + filter('interpolate', ['version', function(version) { + return function(text) { + return String(text).replace(/\%VERSION\%/mg, version); + } + }]); diff --git a/app/js/services.js b/app/js/services.js index 4dcd361..3c48f67 100644 --- a/app/js/services.js +++ b/app/js/services.js @@ -1,18 +1,7 @@ +'use strict'; /* http://docs.angularjs.org/#!angular.service */ -/** - * App service which is responsible for the main configuration of the app. - */ -angular.service('myAngularApp', function($route, $window) { - - $route.when('/view1', {template: 'partials/partial1.html', controller: MyCtrl1}); - $route.when('/view2', {template: 'partials/partial2.html', controller: MyCtrl2}); - $route.otherwise({redirectTo: '/view1'}); - - var self = this; - - self.$on('$afterRouteChange', function(){ - $window.scrollTo(0,0); - }); - -}, {$inject:['$route', '$window'], $eager: true}); +// Demonstrate how to register services +// In this case it is a simple constant service. +angular.module('myApp.services', []). + value('version', '0.1'); diff --git a/app/js/widgets.js b/app/js/widgets.js index 49f12c1..f8c18d6 100644 --- a/app/js/widgets.js +++ b/app/js/widgets.js @@ -1 +1,11 @@ +'use strict'; /* http://docs.angularjs.org/#!angular.widget */ + +angular.module('myApp.widgets', [], function() { + // temporary hack until we have proper directive injection. + angular.directive('app-version', function() { + return ['version', '$element', function(version, element) { + element.text(version); + }]; + }); +}); diff --git a/app/lib/angular/angular-ie-compat.js b/app/lib/angular/angular-ie-compat.js deleted file mode 100644 index 697295c..0000000 --- a/app/lib/angular/angular-ie-compat.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Content-Type: multipart/related; boundary="_" - ---_ -Content-Location:img0 -Content-Transfer-Encoding:base64 - -R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrSLoc/AG8FeUUIN+sGebWAnbKSJodqqlsOxJtqYooU9vvk+vcJIcTkg+QAAA7 ---_ -Content-Location:img1 -Content-Transfer-Encoding:base64 - -R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrCLTcoM29yN6k9socs91e5X3EyJloipYrO4ohTMqA0Fn2XVNswJe+H+SXAAA7 ---_ -Content-Location:img2 -Content-Transfer-Encoding:base64 - -R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA== ---_-- -*/ -(function(){ - var jsUri = document.location.href.replace(/\/[^\/]+(#.*)?$/, '/') + - document.getElementById('ng-ie-compat').src, - css = '#ng-callout .ng-arrow-left{*background-image:url("mhtml:' + jsUri + '!img0")}#ng-callout .ng-arrow-right{*background-image:url("mhtml:' + jsUri + '!img1")}.ng-input-indicator-wait {*background-image:url("mhtml:' + jsUri + '!img2")}', - s = document.createElement('style'); - - s.setAttribute('type', 'text/css'); - - if (s.styleSheet) { - s.styleSheet.cssText = css; - } else { - s.appendChild(document.createTextNode(css)); - } - document.getElementsByTagName('head')[0].appendChild(s); -})(); diff --git a/app/lib/angular/angular-loader.js b/app/lib/angular/angular-loader.js new file mode 100644 index 0000000..cd4d3cf --- /dev/null +++ b/app/lib/angular/angular-loader.js @@ -0,0 +1,221 @@ +/** + * @license AngularJS v0.10.6-5cdfe45a + * (c) 2010-2012 AngularJS http://angularjs.org + * License: MIT + */ + +( + +/** + * @ngdoc interface + * @name angular.Module + * @description + * + * Interface for configuring angular {@link angular.module modules}. + */ + +function setupModuleLoader(window) { + + function ensure(obj, name, factory) { + return obj[name] || (obj[name] = factory()); + } + + return ensure(ensure(window, 'angular', Object), 'module', function() { + /** @type {Object.} */ + var modules = {}; + + /** + * @ngdoc function + * @name angular.module + * @description + * + * The `angular.module` is a global place for registering angular modules. All modules + * (angular core or 3rd party) that should be available to an application must be registered using this mechanism. + * + * # Module + * + * A module is a collocation of services, directives, filters, and configure information. Module is used to configure the, + * {@link angular.module.AUTO.$injector $injector}. + * + *
+     * // Create a new module
+     * var myModule = angular.module('myModule', []);
+     *
+     * // configure a new service
+     * myModule.value('appName', 'MyCoolApp');
+     *
+     * // configure existing services inside initialization blocks.
+     * myModule.init(function($locationProvider) {
+     *   // Configure existing providers
+     *   $locationProvider.hashPrefix = '!';
+     * });
+     * 
+ * + * Then you can load your module like this: + * + *
+     * var injector = angular.injector(['ng', 'MyModule'])
+     * 
+ * + * @param {!string} name The name of the module to create or retrieve. + * @param {Array.=} requires If specified then new module is being created. If unspecified then the + * the module is being retrieved for further configuration. + * @param {Function} initFn Option configuration function for the module. Same as + * {@link angular.Module#init Module.init()}. + * @return {angular.Module} + */ + return function module(name, requires, configFn) { + if (requires && modules.hasOwnProperty(name)) { + modules[name] = null; + } + return ensure(modules, name, function() { + if (!requires) { + throw Error('No module: ' + name); + } + + /** @type {!Array.>} */ + var invokeQueue = []; + + /** @type {!Array.} */ + var runBlocks = []; + + var config = invokeLater('$injector', 'invoke'); + + /** @type {angular.Module} */ + var moduleInstance = { + // Private state + _invokeQueue: invokeQueue, + _runBlocks: runBlocks, + + /** + * @ngdoc property + * @name angular.Module#requires + * @propertyOf angular.Module + * @returns {Array.} List of module names which must be loaded before this module. + * @description + * Holds the list of modules which the injector will load before the current module is loaded. + */ + requires: requires, + + /** + * @ngdoc property + * @name angular.Module#name + * @propertyOf angular.Module + * @returns {string} Name of the module. + * @description + */ + name: name, + + + /** + * @ngdoc method + * @name angular.Module#service + * @methodOf angular.Module + * @param {string} name service name + * @param {Function} providerType Construction function for creating new instance of the service. + * @description + * See {@link angular.module.AUTO.$provide#service $provide.service()}. + */ + service: invokeLater('$provide', 'service'), + + /** + * @ngdoc method + * @name angular.Module#factory + * @methodOf angular.Module + * @param {string} name service name + * @param {Function} providerFunction Function for creating new instance of the service. + * @description + * See {@link angular.module.AUTO.$provide#service $provide.factory()}. + */ + factory: invokeLater('$provide', 'factory'), + + /** + * @ngdoc method + * @name angular.Module#value + * @methodOf angular.Module + * @param {string} name service name + * @param {*} object Service instance object. + * @description + * See {@link angular.module.AUTO.$provide#value $provide.value()}. + */ + value: invokeLater('$provide', 'value'), + + /** + * @ngdoc method + * @name angular.Module#filter + * @methodOf angular.Module + * @param {string} name filterr name + * @param {Function} filterFactory Factory function for creating new instance of filter. + * @description + * See {@link angular.module.ng.$filterProvider#register $filterProvider.register()}. + */ + filter: invokeLater('$filterProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#config + * @methodOf angular.Module + * @param {Function} initializationFn Execute this function on module load. Useful for + * service configuration. + * @description + * Use this method to register work which needs to be performed on module loading. + */ + config: config, + + /** + * @ngdoc method + * @name angular.Module#run + * @methodOf angular.Module + * @param {Function} initializationFn Execute this function after injector creation. + * Useful for application initialization. + * @description + * Use this method to register work which needs to be performed on module loading. + */ + run: function(block) { + runBlocks.push(block); + return this; + } + }; + + if (configFn) { + config(configFn); + } + + return moduleInstance; + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLater(provider, method) { + return function() { + invokeQueue.push([provider, method, arguments]); + return moduleInstance; + } + } + }); + }; + }); + +} +)(window); + +/** + * Closure compiler type information + * + * @typedef { { + * requires: !Array., + * invokeQueue: !Array.>, + * + * service: function(string, Function):angular.Module, + * factory: function(string, Function):angular.Module, + * value: function(string, *):angular.Module, + * + * filter: function(string, Function):angular.Module, + * + * init: function(Function):angular.Module + * } } + */ +angular.Module; + diff --git a/app/lib/angular/angular-loader.min.js b/app/lib/angular/angular-loader.min.js new file mode 100644 index 0000000..28919dc --- /dev/null +++ b/app/lib/angular/angular-loader.min.js @@ -0,0 +1,7 @@ +/* + AngularJS v0.10.6-5cdfe45a + (c) 2010-2012 AngularJS http://angularjs.org + License: MIT +*/ +'use strict';(function(i){function d(a,b,c){return a[b]||(a[b]=c())}return d(d(i,"angular",Object),"module",function(){var a={};return function(b,c,e){c&&a.hasOwnProperty(b)&&(a[b]=null);return d(a,b,function(){function a(b,c){return function(){d.push([b,c,arguments]);return f}}if(!c)throw Error("No module: "+b);var d=[],g=[],h=a("$injector","invoke"),f={_invokeQueue:d,_runBlocks:g,requires:c,name:b,service:a("$provide","service"),factory:a("$provide","factory"),value:a("$provide","value"),filter:a("$filterProvider", +"register"),config:h,run:function(a){g.push(a);return this}};e&&h(e);return f})}})})(window); diff --git a/app/lib/angular/angular.js b/app/lib/angular/angular.js index 9762c62..86b503f 100644 --- a/app/lib/angular/angular.js +++ b/app/lib/angular/angular.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v0.10.0 - * (c) 2010-2011 AngularJS http://angularjs.org + * @license AngularJS v0.10.6-5cdfe45a + * (c) 2010-2012 AngularJS http://angularjs.org * License: MIT */ 'use strict'; @@ -20,7 +20,7 @@ if (typeof document.getAttribute == $undefined) * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ -var lowercase = function (string){ return isString(string) ? string.toLowerCase() : string; }; +var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; /** @@ -32,17 +32,17 @@ var lowercase = function (string){ return isString(string) ? string.toLowerCase( * @param {string} string String to be converted to uppercase. * @returns {string} Uppercased string. */ -var uppercase = function (string){ return isString(string) ? string.toUpperCase() : string; }; +var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;}; -var manualLowercase = function (s) { +var manualLowercase = function(s) { return isString(s) - ? s.replace(/[A-Z]/g, function (ch) {return fromCharCode(ch.charCodeAt(0) | 32); }) + ? s.replace(/[A-Z]/g, function(ch) {return fromCharCode(ch.charCodeAt(0) | 32);}) : s; }; -var manualUppercase = function (s) { +var manualUppercase = function(s) { return isString(s) - ? s.replace(/[a-z]/g, function (ch) {return fromCharCode(ch.charCodeAt(0) & ~32); }) + ? s.replace(/[a-z]/g, function(ch) {return fromCharCode(ch.charCodeAt(0) & ~32);}) : s; }; @@ -55,31 +55,33 @@ if ('i' !== 'I'.toLowerCase()) { uppercase = manualUppercase; } -function fromCharCode(code) { return String.fromCharCode(code); } +function fromCharCode(code) {return String.fromCharCode(code);} +/** + * Creates the element for IE8 and below to allow styling of widgets + * (http://ejohn.org/blog/html5-shiv/). This hack works only if angular is + * included synchronously at the top of the document before IE sees any + * unknown elements. See regression/issue-584.html. + * + * @param {string} elementName Name of the widget. + * @returns {string} Lowercased string. + */ +function shivForIE(elementName) { + elementName = lowercase(elementName); + if (msie < 9 && elementName.charAt(0) != '@') { // ignore attr-widgets + document.createElement(elementName); + } + return elementName; +} -var _undefined = undefined, - _null = null, - $$scope = '$scope', - $$validate = '$validate', - $angular = 'angular', - $array = 'array', +var $$scope = '$scope', $boolean = 'boolean', $console = 'console', - $date = 'date', $length = 'length', $name = 'name', - $noop = 'noop', - $null = 'null', - $number = 'number', $object = 'object', $string = 'string', - $value = 'value', - $selected = 'selected', $undefined = 'undefined', - NG_EXCEPTION = 'ng-exception', - NG_VALIDATION_ERROR = 'ng-validation-error', - NOOP = 'noop', Error = window.Error, /** holds major version number for IE or NaN for real browsers */ msie = parseInt((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1], 10), @@ -87,31 +89,25 @@ var _undefined = undefined, jQuery, // delay binding slice = [].slice, push = [].push, + toString = Object.prototype.toString, error = window[$console] ? bind(window[$console], window[$console]['error'] || noop) : noop, /** @name angular */ - angular = window[$angular] || (window[$angular] = {}), + angular = window.angular || (window.angular = {}), + angularModule = null, /** @name angular.markup */ angularTextMarkup = extensionMap(angular, 'markup'), /** @name angular.attrMarkup */ angularAttrMarkup = extensionMap(angular, 'attrMarkup'), /** @name angular.directive */ - angularDirective = extensionMap(angular, 'directive'), + angularDirective = extensionMap(angular, 'directive', lowercase), /** @name angular.widget */ - angularWidget = extensionMap(angular, 'widget', lowercase), - /** @name angular.validator */ - angularValidator = extensionMap(angular, 'validator'), - /** @name angular.fileter */ - angularFilter = extensionMap(angular, 'filter'), - /** @name angular.formatter */ - angularFormatter = extensionMap(angular, 'formatter'), - /** @name angular.service */ - angularService = extensionMap(angular, 'service'), - angularCallbacks = extensionMap(angular, 'callbacks'), + angularWidget = extensionMap(angular, 'widget', shivForIE), + /** @name angular.module.ng */ + angularInputType = extensionMap(angular, 'inputType', lowercase), nodeName_, - rngScript = /^(|.*\/)angular(-.*?)?(\.min)?.js(\?[^#]*)?(#(.*))?$/, uid = ['0', '0', '0'], DATE_ISOSTRING_LN = 24; @@ -138,7 +134,7 @@ var _undefined = undefined, * * @param {Object|Array} obj Object to iterate over. - * @param {function()} iterator Iterator function. + * @param {Function} iterator Iterator function. * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ @@ -164,10 +160,18 @@ function forEach(obj, iterator, context) { return obj; } -function forEachSorted(obj, iterator, context) { +function sortedKeys(obj) { var keys = []; - for (var key in obj) keys.push(key); - keys.sort(); + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + keys.push(key); + } + } + return keys.sort(); +} + +function forEachSorted(obj, iterator, context) { + var keys = sortedKeys(obj) for ( var i = 0; i < keys.length; i++) { iterator.call(context, obj[keys[i]], keys[i]); } @@ -175,20 +179,16 @@ function forEachSorted(obj, iterator, context) { } -function formatError(arg) { - if (arg instanceof Error) { - if (arg.stack) { - arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? - 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; - } else if (arg.sourceURL) { - arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; - } - } - return arg; +/** + * when using forEach the params are value, key, but it is often useful to have key, value. + * @param {function(string, *)} iteratorFn + * @returns {function(*, string)} + */ +function reverseParams(iteratorFn) { + return function(value, key) { iteratorFn(key, value) }; } /** - * @description * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric * characters such as '012ABC'. The reason why we are not using simply a number counter is that * the number string gets longer over time, and it can also overflow, where as the the nextId @@ -243,7 +243,7 @@ function extend(dst) { function inherit(parent, extra) { - return extend(new (extend(function(){}, {prototype:parent}))(), extra); + return extend(new (extend(function() {}, {prototype:parent}))(), extra); } @@ -263,6 +263,7 @@ function inherit(parent, extra) { */ function noop() {} +noop.$inject = []; /** @@ -281,13 +282,14 @@ function noop() {} */ function identity($) {return $;} +identity.$inject = []; -function valueFn(value) {return function(){ return value; };} +function valueFn(value) {return function() {return value;};} function extensionMap(angular, name, transform) { var extPoint; - return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ + return angular[name] || (extPoint = angular[name] = function(name, fn, prop){ name = (transform || identity)(name); if (isDefined(fn)) { extPoint[name] = extend(fn, prop || {}); @@ -307,7 +309,7 @@ function extensionMap(angular, name, transform) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ -function isUndefined(value){ return typeof value == $undefined; } +function isUndefined(value){return typeof value == $undefined;} /** @@ -321,7 +323,7 @@ function isUndefined(value){ return typeof value == $undefined; } * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ -function isDefined(value){ return typeof value != $undefined; } +function isDefined(value){return typeof value != $undefined;} /** @@ -336,7 +338,7 @@ function isDefined(value){ return typeof value != $undefined; } * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -function isObject(value){ return value!=null && typeof value == $object;} +function isObject(value){return value!=null && typeof value == $object;} /** @@ -350,7 +352,7 @@ function isObject(value){ return value!=null && typeof value == $object;} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ -function isString(value){ return typeof value == $string;} +function isString(value){return typeof value == $string;} /** @@ -364,7 +366,7 @@ function isString(value){ return typeof value == $string;} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ -function isNumber(value){ return typeof value == $number;} +function isNumber(value){return typeof value == 'number';} /** @@ -378,7 +380,9 @@ function isNumber(value){ return typeof value == $number;} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ -function isDate(value){ return value instanceof Date; } +function isDate(value){ + return toString.apply(value) == '[object Date]'; +} /** @@ -392,7 +396,9 @@ function isDate(value){ return value instanceof Date; } * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -function isArray(value) { return value instanceof Array; } +function isArray(value) { + return toString.apply(value) == '[object Array]'; +} /** @@ -406,7 +412,7 @@ function isArray(value) { return value instanceof Array; } * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ -function isFunction(value){ return typeof value == 'function';} +function isFunction(value){return typeof value == 'function';} /** @@ -420,13 +426,30 @@ function isWindow(obj) { return obj && obj.document && obj.location && obj.alert && obj.setInterval; } -function isBoolean(value) { return typeof value == $boolean; } -function isTextNode(node) { return nodeName_(node) == '#text'; } + +function isScope(obj) { + return obj && obj.$evalAsync && obj.$watch; +} + + +function isBoolean(value) {return typeof value == $boolean;} +function isTextNode(node) {return nodeName_(node) == '#text';} function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; } +/** + * @ngdoc function + * @name angular.isElement + * @function + * + * @description + * Determines if a reference is a DOM element (or wrapped jQuery element). + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). + */ function isElement(node) { return node && (node.nodeName // we are a direct element @@ -494,10 +517,6 @@ function map(obj, iterator, context) { /** - * @ngdoc function - * @name angular.Object.size - * @function - * * @description * Determines the number of elements in an array, the number of properties an object has, or * the length of a string. @@ -508,29 +527,6 @@ function map(obj, iterator, context) { * @param {Object|Array|string} obj Object, array, or string to inspect. * @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. - * - * @example - * - * - * - *
- * Number of items in array: {{ [1,2].$size() }}
- * Number of items in object: {{ {a:1, b:2, c:3}.$size() }}
- * String length: {{fooStringLength}} - *
- *
- * - * it('should print correct sizes for an array and an object', function() { - * expect(binding('[1,2].$size()')).toBe('2'); - * expect(binding('{a:1, b:2, c:3}.$size()')).toBe('3'); - * expect(binding('fooStringLength')).toBe('3'); - * }); - * - *
*/ function size(obj, ownPropsOnly) { var size = 0, key; @@ -561,6 +557,13 @@ function indexOf(array, obj) { return -1; } +function arrayRemove(array, value) { + var index = indexOf(array, value); + if (index >=0) + array.splice(index, 1); + return value; +} + function isLeafNode (node) { if (node) { switch (node.nodeName) { @@ -579,15 +582,6 @@ function isLeafNode (node) { * @function * * @description - * Alias for {@link angular.Object.copy} - */ - -/** - * @ngdoc function - * @name angular.Object.copy - * @function - * - * @description * Creates a deep copy of `source`, which should be an object or an array. * * * If no destination is supplied, a copy of the object or array is created. @@ -596,42 +590,16 @@ function isLeafNode (node) { * * If `source` is not an object or array, `source` is returned. * * Note: this function is used to augment the Object type in Angular expressions. See - * {@link angular.Array} for more information about Angular arrays. + * {@link angular.module.ng.$filter} for more information about Angular arrays. * * @param {*} source The source that will be used to make a copy. * Can be any type, including primitives, `null`, and `undefined`. * @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 - * - * - Salutation:
- Name:
- -
- - The master object is NOT equal to the form object. - -
master={{master}}
-
form={{form}}
- *
- * - it('should print that initialy the form object is NOT equal to master', function() { - expect(element('.doc-example-live input[name=master.salutation]').val()).toBe('Hello'); - expect(element('.doc-example-live input[name=master.name]').val()).toBe('world'); - expect(element('.doc-example-live span').css('display')).toBe('inline'); - }); - - it('should make form and master equal when the copy button is clicked', function() { - element('.doc-example-live button').click(); - expect(element('.doc-example-live span').css('display')).toBe('none'); - }); - * - *
*/ function copy(source, destination){ + if (isWindow(source) || isScope(source)) throw Error("Can't copy Window or Scope"); if (!destination) { destination = source; if (source) { @@ -644,6 +612,7 @@ function copy(source, destination){ } } } else { + if (source === destination) throw Error("Can't copy equivalent objects or arrays"); if (isArray(source)) { while(destination.length) { destination.pop(); @@ -669,15 +638,6 @@ function copy(source, destination){ * @function * * @description - * Alias for {@link angular.Object.equals} - */ - -/** - * @ngdoc function - * @name angular.Object.equals - * @function - * - * @description * Determines if two objects or two values are equivalent. Supports value types, arrays and * objects. * @@ -685,50 +645,24 @@ function copy(source, destination){ * * * 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) * * During a property comparision, properties of `function` type and properties with names * that begin with `$` are ignored. * - * Note: This function is used to augment the Object type in Angular expressions. See - * {@link angular.Array} for more information about Angular arrays. + * Scope and DOMWindow objects are being compared only be identify (`===`). * * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. - * - * @example - * - * - Salutation:
- Name:
-
- - The greeting object is - NOT equal to - {salutation:'Hello', name:'world'}. - -
greeting={{greeting}}
- *
- * - it('should print that initialy greeting is equal to the hardcoded value object', function() { - expect(element('.doc-example-live input[name=greeting.salutation]').val()).toBe('Hello'); - expect(element('.doc-example-live input[name=greeting.name]').val()).toBe('world'); - expect(element('.doc-example-live span').css('display')).toBe('none'); - }); - - it('should say that the objects are not equal when the form is modified', function() { - input('greeting.name').enter('kitty'); - expect(element('.doc-example-live span').css('display')).toBe('inline'); - }); - * - *
*/ function equals(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; + if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN var t1 = typeof o1, t2 = typeof o2, length, key, keySet; if (t1 == t2 && t1 == 'object') { - if (o1 instanceof Array) { + if (isArray(o1)) { if ((length = o1.length) == o2.length) { for(key=0; key + + I can add: 1 + 2 = {{ 1+2 }} + + * - */ -function angularInit(config, document){ - var autobind = config.autobind; +function angularInit(element, bootstrap) { + var elements = [element], + appElement, + module, + names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], + NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + + function append(element) { + element && elements.push(element); + } - if (autobind) { - var element = isString(autobind) ? document.getElementById(autobind) : document, - scope = compile(element)(createScope()), - $browser = scope.$service('$browser'); + forEach(names, function(name) { + names[name] = true; + append(document.getElementById(name)); + name = name.replace(':', '\\:'); + if (element.querySelectorAll) { + forEach(element.querySelectorAll('.' + name), append); + forEach(element.querySelectorAll('.' + name + '\\:'), append); + forEach(element.querySelectorAll('[' + name + ']'), append); + }; + }); - if (config.css) - $browser.addCss(config.base_url + config.css); - else if(msie<8) - $browser.addJs(config.ie_compat, config.ie_compat_id); - scope.$apply(); + forEach(elements, function(element) { + if (!appElement) { + var className = ' ' + element.className + ' '; + var match = NG_APP_CLASS_REGEXP.exec(className); + if (match) { + appElement = element; + module = (match[2] || '').replace(/\s+/g, ','); + } else { + forEach(element.attributes, function(attr) { + if (!appElement && names[attr.name]) { + appElement = element; + module = attr.value; + } + }); + } + } + }); + if (appElement) { + bootstrap(appElement, module ? [module] : []); } } -function angularJsConfig(document, config) { - bindJQuery(); - var scripts = document.getElementsByTagName("script"), - match; - config = extend({ - ie_compat_id: 'ng-ie-compat' - }, config); - for(var j = 0; j < scripts.length; j++) { - match = (scripts[j].src || "").match(rngScript); - if (match) { - config.base_url = match[1]; - config.ie_compat = match[1] + 'angular-ie-compat' + (match[2] || '') + '.js'; - extend(config, parseKeyValue(match[6])); - eachAttribute(jqLite(scripts[j]), function(value, name){ - if (/^ng:/.exec(name)) { - name = name.substring(3).replace(/-/g, '_'); - value = value || true; - config[name] = value; - } +/** + * @ngdoc function + * @name angular.bootstrap + * @description + * Use this function to manually start up angular application. + * + * See: {@link guide/dev_guide.bootstrap.manual_bootstrap Bootstrap} + * + * @param {Element} element DOM element which is the root of angular application. + * @param {Array=} modules an array of module declarations. See: {@link angular.module modules} + * @param {angular.module.auta.$injector} the injector; + */ +function bootstrap(element, modules) { + element = jqLite(element); + modules = modules || []; + modules.unshift('ng'); + var injector = createInjector(modules); + injector.invoke( + ['$rootScope', '$compile', '$injector', function(scope, compile, injector){ + scope.$apply(function() { + element.data('$injector', injector); + compile(element)(scope); }); - } - } - return config; + }] + ); + return injector; } -function bindJQuery(){ +function bindJQuery() { // bind to jQuery if present; jQuery = window.jQuery; // reset to jQuery or default to us. if (jQuery) { jqLite = jQuery; extend(jQuery.fn, { - scope: JQLitePrototype.scope + scope: JQLitePrototype.scope, + inheritedData: JQLitePrototype.inheritedData }); + JQLitePatchJQueryRemove('remove', true); + JQLitePatchJQueryRemove('empty'); + JQLitePatchJQueryRemove('html'); } else { jqLite = jqLiteWrap; } @@ -1008,13 +949,208 @@ function assertArg(arg, name, reason) { (reason || "required")); throw error; } + return arg; } function assertArgFn(arg, name) { - assertArg(isFunction(arg), name, 'not a function, got ' + - (typeof arg == 'object' ? arg.constructor.name : typeof arg)); + assertArg(isFunction(arg), name, 'not a function, got ' + + (typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + return arg; } +/** + * @ngdoc interface + * @name angular.Module + * @description + * + * Interface for configuring angular {@link angular.module modules}. + */ + +function setupModuleLoader(window) { + + function ensure(obj, name, factory) { + return obj[name] || (obj[name] = factory()); + } + + return ensure(ensure(window, 'angular', Object), 'module', function() { + /** @type {Object.} */ + var modules = {}; + + /** + * @ngdoc function + * @name angular.module + * @description + * + * The `angular.module` is a global place for registering angular modules. All modules + * (angular core or 3rd party) that should be available to an application must be registered using this mechanism. + * + * # Module + * + * A module is a collocation of services, directives, filters, and configure information. Module is used to configure the, + * {@link angular.module.AUTO.$injector $injector}. + * + *
+     * // Create a new module
+     * var myModule = angular.module('myModule', []);
+     *
+     * // configure a new service
+     * myModule.value('appName', 'MyCoolApp');
+     *
+     * // configure existing services inside initialization blocks.
+     * myModule.init(function($locationProvider) {
+     *   // Configure existing providers
+     *   $locationProvider.hashPrefix = '!';
+     * });
+     * 
+ * + * Then you can load your module like this: + * + *
+     * var injector = angular.injector(['ng', 'MyModule'])
+     * 
+ * + * @param {!string} name The name of the module to create or retrieve. + * @param {Array.=} requires If specified then new module is being created. If unspecified then the + * the module is being retrieved for further configuration. + * @param {Function} initFn Option configuration function for the module. Same as + * {@link angular.Module#init Module.init()}. + * @return {angular.Module} + */ + return function module(name, requires, configFn) { + if (requires && modules.hasOwnProperty(name)) { + modules[name] = null; + } + return ensure(modules, name, function() { + if (!requires) { + throw Error('No module: ' + name); + } + + /** @type {!Array.>} */ + var invokeQueue = []; + + /** @type {!Array.} */ + var runBlocks = []; + + var config = invokeLater('$injector', 'invoke'); + + /** @type {angular.Module} */ + var moduleInstance = { + // Private state + _invokeQueue: invokeQueue, + _runBlocks: runBlocks, + + /** + * @ngdoc property + * @name angular.Module#requires + * @propertyOf angular.Module + * @returns {Array.} List of module names which must be loaded before this module. + * @description + * Holds the list of modules which the injector will load before the current module is loaded. + */ + requires: requires, + + /** + * @ngdoc property + * @name angular.Module#name + * @propertyOf angular.Module + * @returns {string} Name of the module. + * @description + */ + name: name, + + + /** + * @ngdoc method + * @name angular.Module#service + * @methodOf angular.Module + * @param {string} name service name + * @param {Function} providerType Construction function for creating new instance of the service. + * @description + * See {@link angular.module.AUTO.$provide#service $provide.service()}. + */ + service: invokeLater('$provide', 'service'), + + /** + * @ngdoc method + * @name angular.Module#factory + * @methodOf angular.Module + * @param {string} name service name + * @param {Function} providerFunction Function for creating new instance of the service. + * @description + * See {@link angular.module.AUTO.$provide#service $provide.factory()}. + */ + factory: invokeLater('$provide', 'factory'), + + /** + * @ngdoc method + * @name angular.Module#value + * @methodOf angular.Module + * @param {string} name service name + * @param {*} object Service instance object. + * @description + * See {@link angular.module.AUTO.$provide#value $provide.value()}. + */ + value: invokeLater('$provide', 'value'), + + /** + * @ngdoc method + * @name angular.Module#filter + * @methodOf angular.Module + * @param {string} name filterr name + * @param {Function} filterFactory Factory function for creating new instance of filter. + * @description + * See {@link angular.module.ng.$filterProvider#register $filterProvider.register()}. + */ + filter: invokeLater('$filterProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#config + * @methodOf angular.Module + * @param {Function} initializationFn Execute this function on module load. Useful for + * service configuration. + * @description + * Use this method to register work which needs to be performed on module loading. + */ + config: config, + + /** + * @ngdoc method + * @name angular.Module#run + * @methodOf angular.Module + * @param {Function} initializationFn Execute this function after injector creation. + * Useful for application initialization. + * @description + * Use this method to register work which needs to be performed on module loading. + */ + run: function(block) { + runBlocks.push(block); + return this; + } + }; + + if (configFn) { + config(configFn); + } + + return moduleInstance; + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLater(provider, method) { + return function() { + invokeQueue.push([provider, method, arguments]); + return moduleInstance; + } + } + }); + }; + }); + +} /** * @ngdoc property @@ -1030,13 +1166,86 @@ function assertArgFn(arg, name) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '0.10.0', // all of these placeholder strings will be replaced by rake's + full: '0.10.6-5cdfe45a', // all of these placeholder strings will be replaced by rake's major: 0, // compile task minor: 10, - dot: 0, - codeName: 'chicken-hands' + dot: 6, + codeName: 'bubblewrap-cape' }; + +function publishExternalAPI(angular){ + extend(angular, { + 'bootstrap': bootstrap, + 'copy': copy, + 'extend': extend, + 'equals': equals, + 'element': jqLite, + 'forEach': forEach, + 'injector': createInjector, + 'noop':noop, + 'bind':bind, + 'toJson': toJson, + 'fromJson': fromJson, + 'identity':identity, + 'isUndefined': isUndefined, + 'isDefined': isDefined, + 'isString': isString, + 'isFunction': isFunction, + 'isObject': isObject, + 'isNumber': isNumber, + 'isElement': isElement, + 'isArray': isArray, + 'version': version, + 'isDate': isDate, + 'lowercase': lowercase, + 'uppercase': uppercase, + 'callbacks': {counter: 0} + }); + + angularModule = setupModuleLoader(window); + try { + angularModule('ngLocale'); + } catch (e) { + angularModule('ngLocale', []).service('$locale', $LocaleProvider); + } + + angularModule('ng', ['ngLocale'], ['$provide', + function ngModule($provide) { + // TODO(misko): temporary services to get the compiler working; + $provide.value('$textMarkup', angularTextMarkup); + $provide.value('$attrMarkup', angularAttrMarkup); + $provide.value('$directive', angularDirective); + $provide.value('$widget', angularWidget); + + $provide.service('$anchorScroll', $AnchorScrollProvider); + $provide.service('$browser', $BrowserProvider); + $provide.service('$cacheFactory', $CacheFactoryProvider); + $provide.service('$compile', $CompileProvider); + $provide.service('$cookies', $CookiesProvider); + $provide.service('$cookieStore', $CookieStoreProvider); + $provide.service('$defer', $DeferProvider); + $provide.service('$document', $DocumentProvider); + $provide.service('$exceptionHandler', $ExceptionHandlerProvider); + $provide.service('$filter', $FilterProvider); + $provide.service('$interpolate', $InterpolateProvider); + $provide.service('$formFactory', $FormFactoryProvider); + $provide.service('$http', $HttpProvider); + $provide.service('$httpBackend', $HttpBackendProvider); + $provide.service('$location', $LocationProvider); + $provide.service('$log', $LogProvider); + $provide.service('$parse', $ParseProvider); + $provide.service('$resource', $ResourceProvider); + $provide.service('$route', $RouteProvider); + $provide.service('$routeParams', $RouteParamsProvider); + $provide.service('$rootScope', $RootScopeProvider); + $provide.service('$q', $QProvider); + $provide.service('$sniffer', $SnifferProvider); + $provide.service('$templateCache', $TemplateCacheProvider); + $provide.service('$window', $WindowProvider); + }]); +} + var array = [].constructor; /** @@ -1077,18 +1286,20 @@ function fromJson(json, useNative) { try { if (useNative && window.JSON && window.JSON.parse) { obj = JSON.parse(json); - return transformDates(obj); + } else { + obj = parseJson(json, true)(); } - return parser(json, true).primary()(); + return transformDates(obj); } catch (e) { error("fromJson error: ", json, e); throw e; } // TODO make forEach optionally recursive and remove this function + // TODO(misko): remove this once the $http service is checked in. function transformDates(obj) { if (isString(obj) && obj.length === DATE_ISOSTRING_LN) { - return angularString.toDate(obj); + return jsonStringToDate(obj); } else if (isArray(obj) || isObject(obj)) { forEach(obj, function(val, name) { obj[name] = transformDates(val); @@ -1098,8 +1309,57 @@ function fromJson(json, useNative) { } } -angular.toJson = toJson; -angular.fromJson = fromJson; +var R_ISO8061_STR = /^(\d{4})-(\d\d)-(\d\d)(?:T(\d\d)(?:\:(\d\d)(?:\:(\d\d)(?:\.(\d{3}))?)?)?Z)?$/; +function jsonStringToDate(string){ + var match; + if (isString(string) && (match = string.match(R_ISO8061_STR))){ + var date = new Date(0); + date.setUTCFullYear(match[1], match[2] - 1, match[3]); + date.setUTCHours(match[4]||0, match[5]||0, match[6]||0, match[7]||0); + return date; + } + return string; +} + +function jsonDateToString(date){ + if (!date) return date; + var isoString = date.toISOString ? date.toISOString() : ''; + return (isoString.length==24) + ? isoString + : padNumber(date.getUTCFullYear(), 4) + '-' + + padNumber(date.getUTCMonth() + 1, 2) + '-' + + padNumber(date.getUTCDate(), 2) + 'T' + + padNumber(date.getUTCHours(), 2) + ':' + + padNumber(date.getUTCMinutes(), 2) + ':' + + padNumber(date.getUTCSeconds(), 2) + '.' + + padNumber(date.getUTCMilliseconds(), 3) + 'Z'; +} + +function quoteUnicode(string) { + var chars = ['"']; + for ( var i = 0; i < string.length; i++) { + var code = string.charCodeAt(i); + var ch = string.charAt(i); + switch(ch) { + case '"': chars.push('\\"'); break; + case '\\': chars.push('\\\\'); break; + case '\n': chars.push('\\n'); break; + case '\f': chars.push('\\f'); break; + case '\r': chars.push(ch = '\\r'); break; + case '\t': chars.push(ch = '\\t'); break; + default: + if (32 <= code && code <= 126) { + chars.push(ch); + } else { + var encode = "000" + code.toString(16); + chars.push("\\u" + encode.substring(encode.length - 4)); + } + } + } + chars.push('"'); + return chars.join(''); + } + function toJsonArray(buf, obj, pretty, stack) { if (isObject(obj)) { @@ -1120,21 +1380,21 @@ function toJsonArray(buf, obj, pretty, stack) { stack.push(obj); } if (obj === null) { - buf.push($null); + buf.push('null'); } else if (obj instanceof RegExp) { - buf.push(angularString.quoteUnicode(obj.toString())); + buf.push(quoteUnicode(obj.toString())); } else if (isFunction(obj)) { return; } else if (isBoolean(obj)) { buf.push('' + obj); } else if (isNumber(obj)) { if (isNaN(obj)) { - buf.push($null); + buf.push('null'); } else { buf.push('' + obj); } } else if (isString(obj)) { - return buf.push(angularString.quoteUnicode(obj)); + return buf.push(quoteUnicode(obj)); } else if (isObject(obj)) { if (isArray(obj)) { buf.push("["); @@ -1144,7 +1404,7 @@ function toJsonArray(buf, obj, pretty, stack) { var item = obj[i]; if (sep) buf.push(","); if (!(item instanceof RegExp) && (isFunction(item) || isUndefined(item))) { - buf.push($null); + buf.push('null'); } else { toJsonArray(buf, item, pretty, stack); } @@ -1155,7 +1415,7 @@ function toJsonArray(buf, obj, pretty, stack) { // TODO(misko): maybe in dev mode have a better error reporting? buf.push('DOM_ELEMENT'); } else if (isDate(obj)) { - buf.push(angularString.quoteUnicode(angular.Date.toString(obj))); + buf.push(quoteUnicode(jsonDateToString(obj))); } else { buf.push("{"); if (pretty) buf.push(pretty); @@ -1163,7 +1423,7 @@ function toJsonArray(buf, obj, pretty, stack) { var childPretty = pretty ? pretty + " " : false; var keys = []; for(var k in obj) { - if (obj.hasOwnProperty(k) && obj[k] !== undefined) { + if (k!='this' && k!='$parent' && k.substring(0,2) != '$$' && obj.hasOwnProperty(k) && obj[k] !== undefined) { keys.push(k); } } @@ -1176,7 +1436,7 @@ function toJsonArray(buf, obj, pretty, stack) { buf.push(","); if (pretty) buf.push(pretty); } - buf.push(angularString.quote(key)); + buf.push(quoteUnicode(key)); buf.push(":"); toJsonArray(buf, value, childPretty, stack); comma = true; @@ -1191,3654 +1451,3036 @@ function toJsonArray(buf, obj, pretty, stack) { } /** - * Template provides directions an how to bind to a given element. - * It contains a list of init functions which need to be called to - * bind to a new instance of elements. It also provides a list - * of child paths which contain child templates - */ -function Template() { - this.paths = []; - this.children = []; - this.linkFns = []; - this.newScope = false; -} - -Template.prototype = { - link: function(element, scope) { - var childScope = scope; - if (this.newScope) { - childScope = isFunction(this.newScope) ? scope.$new(this.newScope(scope)) : scope.$new(); - element.data($$scope, childScope); - } - forEach(this.linkFns, function(fn) { - try { - childScope.$service.invoke(childScope, fn, [element]); - } catch (e) { - childScope.$service('$exceptionHandler')(e); - } - }); - var i, - childNodes = element[0].childNodes, - children = this.children, - paths = this.paths, - length = paths.length; - for (i = 0; i < length; i++) { - children[i].link(jqLite(childNodes[paths[i]]), childScope); - } - }, - + * @ngdoc function + * @name angular.injector + * @function + * + * @description + * Creates an injector function that can be used for retrieving services as well as for + * dependency injection (see {@link guide/dev_guide.di dependency injection}). + * - addLinkFn:function(linkingFn) { - if (linkingFn) { - if (!linkingFn.$inject) - linkingFn.$inject = []; - this.linkFns.push(linkingFn); - } - }, + * @param {Array.} modules A list of module functions or their aliases. See + * {@link angular.module}. The `ng` module must be explicitly added. + * @returns {function()} Injector function. See {@link angular.module.AUTO.$injector $injector}. + * + * @example + * Typical usage + *
+ *   // create an injector
+ *   var $injector = angular.injector(['ng']);
+ *
+ *   // use the injector to kick of your application
+ *   // use the type inference to auto inject arguments, or use implicit injection
+ *   $injector.invoke(function($rootScope, $compile, $document){
+ *     $compile($document)($rootScope);
+ *     $rootScope.$digest();
+ *   });
+ * 
+ */ - addChild: function(index, template) { - if (template) { - this.paths.push(index); - this.children.push(template); - } - }, +/** + * @ngdoc overview + * @name angular.module.AUTO + * @description + * + * Implicit module which gets automatically added to each {@link angular.module.AUTO.$injector $injector}. + */ - empty: function() { - return this.linkFns.length === 0 && this.paths.length === 0; +var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/m; +var FN_ARG_SPLIT = /,/; +var FN_ARG = /^\s*(.+?)\s*$/; +var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; +function inferInjectionArgs(fn) { + assertArgFn(fn); + if (!fn.$inject) { + var args = fn.$inject = []; + var fnText = fn.toString().replace(STRIP_COMMENTS, ''); + var argDecl = fnText.match(FN_ARGS); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ + arg.replace(FN_ARG, function(all, name){ + args.push(name); + }); + }); } -}; + return fn.$inject; +} -/////////////////////////////////// -//Compiler -////////////////////////////////// +/////////////////////////////////////// /** - * @workInProgress - * @ngdoc function - * @name angular.compile + * @ngdoc object + * @name angular.module.AUTO.$injector * @function * * @description - * Compiles a piece of HTML string or DOM into a template and produces a template function, which - * can then be used to link {@link angular.scope scope} and the template together. * - * The compilation is a process of walking the DOM tree and trying to match DOM elements to - * {@link angular.markup markup}, {@link angular.attrMarkup attrMarkup}, - * {@link angular.widget widgets}, and {@link angular.directive directives}. For each match it - * executes corresponding markup, attrMarkup, widget or directive template function and collects the - * instance functions into a single template function which is then returned. + * `$injector` is used to retrieve object instances as defined by + * {@link angular.module.AUTO.$provide provider}, instantiate types, invoke methods, + * and load modules. * - * The template function can then be used once to produce the view or as it is the case with - * {@link angular.widget.@ng:repeat repeater} many-times, in which case each call results in a view - * that is a DOM clone of the original template. + * The following always holds true: * -
-    // compile the entire window.document and give me the scope bound to this template.
-    var rootScope = angular.compile(window.document)();
-
-    // compile a piece of html
-    var rootScope2 = angular.compile('
click me
')(); - - // compile a piece of html and retain reference to both the dom and scope - var template = angular.element('
click me
'), - scope = angular.compile(template)(); - // at this point template was transformed into a view -
+ *
+ *   var $injector = angular.injector();
+ *   expect($injector.get('$injector')).toBe($injector);
+ *   expect($injector.invoke(null, function($injector){
+ *     return $injector;
+ *   }).toBe($injector);
+ * 
* + * # Injection Function Annotation * - * @param {string|DOMElement} element Element or HTML to compile into a template function. - * @returns {function([scope][, cloneAttachFn])} a template function which is used to bind template - * (a DOM element/tree) to a scope. Where: + * JavaScript does not have annotations, and annotations are needed for dependency injection. The + * following ways are all valid way of annotating function with injection arguments and are equivalent. * - * * `scope` - A {@link angular.scope Scope} to bind to. If none specified, then a new - * root scope is created. - * * `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:
`cloneAttachFn(clonedElement, scope)` where: + *
+ *   // inferred (only works if code not minified/obfuscated)
+ *   $inject.invoke(function(serviceA){});
  *
- *      * `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.
+ *   // annotated
+ *   function explicit(serviceA) {};
+ *   explicit.$inject = ['serviceA'];
+ *   $inject.invoke(explicit);
  *
- * Calling the template function returns the scope to which the element is bound to. It is either
- * the same scope as the one passed into the template function, or if none were provided it's the
- * newly create scope.
+ *   // inline
+ *   $inject.invoke(['serviceA', function(serviceA){}]);
+ * 
* - * If you need access to the bound view, there are two ways to do it: + * ## Inference * - * - If you are not asking the linking function to clone the template, create the DOM element(s) - * before you send them to the compiler and keep this reference around. - *
- *     var view = angular.element('

{{total}}

'), - * scope = angular.compile(view)(); - *
+ * In JavaScript calling `toString()` on a function returns the function definition. The definition can then be + * parsed and the function arguments can be extracted. *NOTE:* This does not work with minfication, and obfuscation + * tools since these tools change the argument names. * - * - if on the other hand, you need the element to be cloned, the view reference from the original - * example would not point to the clone, but rather to the original template that was cloned. In - * this case, you can access the clone via the cloneAttachFn: - *
- *     var original = angular.element('

{{total}}

'), - * scope = someParentScope.$new(), - * clone; + * ## `$inject` Annotation + * By adding a `$inject` property onto a function the injection parameters can be specified. * - * angular.compile(original)(scope, function(clonedElement, scope) { - * clone = clonedElement; - * //attach the clone to DOM document at the right place - * }); + * ## Inline + * As an array of injection names, where the last item in the array is the function to call. + */ + +/** + * @ngdoc method + * @name angular.module.AUTO.$injector#get + * @methodOf angular.module.AUTO.$injector * - * //now we have reference to the cloned DOM via `clone` - *
+ * @description + * Return an instance of the service. * + * @param {string} name The name of the instance to retrieve. + * @return {*} The instance. + */ + +/** + * @ngdoc method + * @name angular.module.AUTO.$injector#invoke + * @methodOf angular.module.AUTO.$injector * - * Compiler Methods For Widgets and Directives: + * @description + * Invoke the method and supply the method arguments from the `$injector`. * - * The following methods are available for use when you write your own widgets, directives, - * and markup. (Recall that the compile function's this is a reference to the compiler.) + * @param {!function} fn The function to invoke. The function arguments come form the function annotation. + * @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. + * @return the value returned by the invoked `fn` function. + */ + +/** + * @ngdoc method + * @name angular.module.AUTO.$injector#instantiate + * @methodOf angular.module.AUTO.$injector + * @description + * Create a new instance of JS type. The method takes a constructor function invokes the new operator and supplies + * all of the arguments to the constructor function as specified by the constructor annotation. * - * `compile(element)` - returns linker - - * Invoke a new instance of the compiler to compile a DOM element and return a linker function. - * You can apply the linker function to the original element or a clone of the original element. - * The linker function returns a scope. + * @param {function} Type Annotated constructor function. + * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before + * the `$injector` is consulted. + * @return new instance of `Type`. + */ + + +/** + * @ngdoc object + * @name angular.module.AUTO.$provide * - * * `comment(commentText)` - returns element - Create a comment element. + * @description * - * * `element(elementName)` - returns element - Create an element by name. + * 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 the `Provide` suffixed to them. * - * * `text(text)` - returns element - Create a text element. + * 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. * - * * `descend([set])` - returns descend state (true or false). Get or set the current descend - * state. If true the compiler will descend to children elements. + *
+ *   function GreetProvider() {
+ *     var salutation = 'Hello';
  *
- *  * `directives([set])` - returns directive state (true or false). Get or set the current
- *  directives processing state. The compiler will process directives only when directives set to
- *  true.
+ *     this.salutation = function(text) {
+ *       salutation = text;
+ *     };
  *
- * For information on how the compiler works, see the
- * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide.
- */
-function Compiler(markup, attrMarkup, directives, widgets){
-  this.markup = markup;
-  this.attrMarkup = attrMarkup;
-  this.directives = directives;
-  this.widgets = widgets;
-}
-
-Compiler.prototype = {
-  compile: function(templateElement) {
-    templateElement = jqLite(templateElement);
-    var index = 0,
-        template,
-        parent = templateElement.parent();
-    if (templateElement.length > 1) {
-      // https://github.com/angular/angular.js/issues/338
-      throw Error("Cannot compile multiple element roots: " +
-          jqLite('
').append(templateElement.clone()).html()); - } - if (parent && parent[0]) { - parent = parent[0]; - for(var i = 0; i < parent.childNodes.length; i++) { - if (parent.childNodes[i] == templateElement[0]) { - index = i; - } - } - } - template = this.templatize(templateElement, index) || new Template(); - return function(scope, cloneConnectFn){ - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var element = cloneConnectFn - ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!! - : templateElement; - scope = scope || createScope(); - element.data($$scope, scope); - scope.$element = element; - (cloneConnectFn||noop)(element, scope); - template.link(element, scope); - return scope; - }; - }, - - templatize: function(element, elementIndex){ - var self = this, - widget, - fn, - directiveFns = self.directives, - descend = true, - directives = true, - elementName = nodeName_(element), - elementNamespace = elementName.indexOf(':') > 0 ? lowercase(elementName).replace(':', '-') : '', - template, - selfApi = { - compile: bind(self, self.compile), - descend: function(value){ if(isDefined(value)) descend = value; return descend;}, - directives: function(value){ if(isDefined(value)) directives = value; return directives;}, - scope: function(value){ if(isDefined(value)) template.newScope = template.newScope || value; return template.newScope;} - }; - element.addClass(elementNamespace); - template = new Template(); - eachAttribute(element, function(value, name){ - if (!widget) { - if ((widget = self.widgets('@' + name))) { - element.addClass('ng-attr-widget'); - widget = bind(selfApi, widget, value, element); - } - } - }); - if (!widget) { - if ((widget = self.widgets(elementName))) { - if (elementNamespace) - element.addClass('ng-widget'); - widget = bind(selfApi, widget, element); - } - } - if (widget) { - descend = false; - directives = false; - var parent = element.parent(); - template.addLinkFn(widget.call(selfApi, element)); - if (parent && parent[0]) { - element = jqLite(parent[0].childNodes[elementIndex]); - } - } - if (descend){ - // process markup for text nodes only - for(var i=0, child=element[0].childNodes; - i */ - -function createScope(providers, instanceCache) { - var scope = new Scope(); - (scope.$service = createInjector(scope, providers, instanceCache)).eager(); - return scope; -} - - /** - * @ngdoc function - * @name angular.scope - * + * @ngdoc method + * @name angular.module.AUTO.$provide#service + * @methodOf angular.module.AUTO.$provide * @description - * A root scope can be created by calling {@link angular.scope angular.scope()}. Child scopes - * are created using the {@link angular.scope.$new $new()} method. - * (Most scopes are created automatically when compiled HTML template is executed.) - * - * Here is a simple scope snippet to show how you can interact with the scope. - *
-       var scope = angular.scope();
-       scope.salutation = 'Hello';
-       scope.name = 'World';
-
-       expect(scope.greeting).toEqual(undefined);
-
-       scope.$watch('name', function(){
-         this.greeting = this.salutation + ' ' + this.name + '!';
-       }); // initialize the watch
-
-       expect(scope.greeting).toEqual(undefined);
-       scope.name = 'Misko';
-       // still old value, since watches have not been called yet
-       expect(scope.greeting).toEqual(undefined);
-
-       scope.$digest(); // fire all  the watches
-       expect(scope.greeting).toEqual('Hello Misko!');
- * 
- * - * # Inheritance - * A scope can inherit from a parent scope, as in this example: - *
-     var parent = angular.scope();
-     var child = parent.$new();
-
-     parent.salutation = "Hello";
-     child.name = "World";
-     expect(child.salutation).toEqual('Hello');
-
-     child.salutation = "Welcome";
-     expect(child.salutation).toEqual('Welcome');
-     expect(parent.salutation).toEqual('Hello');
- * 
* - * # Dependency Injection - * See {@link guide/dev_guide.di dependency injection}. + * Register a provider for a service. The providers can be retrieved and can have additional configuration methods. * + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provide'` key. + * @param {(Object|function())} provider If the provider is: * - * @param {Object.=} providers Map of service factory which need to be provided - * for the current scope. Defaults to {@link angular.service}. - * @param {Object.=} instanceCache Provides pre-instantiated services which should - * append/override services provided by `providers`. This is handy when unit-testing and having - * the need to override a default service. - * @returns {Object} Newly created scope. + * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using + * {@link angular.module.AUTO.$injector#invoke $injector.invoke()} when an instance needs to be created. + * - `Constructor`: a new instance of the provider will be created using + * {@link angular.module.AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. * */ -function Scope() { - this.$id = nextUid(); - this.$$phase = this.$parent = this.$$watchers = - this.$$nextSibling = this.$$prevSibling = - this.$$childHead = this.$$childTail = null; - this.$destructor = noop; - this['this'] = this.$root = this; - this.$$asyncQueue = []; - this.$$listeners = {}; -} /** - * @ngdoc property - * @name angular.scope.$id - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. + * @ngdoc method + * @name angular.module.AUTO.$provide#factory + * @methodOf angular.module.AUTO.$provide + * @description + * + * A short hand for configuring services if only `$get` method is required. + * + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provide'` key. + * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand for + * `$provide.service(name, {$get:$getFn})`. */ + /** - * @ngdoc property - * @name angular.scope.$service - * @function - * + * @ngdoc method + * @name angular.module.AUTO.$provide#value + * @methodOf angular.module.AUTO.$provide * @description - * Provides reference to an instance of {@link angular.injector injector} which can be used to - * retrieve {@link angular.service services}. In general the use of this api is discouraged, - * in favor of proper {@link guide/dev_guide.di dependency injection}. * - * @returns {function} {@link angular.injector injector} + * A short hand for configuring services if the `$get` method is a constant. + * + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provide'` key. + * @param {function()} value The $getFn for the instance creation. Internally this is a short hand for + * `$provide.service(name, {$get:function(){ return value; }})`. */ -/** - * @ngdoc property - * @name angular.scope.$root - * @returns {Scope} The root scope of the current scope hierarchy. - */ -/** - * @ngdoc property - * @name angular.scope.$parent - * @returns {Scope} The parent scope of the current scope. - */ +function createInjector(modulesToLoad) { + var providerSuffix = 'Provider', + path = [], + loadedModules = new HashMap(), + providerCache = { + $provide: { + service: supportObject(service), + factory: supportObject(factory), + value: supportObject(value), + decorator: decorator + } + }, + providerInjector = createInternalInjector(providerCache, function() { + throw Error("Unknown provider: " + path.join(' <- ')); + }), + instanceCache = {}, + instanceInjector = (instanceCache.$injector = + createInternalInjector(instanceCache, function(servicename) { + var provider = providerInjector.get(servicename + providerSuffix); + return instanceInjector.invoke(provider.$get, provider); + })); -Scope.prototype = { - /** - * @ngdoc function - * @name angular.scope.$new - * @function - * - * @description - * Creates a new child {@link angular.scope scope}. The new scope can optionally behave as a - * controller. The parent scope will propagate the {@link angular.scope.$digest $digest()} and - * {@link angular.scope.$digest $digest()} events. The scope can be removed from the scope - * hierarchy using {@link angular.scope.$destroy $destroy()}. - * - * {@link angular.scope.$destroy $destroy()} must be called on a scope when it is desired for - * the scope and its child scopes to be permanently detached from the parent and thus stop - * participating in model change detection and listener notification by invoking. - * - * @param {function()=} Class Constructor function which the scope should be applied to the scope. - * @param {...*} curryArguments Any additional arguments which are curried into the constructor. - * See {@link guide/dev_guide.di dependency injection}. - * @returns {Object} The newly created child scope. - * - */ - $new: function(Class, curryArguments) { - var 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 - // name in the debugger. - var child; - Child.prototype = this; - child = new Child(); - child['this'] = child; - child.$$listeners = {}; - child.$parent = this; - child.$id = nextUid(); - child.$$asyncQueue = []; - child.$$phase = child.$$watchers = - child.$$nextSibling = child.$$childHead = child.$$childTail = null; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; - } else { - this.$$childHead = this.$$childTail = child; - } - // short circuit if we have no class - if (Class) { - // can't use forEach, we need speed! - var ClassPrototype = Class.prototype; - for(var key in ClassPrototype) { - child[key] = bind(child, ClassPrototype[key]); + forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); + + return instanceInjector; + + //////////////////////////////////// + // $provider + //////////////////////////////////// + + function supportObject(delegate) { + return function(key, value) { + if (isObject(key)) { + forEach(key, reverseParams(delegate)); + } else { + delegate(key, value); } - this.$service.invoke(child, Class, curryArguments); } - return child; - }, - - /** - * @ngdoc function - * @name angular.scope.$watch - * @function - * - * @description - * Registers a `listener` callback to be executed whenever the `watchExpression` changes. - * - * - The `watchExpression` is called on every call to {@link angular.scope.$digest $digest()} and - * should return the value which will be watched. (Since {@link angular.scope.$digest $digest()} - * reruns when it detects changes the `watchExpression` can execute multiple times per - * {@link angular.scope.$digest $digest()} and should be idempotent.) - * - The `listener` is called only when the value from the current `watchExpression` and the - * previous call to `watchExpression' are not equal. The inequality is determined according to - * {@link angular.equals} function. To save the value of the object for later comparison - * {@link angular.copy} function is used. It also means that watching complex options will - * have adverse memory and performance implications. - * - The watch `listener` may change the model, which may trigger other `listener`s to fire. This - * is achieved by rerunning the watchers until no changes are detected. The rerun iteration - * limit is 100 to prevent infinity loop deadlock. - * - * - * If you want to be notified whenever {@link angular.scope.$digest $digest} is called, - * you can register an `watchExpression` function with no `listener`. (Since `watchExpression`, - * can execute multiple times per {@link angular.scope.$digest $digest} cycle when a change is - * detected, be prepared for multiple calls to your listener.) - * - * - * # Example -
-       var scope = angular.scope();
-       scope.name = 'misko';
-       scope.counter = 0;
-
-       expect(scope.counter).toEqual(0);
-       scope.$watch('name', function(scope, newValue, oldValue) { counter = counter + 1; });
-       expect(scope.counter).toEqual(0);
-
-       scope.$digest();
-       // no variable change
-       expect(scope.counter).toEqual(0);
-
-       scope.name = 'adam';
-       scope.$digest();
-       expect(scope.counter).toEqual(1);
-     
- * - * - * - * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link angular.scope.$digest $digest} cycle. A change in the return value triggers a - * call to the `listener`. - * - * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} - * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. - * - * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} - * - `function(scope, newValue, oldValue)`: called with current `scope` an previous and - * current values as parameters. - * @returns {function()} Returns a deregistration function for this listener. - */ - $watch: function(watchExp, listener) { - var scope = this, - get = compileToFn(watchExp, 'watch'), - listenFn = compileToFn(listener || noop, 'listener'), - array = scope.$$watchers, - watcher = { - fn: listenFn, - last: Number.NaN, // NaN !== NaN. We used this to force $watch to fire on first run. - get: get - }; + } - if (!array) { - array = scope.$$watchers = []; + function service(name, provider) { + if (isFunction(provider)){ + provider = providerInjector.instantiate(provider); + } + if (!provider.$get) { + throw Error('Provider ' + name + ' must define $get factory method.'); } - // we use unshift since we use a while loop in $digest for speed. - // the while loop reads in reverse order. - array.unshift(watcher); + providerCache[name + providerSuffix] = provider; + } + + function factory(name, factoryFn) { service(name, { $get:factoryFn }); } + + function value(name, value) { factory(name, valueFn(value)); } + + function decorator(serviceName, decorFn) { + var origProvider = providerInjector.get(serviceName + providerSuffix), + orig$get = origProvider.$get; - return function() { - angularArray.remove(array, watcher); + origProvider.$get = function() { + var origInstance = instanceInjector.invoke(orig$get, origProvider); + return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); }; - }, + } - /** - * @ngdoc function - * @name angular.scope.$digest - * @function - * - * @description - * Process all of the {@link angular.scope.$watch watchers} of the current scope and its children. - * Because a {@link angular.scope.$watch watcher}'s listener can change the model, the - * `$digest()` keeps calling the {@link angular.scope.$watch watchers} until no more listeners are - * firing. This means that it is possible to get into an infinite loop. This function will throw - * `'Maximum iteration limit exceeded.'` if the number of iterations exceeds 100. - * - * Usually you don't call `$digest()` directly in - * {@link angular.directive.ng:controller controllers} or in {@link angular.directive directives}. - * Instead a call to {@link angular.scope.$apply $apply()} (typically from within a - * {@link angular.directive directive}) will force a `$digest()`. - * - * If you want to be notified whenever `$digest()` is called, - * you can register a `watchExpression` function with {@link angular.scope.$watch $watch()} - * with no `listener`. - * - * You may have a need to call `$digest()` from within unit-tests, to simulate the scope - * life-cycle. - * - * # Example -
-       var scope = angular.scope();
-       scope.name = 'misko';
-       scope.counter = 0;
-
-       expect(scope.counter).toEqual(0);
-       scope.$digest('name', function(scope, newValue, oldValue) { counter = counter + 1; });
-       expect(scope.counter).toEqual(0);
-
-       scope.$digest();
-       // no variable change
-       expect(scope.counter).toEqual(0);
-
-       scope.name = 'adam';
-       scope.$digest();
-       expect(scope.counter).toEqual(1);
-     
- * - */ - $digest: function() { - var watch, value, last, - watchers, - asyncQueue, - length, - dirty, ttl = 100, - next, current, target = this; + //////////////////////////////////// + // Module Loading + //////////////////////////////////// + function loadModules(modulesToLoad){ + var runBlocks = []; + forEach(modulesToLoad, function(module) { + if (loadedModules.get(module)) return; + loadedModules.put(module, true); + if (isString(module)) { + var moduleFn = angularModule(module); + runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - if (target.$$phase) { - throw Error(target.$$phase + ' already in progress'); - } - do { + try { + for(var invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { + var invokeArgs = invokeQueue[i], + provider = invokeArgs[0] == '$injector' + ? providerInjector + : providerInjector.get(invokeArgs[0]); - dirty = false; - current = target; - do { - current.$$phase = '$digest'; - asyncQueue = current.$$asyncQueue; - while(asyncQueue.length) { - try { - current.$eval(asyncQueue.shift()); - } catch (e) { - current.$service('$exceptionHandler')(e); + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } + } catch (e) { + if (e.message) e.message += ' from ' + module; + throw e; } - if ((watchers = current.$$watchers)) { - // process our watches - length = watchers.length; - while (length--) { - try { - watch = watchers[length]; - // Most common watches are on primitives, in which case we can short - // circuit it with === operator, only when === fails do we use .equals - if ((value = watch.get(current)) !== (last = watch.last) && !equals(value, last)) { - dirty = true; - watch.fn(current, watch.last = copy(value), last); - } - } catch (e) { - current.$service('$exceptionHandler')(e); - } - } + } else if (isFunction(module)) { + try { + runBlocks.push(providerInjector.invoke(module)); + } catch (e) { + if (e.message) e.message += ' from ' + module; + throw e; } - - current.$$phase = null; - - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; - } + } else if (isArray(module)) { + try { + runBlocks.push(providerInjector.invoke(module)); + } catch (e) { + if (e.message) e.message += ' from ' + String(module[module.length - 1]); + throw e; } - } while ((current = next)); - - if(!(ttl--)) { - throw Error('100 $digest() iterations reached. Aborting!'); + } else { + assertArgFn(module, 'module'); } - } while (dirty); - }, - - /** - * @ngdoc function - * @name angular.scope.$destroy - * @function - * - * @description - * Remove the current scope (and all of its children) from the parent scope. Removal implies - * that calls to {@link angular.scope.$digest $digest()} will no longer propagate to the current - * scope and its children. Removal also implies that the current scope is eligible for garbage - * collection. - * - * The destructing scope emits an `$destroy` {@link angular.scope.$emit event}. - * - * The `$destroy()` is usually used by directives such as - * {@link angular.widget.@ng:repeat ng:repeat} for managing the unrolling of the loop. - * - */ - $destroy: function() { - if (this.$root == this) return; // we can't remove the root node; - this.$emit('$destroy'); - var parent = this.$parent; - - if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; - if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; - if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; - }, + }); + return runBlocks; + } - /** - * @ngdoc function - * @name angular.scope.$eval - * @function - * - * @description - * Executes the `expression` on the current scope returning the result. Any exceptions in the - * expression are propagated (uncaught). This is useful when evaluating engular expressions. - * - * # Example -
-       var scope = angular.scope();
-       scope.a = 1;
-       scope.b = 2;
-
-       expect(scope.$eval('a+b')).toEqual(3);
-       expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
-     
- * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - * @returns {*} The result of evaluating the expression. - */ - $eval: function(expr) { - var fn = isString(expr) - ? expressionCompile(expr) - : expr || noop; - return fn(this); - }, + //////////////////////////////////// + // internal Injector + //////////////////////////////////// - /** - * @ngdoc function - * @name angular.scope.$evalAsync - * @function - * - * @description - * Executes the expression on the current scope at a later point in time. - * - * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that: - * - * - it will execute in the current script execution context (before any DOM rendering). - * - at least one {@link angular.scope.$digest $digest cycle} will be performed after - * `expression` execution. - * - * Any exceptions from the execution of the expression are forwarded to the - * {@link angular.service.$exceptionHandler $exceptionHandler} service. - * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - */ - $evalAsync: function(expr) { - this.$$asyncQueue.push(expr); - }, + function createInternalInjector(cache, factory) { - /** - * @ngdoc function - * @name angular.scope.$apply - * @function - * - * @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 - * of {@link angular.service.$exceptionHandler exception handling}, - * {@link angular.scope.$digest executing watches}. - * - * ## Life cycle - * - * # Pseudo-Code of `$apply()` - function $apply(expr) { + function getService(serviceName) { + if (typeof serviceName !== 'string') { + throw Error('Service name expected'); + } + if (cache.hasOwnProperty(serviceName)) { + return cache[serviceName]; + } else { try { - return $eval(expr); - } catch (e) { - $exceptionHandler(e); + path.unshift(serviceName); + return cache[serviceName] = factory(serviceName); } finally { - $root.$digest(); + path.shift(); } } - * - * - * Scope's `$apply()` method transitions through the following stages: - * - * 1. The {@link guide/dev_guide.expressions expression} is executed using the - * {@link angular.scope.$eval $eval()} method. - * 2. Any exceptions from the execution of the expression are forwarded to the - * {@link angular.service.$exceptionHandler $exceptionHandler} service. - * 3. The {@link angular.scope.$watch watch} listeners are fired immediately after the expression - * was executed using the {@link angular.scope.$digest $digest()} method. - * - * - * @param {(string|function())=} exp An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. - * - `function(scope)`: execute the function with current `scope` parameter. - * - * @returns {*} The result of evaluating the expression. - */ - $apply: function(expr) { - try { - return this.$eval(expr); - } catch (e) { - this.$service('$exceptionHandler')(e); - } finally { - this.$root.$digest(); } - }, - /** - * @ngdoc function - * @name angular.scope.$on - * @function - * - * @description - * Listen on events of a given type. See {@link angular.scope.$emit $emit} for discussion of - * event life cycle. - * - * @param {string} name Event name to listen on. - * @param {function(event)} listener Function to call when the event is emitted. - * @returns {function()} Returns a deregistration function for this listener. - * - * The event listener function format is: `function(event)`. The `event` object passed into the - * listener has the following attributes - * - `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. - * - `cancel` - {function=}: calling `cancel` function will cancel further event propagation - * (available only for events that were `$emit`-ed). - */ - $on: function(name, listener) { - var namedListeners = this.$$listeners[name]; - if (!namedListeners) { - this.$$listeners[name] = namedListeners = []; + function invoke(fn, self, locals){ + var args = [], + $injectAnnotation, + $injectAnnotationIndex, + key; + + if (typeof fn == 'function') { + $injectAnnotation = inferInjectionArgs(fn); + $injectAnnotationIndex = $injectAnnotation.length; + } else { + if (isArray(fn)) { + $injectAnnotation = fn; + $injectAnnotationIndex = $injectAnnotation.length; + fn = $injectAnnotation[--$injectAnnotationIndex]; + } + assertArgFn(fn, 'fn'); + } + + while($injectAnnotationIndex--) { + key = $injectAnnotation[$injectAnnotationIndex]; + args.unshift(locals && locals.hasOwnProperty(key) ? locals[key] : getService(key)); + } + + // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke + switch (self ? -1 : args.length) { + case 0: return fn(); + case 1: return fn(args[0]); + case 2: return fn(args[0], args[1]); + case 3: return fn(args[0], args[1], args[2]); + case 4: return fn(args[0], args[1], args[2], args[3]); + case 5: return fn(args[0], args[1], args[2], args[3], args[4]); + case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); + case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); + case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); + case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); + case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); + default: return fn.apply(self, args); + } } - namedListeners.push(listener); - return function() { - angularArray.remove(namedListeners, listener); - }; - }, + function instantiate(Type, locals){ + var Constructor = function(){}, + instance; + Constructor.prototype = Type.prototype; + instance = new Constructor(); + return invoke(Type, instance, locals) || instance; + } + return { + invoke: invoke, + instantiate: instantiate, + get: getService + }; + } +} - /** - * @ngdoc function - * @name angular.scope.$emit - * @function - * - * @description - * Dispatches an event `name` upwards through the scope hierarchy notifying the - * registered {@link angular.scope.$on} listeners. - * - * The event life cycle starts at the scope on which `$emit` was called. All - * {@link angular.scope.$on listeners} listening for `name` event on this scope get notified. - * Afterwards, the event traverses upwards toward the root scope and calls all registered - * listeners along the way. The event will stop propagating if one of the listeners cancels it. - * - * Any exception emmited from the {@link angular.scope.$on listeners} will be passed - * onto the {@link angular.service.$exceptionHandler $exceptionHandler} service. - * - * @param {string} name Event name to emit. - * @param {...*} args Optional set of arguments which will be passed onto the event listeners. - */ - $emit: function(name, args) { - var empty = [], - namedListeners, - canceled = false, - scope = this, - event = { - name: name, - targetScope: scope, - cancel: function(){canceled = true;} - }, - listenerArgs = concat([event], arguments, 1), - i, length; - - do { - namedListeners = scope.$$listeners[name] || empty; - event.currentScope = scope; - for (i=0, length=namedListeners.length; i=} [factories=angular.service] Map of the service factory - * functions. - * @param {Object.=} [instanceCache={}] Place where instances of services are - * saved for reuse. Can also be used to override services specified by `serviceFactory` - * (useful in tests). - * @returns {function()} Injector function: - * - * * `injector(serviceName)`: - * * `serviceName` - `{string=}` - Name of the service to retrieve. - * - * The injector function also has these properties: - * - * * An `invoke` property which can be used to invoke methods with dependency-injected arguments. - * `injector.invoke(self, fn, curryArgs)` - * * `self` - The "`this`" to be used when invoking the function. - * * `fn` - The function to be invoked. The function may have the `$inject` property that - * lists the set of arguments which should be auto-injected. - * (see {@link guide/dev_guide.di dependency injection}). - * * `curryArgs(array)` - Optional array of arguments to pass to the function - * invocation after the injection arguments (also known as curry arguments or currying). - * * An `eager` property which is used to initialize the eager services. - * `injector.eager()` - */ -function createInjector(factoryScope, factories, instanceCache) { - factories = factories || angularService; - instanceCache = instanceCache || {}; - factoryScope = factoryScope || {}; - injector.invoke = invoke; - - injector.eager = function(){ - forEach(factories, function(factory, name){ - if (factory.$eager) - injector(name); - - if (factory.$creation) - throw new Error("Failed to register service '" + name + - "': $creation property is unsupported. Use $eager:true or see release notes."); - }); - }; - return injector; +ResourceFactory.DEFAULT_ACTIONS = { + 'get': {method:'GET'}, + 'save': {method:'POST'}, + 'query': {method:'GET', isArray:true}, + 'remove': {method:'DELETE'}, + 'delete': {method:'DELETE'} +}; - function injector(value){ - if (!(value in instanceCache)) { - var factory = factories[value]; - if (!factory) throw Error("Unknown provider for '"+value+"'."); - instanceCache[value] = invoke(factoryScope, factory); +ResourceFactory.prototype = { + route: function(url, paramDefaults, actions){ + var self = this; + var route = new Route(url); + actions = extend({}, ResourceFactory.DEFAULT_ACTIONS, actions); + function extractParams(data){ + var ids = {}; + forEach(paramDefaults || {}, function(value, key){ + ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; + }); + return ids; } - return instanceCache[value]; - } - function invoke(self, fn, args){ - args = args || []; - var injectNames = injectionArgs(fn); - var i = injectNames.length; - while(i--) { - args.unshift(injector(injectNames[i])); + function Resource(value){ + copy(value || {}, this); } - return fn.apply(self, args); + + forEach(actions, function(action, name){ + var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; + Resource[name] = function(a1, a2, a3, a4) { + var params = {}; + var data; + var success = noop; + var error = null; + switch(arguments.length) { + case 4: + error = a4; + success = a3; + //fallthrough + case 3: + case 2: + if (isFunction(a2)) { + if (isFunction(a1)) { + success = a1; + error = a2; + break; + } + + success = a2; + error = a3; + //fallthrough + } else { + params = a1; + data = a2; + success = a3; + break; + } + case 1: + if (isFunction(a1)) success = a1; + else if (isPostOrPut) data = a1; + else params = a1; + break; + case 0: break; + default: + throw "Expected between 0-4 arguments [params, data, success, error], got " + + arguments.length + " arguments."; + } + + var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); + self.$http({ + method: action.method, + url: route.url(extend({}, extractParams(data), action.params || {}, params)), + data: data + }).then(function(response) { + var data = response.data; + + if (data) { + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + value.push(new Resource(item)); + }); + } else { + copy(data, value); + } + } + (success||noop)(value, response.headers); + }, error); + + return value; + }; + + Resource.bind = function(additionalParamDefaults){ + return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + Resource.prototype['$' + name] = function(a1, a2, a3) { + var params = extractParams(this), + success = noop, + error; + + switch(arguments.length) { + case 3: params = a1; success = a2; error = a3; break; + case 2: + case 1: + if (isFunction(a1)) { + success = a1; + error = a2; + } else { + params = a1; + success = a2 || noop; + } + case 0: break; + default: + throw "Expected between 1-3 arguments [params, success, error], got " + + arguments.length + " arguments."; + } + var data = isPostOrPut ? this : undefined; + Resource[name].call(this, params, data, success, error); + }; + }); + return Resource; } -} +}; -/** - * THIS IS NOT PUBLIC DOC YET! - * - * @name angular.annotate - * @function - * - * @description - * Annotate the function with injection arguments. This is equivalent to setting the `$inject` - * property as described in {@link guide.di dependency injection}. - * - *
- * var MyController = angular.annotate('$location', function($location){ ... });
- * 
- * - * is the same as +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js * - *
- * var MyController = function($location){ ... };
- * MyController.$inject = ['$location'];
- * 
+ * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); * - * @param {String|Array} serviceName... zero or more service names to inject into the - * `annotatedFunction`. - * @param {function} annotatedFunction function to annotate with `$inject` - * functions. - * @returns {function} `annotatedFunction` */ -function annotate(services, fn) { - if (services instanceof Array) { - fn.$inject = services; - return fn; - } else { - var i = 0, - length = arguments.length - 1, // last one is the destination function - $inject = arguments[length].$inject = []; - for (; i < length; i++) { - $inject.push(arguments[i]); - } - return arguments[length]; // return the last one - } -} -function angularServiceInject(name, fn, inject, eager) { - angularService(name, fn, {$inject:inject, $eager:eager}); -} +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, + END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^/g, + CDATA_REGEXP = //g, + URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) -/** - * @returns the $inject property of function. If not found the - * the $inject is computed by looking at the toString of function and - * extracting all arguments which and assuming that they are the - * injection names. - */ -var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/m; -var FN_ARG_SPLIT = /,/; -var FN_ARG = /^\s*(.+?)\s*$/; -var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; -function injectionArgs(fn) { - assertArgFn(fn); - if (!fn.$inject) { - var args = fn.$inject = []; - var fnText = fn.toString().replace(STRIP_COMMENTS, ''); - var argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, name){ - args.push(name); - }); - }); - } - return fn.$inject; -} +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements -var OPERATORS = { - 'null':function(self){return null;}, - 'true':function(self){return true;}, - 'false':function(self){return false;}, - $undefined:noop, - '+':function(self, a,b){return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, - '-':function(self, a,b){return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, - '*':function(self, a,b){return a*b;}, - '/':function(self, a,b){return a/b;}, - '%':function(self, a,b){return a%b;}, - '^':function(self, a,b){return a^b;}, - '=':noop, - '==':function(self, a,b){return a==b;}, - '!=':function(self, a,b){return a!=b;}, - '<':function(self, a,b){return a':function(self, a,b){return a>b;}, - '<=':function(self, a,b){return a<=b;}, - '>=':function(self, a,b){return a>=b;}, - '&&':function(self, a,b){return a&&b;}, - '||':function(self, a,b){return a||b;}, - '&':function(self, a,b){return a&b;}, -// '|':function(self, a,b){return a|b;}, - '|':function(self, a,b){return b(self, a);}, - '!':function(self, a){return !a;} -}; -var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); -function lex(text, parseStringsForObjects){ - var dateParseLength = parseStringsForObjects ? DATE_ISOSTRING_LN : -1, - tokens = [], - token, - index = 0, - json = [], - ch, - lastCh = ':'; // can start regexp +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), + optionalEndTagElements = extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); - 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(), - fn = OPERATORS[ch], - fn2 = OPERATORS[ch2]; - 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; - } else { - throwError("Unexpected next character ", index, index+1); - } - } - lastCh = ch; - } - return tokens; +// Safe Block Elements - HTML5 +var blockElements = extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + + "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + + "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); - function is(chars) { - return chars.indexOf(ch) != -1; - } +// Inline Elements - HTML5 +var inlineElements = extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + + "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + + "span,strike,strong,sub,sup,time,tt,u,var")); - function was(chars) { - return chars.indexOf(lastCh) != -1; - } - function peek() { - return index + 1 < text.length ? text.charAt(index + 1) : 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); - } +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); - function throwError(error, start, end) { - end = end || index; - throw Error("Lexer Error: " + error + " at column" + - (isDefined(start) - ? "s " + start + "-" + index + " [" + text.substring(start, end) + "]" - : " " + end) + - " in expression [" + text + "]."); - } +var validElements = extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); - function readNumber() { - var number = ""; - var start = index; - while (index < text.length) { - var ch = lowercase(text.charAt(index)); - if (ch == '.' || isNumber(ch)) { - number += ch; - } else { - var peekCh = peek(); - if (ch == 'e' && isExpOperator(peekCh)) { - number += ch; - } else if (isExpOperator(ch) && - peekCh && isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { - number += ch; - } else if (isExpOperator(ch) && - (!peekCh || !isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { - throwError('Invalid exponent'); - } else { - break; +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); +var validAttrs = extend({}, uriAttrs, makeMap( + 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ + 'scope,scrolling,shape,span,start,summary,target,title,type,'+ + 'valign,value,vspace,width')); + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser( html, handler ) { + var index, chars, match, stack = [], last = html; + stack.last = function() { return stack[ stack.length - 1 ]; }; + + while ( html ) { + chars = true; + + // Make sure we're not in a script or style element + if ( !stack.last() || !specialElements[ stack.last() ] ) { + + // Comment + if ( html.indexOf(""); + + if ( index >= 0 ) { + if (handler.comment) handler.comment( html.substring( 4, index ) ); + html = html.substring( index + 3 ); + chars = false; } - } - index++; - } - number = 1 * number; - tokens.push({index:start, text:number, json:true, - fn:function(){return number;}}); - } - function readIdent() { - var ident = ""; - var start = index; - var fn; - while (index < text.length) { - var ch = text.charAt(index); - if (ch == '.' || isIdent(ch) || isNumber(ch)) { - ident += ch; - } else { - break; - } - index++; - } - fn = OPERATORS[ident]; - tokens.push({ - index:start, - text:ident, - json: fn, - fn:fn||extend(getterFn(ident), { - assign:function(self, value){ - return setter(self, ident, value); + + // end tag + } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { + match = html.match( END_TAG_REGEXP ); + + if ( match ) { + html = html.substring( match[0].length ); + match[0].replace( END_TAG_REGEXP, parseEndTag ); + chars = false; } - }) - }); - } - function readString(quote) { - var start = index; - index++; - var string = ""; - var rawString = quote; - var escape = false; - while (index < text.length) { - var ch = text.charAt(index); - rawString += ch; - if (escape) { - if (ch == 'u') { - var hex = text.substring(index + 1, index + 5); - if (!hex.match(/[\da-f]{4}/i)) - throwError( "Invalid unicode escape [\\u" + hex + "]"); - index += 4; - string += String.fromCharCode(parseInt(hex, 16)); - } else { - var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } + // start tag + } else if ( BEGIN_TAG_REGEXP.test(html) ) { + match = html.match( START_TAG_REGEXP ); + + if ( match ) { + html = html.substring( match[0].length ); + match[0].replace( START_TAG_REGEXP, parseStartTag ); + chars = false; } - escape = false; - } else if (ch == '\\') { - escape = true; - } else if (ch == quote) { - index++; - tokens.push({index:start, text:rawString, string:string, json:true, - fn:function(){ - return (string.length == dateParseLength) - ? angular['String']['toDate'](string) - : string; - }}); - return; - } else { - string += ch; } - index++; - } - throwError("Unterminated quote", start); - } -} -///////////////////////////////////////// + if ( chars ) { + index = html.indexOf("<"); -function parser(text, json){ - var ZERO = valueFn(0), - tokens = lex(text, json), - assignment = _assignment, - assignable = logicalOR, - functionCall = _functionCall, - fieldAccess = _fieldAccess, - objectIndex = _objectIndex, - filterChain = _filterChain, - functionIdent = _functionIdent, - pipeFunction = _pipeFunction; - 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 = - assignable = - filterChain = - functionIdent = - pipeFunction = - function (){ throwError("is not valid json", {text:text, index:0}); }; - } - //TODO: Shouldn't all of the public methods have assertAllConsumed? - //TODO: I think these should be public as part of the parser api instead of scope.$eval(). - return { - assignable: assertConsumed(assignable), - primary: assertConsumed(primary), - statements: assertConsumed(statements), - validator: assertConsumed(validator), - formatter: assertConsumed(formatter), - filter: assertConsumed(filter) - }; + var text = index < 0 ? html : html.substring( 0, index ); + html = index < 0 ? "" : html.substring( index ); - function assertConsumed(fn) { - return function(){ - var value = fn(); - if (tokens.length !== 0) { - throwError("is an unexpected token", tokens[0]); + if (handler.chars) handler.chars( decodeEntities(text) ); } - return value; - }; - } - /////////////////////////////////// - function throwError(msg, token) { - throw Error("Syntax Error: Token '" + token.text + - "' " + msg + " at column " + - (token.index + 1) + " of the expression [" + - text + "] starting at [" + text.substring(token.index) + "]."); - } + } else { + html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ + text = text. + replace(COMMENT_REGEXP, "$1"). + replace(CDATA_REGEXP, "$1"); - function peekToken() { - if (tokens.length === 0) - throw Error("Unexpected end of expression: " + text); - return tokens[0]; - } + if (handler.chars) handler.chars( decodeEntities(text) ); - function peek(e1, e2, e3, e4) { - if (tokens.length > 0) { - var token = tokens[0]; - var t = token.text; - 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); - if (token) { - if (json && !token.json) { - throwError("is not valid json", token); - } - tokens.shift(); - return token; - } - return false; - } - - function consume(e1){ - if (!expect(e1)) { - throwError("is unexpected, expecting [" + e1 + "]", peek()); - } - } - - function unaryFn(fn, right) { - return function(self) { - return fn(self, right(self)); - }; - } - - function binaryFn(left, fn, right) { - return function(self) { - return fn(self, left(self), right(self)); - }; - } - - function hasTokens () { - return tokens.length > 0; - } + return ""; + }); - function statements(){ - var statements = []; - while(true) { - if (tokens.length > 0 && !peek('}', ')', ';', ']')) - statements.push(filterChain()); - if (!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){ - var value; - for ( var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) - value = statement(self); - } - return value; - }; - } + parseEndTag( "", stack.last() ); } - } - function _filterChain(){ - var left = expression(); - var token; - while(true) { - if ((token = expect('|'))) { - left = binaryFn(left, token.fn, filter()); - } else { - return left; - } + if ( html == last ) { + throw "Parse Error: " + html; } + last = html; } - function filter(){ - return pipeFunction(angularFilter); - } - - function validator(){ - return pipeFunction(angularValidator); - } + // Clean up any remaining tags + parseEndTag(); - function formatter(){ - var token = expect(); - var formatter = angularFormatter[token.text]; - var argFns = []; - if (!formatter) throwError('is not a valid formatter.', token); - while(true) { - if ((token = expect(':'))) { - argFns.push(expression()); - } else { - return valueFn({ - format:invokeFn(formatter.format), - parse:invokeFn(formatter.parse) - }); + function parseStartTag( tag, tagName, rest, unary ) { + tagName = lowercase(tagName); + if ( blockElements[ tagName ] ) { + while ( stack.last() && inlineElements[ stack.last() ] ) { + parseEndTag( "", stack.last() ); } } - function invokeFn(fn){ - return function(self, input){ - var args = [input]; - for ( var i = 0; i < argFns.length; i++) { - args.push(argFns[i](self)); - } - return fn.apply(self, args); - }; - } - } - function _pipeFunction(fnScope){ - var fn = functionIdent(fnScope); - var argsFn = []; - var token; - while(true) { - if ((token = expect(':'))) { - argsFn.push(expression()); - } else { - var fnInvoke = function(self, input){ - var args = [input]; - for ( var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self)); - } - return fn.apply(self, args); - }; - return function(){ - return fnInvoke; - }; - } + if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { + parseEndTag( "", tagName ); } - } - function expression(){ - return assignment(); - } + unary = voidElements[ tagName ] || !!unary; - function _assignment(){ - var left = logicalOR(); - var right; - var token; - if ((token = expect('='))) { - if (!left.assign) { - throwError("implies assignment but [" + - text.substring(0, token.index) + "] can not be assigned to", token); - } - right = logicalOR(); - return function(self){ - return left.assign(self, right(self)); - }; - } else { - return left; - } - } + if ( !unary ) + stack.push( tagName ); - function logicalOR(){ - var left = logicalAND(); - var token; - while(true) { - if ((token = expect('||'))) { - left = binaryFn(left, token.fn, logicalAND()); - } else { - return left; - } - } - } + var attrs = {}; - function logicalAND(){ - var left = equality(); - var token; - if ((token = expect('&&'))) { - left = binaryFn(left, token.fn, logicalAND()); - } - return left; - } + rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { + var value = doubleQuotedValue + || singleQoutedValue + || unqoutedValue + || ''; - function equality(){ - var left = relational(); - var token; - if ((token = expect('==','!='))) { - left = binaryFn(left, token.fn, equality()); - } - return left; + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start( tagName, attrs, unary ); } - function relational(){ - var left = additive(); - var token; - if ((token = expect('<', '>', '<=', '>='))) { - left = binaryFn(left, token.fn, relational()); - } - return left; - } + function parseEndTag( tag, tagName ) { + var pos = 0, i; + tagName = lowercase(tagName); + if ( tagName ) + // Find the closest opened tag of the same type + for ( pos = stack.length - 1; pos >= 0; pos-- ) + if ( stack[ pos ] == tagName ) + break; - function additive(){ - var left = multiplicative(); - var token; - while ((token = expect('+','-'))) { - left = binaryFn(left, token.fn, multiplicative()); - } - return left; - } + if ( pos >= 0 ) { + // Close all the open elements, up the stack + for ( i = stack.length - 1; i >= pos; i-- ) + if (handler.end) handler.end( stack[ i ] ); - function multiplicative(){ - var left = unary(); - var token; - while ((token = expect('*','/','%'))) { - left = binaryFn(left, token.fn, unary()); + // Remove the open elements from the stack + stack.length = pos; } - return left; } +} - function unary(){ - 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()); - } else { - return primary(); - } - } +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +var hiddenPre=document.createElement("pre"); +function decodeEntities(value) { + hiddenPre.innerHTML=value.replace(//g, '>'); +} - function primary() { - var primary; - if (expect('(')) { - var expression = filterChain(); - consume(')'); - primary = expression; - } 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); +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf){ + var ignore = false; + var out = bind(buf, buf.push); + return { + start: function(tag, attrs, unary){ + tag = lowercase(tag); + if (!ignore && specialElements[tag]) { + ignore = tag; } - } - var next; - while ((next = expect('(', '[', '.'))) { - if (next.text === '(') { - primary = functionCall(primary); - } else if (next.text === '[') { - primary = objectIndex(primary); - } else if (next.text === '.') { - primary = fieldAccess(primary); - } else { - throwError("IMPOSSIBLE"); + if (!ignore && validElements[tag] == true) { + out('<'); + out(tag); + forEach(attrs, function(value, key){ + var lkey=lowercase(key); + if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out(unary ? '/>' : '>'); } - } - return primary; - } - - function _fieldAccess(object) { - var field = expect().text; - var getter = getterFn(field); - return extend(function (self){ - return getter(object(self)); - }, { - assign:function(self, value){ - return setter(object(self), field, value); + }, + end: function(tag){ + tag = lowercase(tag); + if (!ignore && validElements[tag] == true) { + out(''); + } + if (tag == ignore) { + ignore = false; + } + }, + chars: function(chars){ + if (!ignore) { + out(encodeEntities(chars)); + } } - }); - } + }; +} - function _objectIndex(obj) { - var indexFn = expression(); - consume(']'); - return extend( - function (self){ - var o = obj(self); - var i = indexFn(self); - return (o) ? o[i] : undefined; - }, { - assign:function(self, value){ - return obj(self)[indexFn(self)] = value; - } - }); - } +////////////////////////////////// +//JQLite +////////////////////////////////// - function _functionCall(fn) { - var argsFn = []; - if (peekToken().text != ')') { - do { - argsFn.push(expression()); - } while (expect(',')); - } - consume(')'); - return function (self){ - var args = []; - for ( var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self)); - } - var fnPtr = fn(self) || noop; - // IE stupidity! - return fnPtr.apply - ? fnPtr.apply(self, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); - }; - } +/** + * @ngdoc function + * @name angular.element + * @function + * + * @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. + * + * 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. + * + * Note: All element references in Angular are always wrapped with jQuery or jqLite; they are never + * raw DOM references. + * + * ## Angular's jQuery lite provides the following 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/bind/) + * - [children()](http://api.jquery.com/children/) + * - [clone()](http://api.jquery.com/clone/) + * - [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/) + * - [parent()](http://api.jquery.com/parent/) + * - [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/) + * - [unbind()](http://api.jquery.com/unbind/) + * - [val()](http://api.jquery.com/val/) + * + * ## In addtion to the above, Angular privides an additional method to both jQuery and jQuery lite: + * + * - `scope()` - retrieves the current Angular scope of the element. + * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top + * parent element is reached. + * + * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. + * @returns {Object} jQuery object. + */ - // This is used with json array declaration - function arrayDeclaration () { - var elementFns = []; - if (peekToken().text != ']') { - do { - elementFns.push(expression()); - } while (expect(',')); - } - consume(']'); - return function (self){ - var array = []; - for ( var i = 0; i < elementFns.length; i++) { - array.push(elementFns[i](self)); - } - return array; - }; - } +var jqCache = {}, + jqName = 'ng-' + new Date().getTime(), + jqId = 1, + addEventListenerFn = (window.document.addEventListener + ? function(element, type, fn) {element.addEventListener(type, fn, false);} + : function(element, type, fn) {element.attachEvent('on' + type, fn);}), + removeEventListenerFn = (window.document.removeEventListener + ? function(element, type, fn) {element.removeEventListener(type, fn, false); } + : function(element, type, fn) {element.detachEvent('on' + type, fn); }); - function object () { - var keyValues = []; - if (peekToken().text != '}') { - do { - var token = expect(), - key = token.string || token.text; - consume(":"); - var value = expression(); - keyValues.push({key:key, value:value}); - } while (expect(',')); - } - consume('}'); - return function (self){ - var object = {}; - for ( var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - var value = keyValue.value(self); - object[keyValue.key] = value; - } - return object; - }; - } +function jqNextId() { return (jqId++); } - function watchDecl () { - var anchorName = expect().text; - consume(":"); - var expressionFn; - if (peekToken().text == '{') { - consume("{"); - expressionFn = statements(); - consume("}"); - } else { - expressionFn = expression(); + +function getStyle(element) { + var current = {}, style = element[0].style, value, name, i; + if (typeof style.length == 'number') { + for(i = 0; i < style.length; i++) { + name = style[i]; + current[name] = style[name]; + } + } else { + for (name in style) { + value = style[name]; + if (1*name != name && name != 'cssText' && value && typeof value == 'string' && value !='false') + current[name] = value; } - return function(self) { - return {name:anchorName, fn:expressionFn}; - }; } + return current; } -////////////////////////////////////////////////// -// Parser helper functions -////////////////////////////////////////////////// -function setter(obj, path, setValue) { - var element = path.split('.'); - for (var i = 0; element.length > 1; i++) { - var key = element.shift(); - var propertyObj = obj[key]; - if (!propertyObj) { - propertyObj = {}; - obj[key] = propertyObj; - } - obj = propertyObj; - } - obj[element.shift()] = setValue; - return setValue; +/** + * Converts dash-separated names to camelCase. Useful for dealing with css properties. + */ +function camelCase(name) { + return name.replace(/\-(\w)/g, function(all, letter, offset){ + return (offset == 0 && letter == 'w') ? 'w' : letter.toUpperCase(); + }); } -/** - * Return the value accesible from the object by path. Any undefined traversals are ignored - * @param {Object} obj starting object - * @param {string} path path to traverse - * @param {boolean=true} bindFnToScope - * @returns value as accesbile by path - */ -function getter(obj, path, bindFnToScope) { - if (!path) return obj; - var keys = path.split('.'); - var key; - var lastInstance = obj; - var len = keys.length; +///////////////////////////////////////////// +// jQuery mutation patch +// +// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a +// $destroy event on all DOM nodes being removed. +// +///////////////////////////////////////////// - for (var i = 0; i < len; i++) { - key = keys[i]; - if (obj) { - obj = (lastInstance = obj)[key]; - } - if (isUndefined(obj) && key.charAt(0) == '$') { - var type = angularGlobal.typeOf(lastInstance); - type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; - var fn = type ? type[[key.substring(1)]] : _undefined; - if (fn) { - return obj = bind(lastInstance, fn, lastInstance); +function JQLitePatchJQueryRemove(name, dispatchThis) { + var originalJqFn = jQuery.fn[name]; + originalJqFn = originalJqFn.$original || originalJqFn; + removePatch.$original = originalJqFn; + jQuery.fn[name] = removePatch; + + function removePatch() { + var list = [this], + fireEvent = dispatchThis, + set, setIndex, setLength, + element, childIndex, childLength, children, + fns, data; + + while(list.length) { + set = list.shift(); + for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { + element = jqLite(set[setIndex]); + if (fireEvent) { + data = element.data('events'); + if ( (fns = data && data.$destroy) ) { + forEach(fns, function(fn){ + fn.handler(); + }); + } + } else { + fireEvent = !fireEvent; + } + for(childIndex = 0, childLength = (children = element.children()).length; + childIndex < childLength; + childIndex++) { + list.push(jQuery(children[childIndex])); + } } } + return originalJqFn.apply(this, arguments); } - if (!bindFnToScope && isFunction(obj)) { - return bind(lastInstance, obj); - } - return obj; } -var getterFnCache = {}, - compileCache = {}, - JS_KEYWORDS = {}; - -forEach( - ("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," + - "delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," + - "if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," + - "protected,public,return,short,static,super,switch,synchronized,this,throw,throws," + - "transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/), - function(key){ JS_KEYWORDS[key] = true;} -); - -function getterFn(path) { - var fn = getterFnCache[path]; - if (fn) return fn; - - var code = 'var l, fn, t;\n'; - forEach(path.split('.'), function(key) { - key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key; - code += 'if(!s) return s;\n' + - 'l=s;\n' + - 's=s' + key + ';\n' + - 'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l' + - key + '.apply(l, arguments); };\n'; - if (key.charAt(1) == '$') { - // special code for super-imposed functions - var name = key.substr(2); - code += 'if(!s) {\n' + - ' t = angular.Global.typeOf(l);\n' + - ' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' + - ' if (fn) s = function(){ return fn.apply(l, ' + - '[l].concat(Array.prototype.slice.call(arguments, 0))); };\n' + - '}\n'; - } - }); - code += 'return s;'; - fn = Function('s', code); - fn["toString"] = function(){ return code; }; +///////////////////////////////////////////// +function jqLiteWrap(element) { + if (isString(element) && element.charAt(0) != '<') { + throw new Error('selectors not implemented'); + } + return new JQLite(element); +} - return getterFnCache[path] = fn; +function JQLite(element) { + if (element instanceof JQLite) { + return element; + } else if (isString(element)) { + var div = document.createElement('div'); + // Read about the NoScope elements here: + // http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx + div.innerHTML = '
 
' + element; // IE insanity to make NoScope elements work! + div.removeChild(div.firstChild); // remove the superfluous div + JQLiteAddNodes(this, div.childNodes); + this.remove(); // detach the elements from the temporary DOM div. + } else { + JQLiteAddNodes(this, element); + } } -/////////////////////////////////// +function JQLiteClone(element) { + return element.cloneNode(true); +} -// TODO(misko): Should this function be public? -function compileExpr(expr) { - return parser(expr).statements(); +function JQLiteDealoc(element){ + JQLiteRemoveData(element); + for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { + JQLiteDealoc(children[i]); + } } -// TODO(misko): Deprecate? Remove! -// I think that compilation should be a service. -function expressionCompile(exp) { - if (isFunction(exp)) return exp; - var fn = compileCache[exp]; - if (!fn) { - fn = compileCache[exp] = parser(exp).statements(); +function JQLiteRemoveData(element) { + var cacheId = element[jqName], + cache = jqCache[cacheId]; + if (cache) { + if (cache.bind) { + forEach(cache.bind, function(fn, type){ + if (type == '$destroy') { + fn({}); + } else { + removeEventListenerFn(element, type, fn); + } + }); + } + delete jqCache[cacheId]; + element[jqName] = undefined; // ie does not allow deletion of attributes on elements. } - return fn; } -function Route(template, defaults) { - this.template = template = template + '#'; - this.defaults = defaults || {}; - var urlParams = this.urlParams = {}; - forEach(template.split(/\W/), function(param){ - if (param && template.match(new RegExp(":" + param + "\\W"))) { - urlParams[param] = true; +function JQLiteData(element, key, value) { + var cacheId = element[jqName], + cache = jqCache[cacheId || -1]; + if (isDefined(value)) { + if (!cache) { + element[jqName] = cacheId = jqNextId(); + cache = jqCache[cacheId] = {}; } - }); + cache[key] = value; + } else { + return cache ? cache[key] : null; + } } -Route.prototype = { - url: function(params) { - var self = this, - url = this.template, - encodedVal; +function JQLiteHasClass(element, selector) { + return ((" " + element.className + " ").replace(/[\n\t]/g, " "). + indexOf( " " + selector + " " ) > -1); +} - params = params || {}; - forEach(this.urlParams, function(_, urlParam){ - encodedVal = encodeUriSegment(params[urlParam] || self.defaults[urlParam] || ""); - url = url.replace(new RegExp(":" + urlParam + "(\\W)"), encodedVal + "$1"); +function JQLiteRemoveClass(element, selector) { + if (selector) { + forEach(selector.split(' '), function(cssClass) { + element.className = trim( + (" " + element.className + " ") + .replace(/[\n\t]/g, " ") + .replace(" " + trim(cssClass) + " ", " ") + ); }); - url = url.replace(/\/?#$/, ''); - var query = []; - forEachSorted(params, function(value, key){ - if (!self.urlParams[key]) { - query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); + } +} + +function JQLiteAddClass(element, selector) { + if (selector) { + forEach(selector.split(' '), function(cssClass) { + if (!JQLiteHasClass(element, cssClass)) { + element.className = trim(element.className + ' ' + trim(cssClass)); } }); - url = url.replace(/\/*$/, ''); - return url + (query.length ? '?' + query.join('&') : ''); } -}; +} -function ResourceFactory(xhr) { - this.xhr = xhr; +function JQLiteAddNodes(root, elements) { + if (elements) { + elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) + ? elements + : [ elements ]; + for(var i=0; i < elements.length; i++) { + root.push(elements[i]); + } + } } -ResourceFactory.DEFAULT_ACTIONS = { - 'get': {method:'GET'}, - 'save': {method:'POST'}, - 'query': {method:'GET', isArray:true}, - 'remove': {method:'DELETE'}, - 'delete': {method:'DELETE'} -}; +////////////////////////////////////////// +// Functions which are declared directly. +////////////////////////////////////////// +var JQLitePrototype = JQLite.prototype = { + ready: function(fn) { + var fired = false; -ResourceFactory.prototype = { - route: function(url, paramDefaults, actions){ - var self = this; - var route = new Route(url); - actions = extend({}, ResourceFactory.DEFAULT_ACTIONS, actions); - function extractParams(data){ - var ids = {}; - forEach(paramDefaults || {}, function(value, key){ - ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; - }); - return ids; + function trigger() { + if (fired) return; + fired = true; + fn(); } - function Resource(value){ - copy(value || {}, this); - } + this.bind('DOMContentLoaded', trigger); // works for modern browsers and IE9 + // we can not use jqLite since we are not done loading and jQuery could be loaded later. + jqLiteWrap(window).bind('load', trigger); // fallback to window.onload for others + }, + toString: function() { + var value = []; + forEach(this, function(e){ value.push('' + e);}); + return '[' + value.join(', ') + ']'; + }, - forEach(actions, function(action, name){ - var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; - Resource[name] = function (a1, a2, a3, a4) { - var params = {}; - var data; - var success = noop; - var error = null; - switch(arguments.length) { - case 4: - error = a4; - success = a3; - //fallthrough - case 3: - case 2: - if (isFunction(a2)) { - if (isFunction(a1)) { - success = a1; - error = a2; - break; - } + eq: function(index) { + return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); + }, - success = a2; - error = a3; - //fallthrough - } else { - params = a1; - data = a2; - success = a3; - break; - } - case 1: - if (isFunction(a1)) success = a1; - else if (isPostOrPut) data = a1; - else params = a1; - break; - case 0: break; - default: - throw "Expected between 0-4 arguments [params, data, success, error], got " + - arguments.length + " arguments."; - } + length: 0, + push: push, + sort: [].sort, + splice: [].splice +}; - var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); - self.xhr( - action.method, - route.url(extend({}, action.params || {}, extractParams(data), params)), - data, - function(status, response) { - if (response) { - if (action.isArray) { - value.length = 0; - forEach(response, function(item) { - value.push(new Resource(item)); - }); - } else { - copy(response, value); - } - } - (success||noop)(value); - }, - error || action.verifyCache, - action.verifyCache); - return value; - }; +////////////////////////////////////////// +// Functions iterating getter/setters. +// these functions return self on setter and +// value on get. +////////////////////////////////////////// +var BOOLEAN_ATTR = {}; +forEach('multiple,selected,checked,disabled,readOnly,required'.split(','), function(value) { + BOOLEAN_ATTR[lowercase(value)] = value; +}); - Resource.bind = function(additionalParamDefaults){ - return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); - }; +forEach({ + data: JQLiteData, + inheritedData: function(element, name, value) { + element = jqLite(element); + while (element.length) { + if (value = element.data(name)) return value; + element = element.parent(); + } + }, - Resource.prototype['$' + name] = function(a1, a2, a3) { - var params = extractParams(this), - success = noop, - error; + scope: function(element) { + return jqLite(element).inheritedData($$scope); + }, - switch(arguments.length) { - case 3: params = a1; success = a2; error = a3; break; - case 2: - case 1: - if (isFunction(a1)) { - success = a1; - error = a2; - } else { - params = a1; - success = a2 || noop; - } - case 0: break; - default: - throw "Expected between 1-3 arguments [params, success, error], got " + - arguments.length + " arguments."; - } - var data = isPostOrPut ? this : undefined; - Resource[name].call(this, params, data, success, error); - }; - }); - return Resource; - } -}; + removeAttr: function(element,name) { + element.removeAttribute(name); + }, -////////////////////////////// -// Browser -////////////////////////////// -var XHR = window.XMLHttpRequest || function () { - try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} - try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} - try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} - throw new Error("This browser does not support XMLHttpRequest."); -}; + hasClass: JQLiteHasClass, + css: function(element, name, value) { + name = camelCase(name); -/** - * @ngdoc service - * @name angular.service.$browser - * @requires $log - * - * @description - * Constructor for the object exposed as $browser service. - * - * This object has two goals: - * - * - hide all the global state in the browser caused by the window object - * - abstract away all the browser specific features and inconsistencies - * - * For tests we provide {@link angular.mock.service.$browser mock implementation} of the `$browser` - * service, which can be used for convenient testing of the application without the interaction with - * the real browser apis. - */ -/** - * @param {object} window The global window object. - * @param {object} document jQuery wrapped document. - * @param {object} body jQuery wrapped document.body. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. - */ -function Browser(window, document, body, XHR, $log) { - var self = this, - rawDocument = document[0], - location = window.location, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - pendingDeferIds = {}, - lastLocationUrl; + if (isDefined(value)) { + element.style[name] = value; + } else { + var val; - self.isMock = false; + if (msie <= 8) { + // this is some IE specific weirdness that jQuery 1.6.4 does not sure why + val = element.currentStyle && element.currentStyle[name]; + if (val === '') val = 'auto'; + } - ////////////////////////////////////////////////////////////// - // XHR API - ////////////////////////////////////////////////////////////// - var idCounter = 0; - var outstandingRequestCount = 0; - var outstandingRequestCallbacks = []; + val = val || element.style[name]; + if (msie <= 8) { + // jquery weirdness :-/ + val = (val === '') ? undefined : val; + } - /** - * Executes the `fn` function (supports currying) and decrements the `outstandingRequestCallbacks` - * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. - */ - function completeOutstandingRequest(fn) { - try { - fn.apply(null, sliceArgs(arguments, 1)); - } finally { - outstandingRequestCount--; - if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { - try { - outstandingRequestCallbacks.pop()(); - } catch (e) { - $log.error(e); - } + return val; + } + }, + + attr: function(element, name, value){ + var lowercasedName = lowercase(name); + if (BOOLEAN_ATTR[lowercasedName]) { + if (isDefined(value)) { + if (!!value) { + element[name] = true; + element.setAttribute(name, lowercasedName); + } else { + element[name] = false; + element.removeAttribute(lowercasedName); } + } else { + return (element[name] || + element.getAttribute(name) !== null && + (msie < 9 ? element.getAttribute(name) !== '' : true)) + ? lowercasedName + : undefined; } + } else if (isDefined(value)) { + element.setAttribute(name, value); + } else if (element.getAttribute) { + // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code + // some elements (e.g. Document) don't have get attribute, so return undefined + var ret = element.getAttribute(name, 2); + // normalize non-existing attributes to undefined (as jQuery) + return ret === null ? undefined : ret; } - } + }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#xhr - * @methodOf angular.service.$browser - * - * @param {string} method Requested method (get|post|put|delete|head|json) - * @param {string} url Requested url - * @param {?string} post Post data to send (null if nothing to post) - * @param {function(number, string)} callback Function that will be called on response - * @param {object=} header additional HTTP headers to send with XHR. - * Standard headers are: - *
    - *
  • Content-Type: application/x-www-form-urlencoded
  • - *
  • Accept: application/json, text/plain, */*
  • - *
  • X-Requested-With: XMLHttpRequest
  • - *
- * - * @description - * Send ajax request - */ - self.xhr = function(method, url, post, callback, headers) { - outstandingRequestCount ++; - if (lowercase(method) == 'json') { - var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, ''); - window[callbackId] = function(data) { - window[callbackId].data = data; - }; + prop: function(element, name, value) { + if (isDefined(value)) { + element[name] = value; + } else { + return element[name]; + } + }, - var script = self.addJs(url.replace('JSON_CALLBACK', callbackId), null, function() { - if (window[callbackId].data) { - completeOutstandingRequest(callback, 200, window[callbackId].data); + text: extend((msie < 9) + ? function(element, value) { + // NodeType == 3 is text node + if (element.nodeType == 3) { + if (isUndefined(value)) + return element.nodeValue; + element.nodeValue = value; } else { - completeOutstandingRequest(callback); + if (isUndefined(value)) + return element.innerText; + element.innerText = value; } - delete window[callbackId]; - body[0].removeChild(script); - }); - } else { - var xhr = new XHR(); - xhr.open(method, url, true); - forEach(headers, function(value, key) { - if (value) xhr.setRequestHeader(key, value); - }); - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - var status = xhr.status == 1223 ? 204 : xhr.status || 200; - completeOutstandingRequest(callback, status, xhr.responseText); + } + : function(element, value) { + if (isUndefined(value)) { + return element.textContent; } - }; - xhr.send(post || ''); + element.textContent = value; + }, {$dv:''}), + + val: function(element, value) { + if (isUndefined(value)) { + return element.value; } - }; + element.value = value; + }, + html: function(element, value) { + if (isUndefined(value)) { + return element.innerHTML; + } + for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { + JQLiteDealoc(childNodes[i]); + } + element.innerHTML = value; + } +}, function(fn, name){ /** - * @private - * Note: this method is used only by scenario runner - * TODO(vojta): prefix this method with $$ ? - * @param {function()} callback Function that will be called when no outstanding request + * Properties: writes return selection, reads return first value */ - self.notifyWhenNoOutstandingRequests = function(callback) { - // force browser to execute all pollFns - this is needed so that cookies and other pollers fire - // at some deterministic time in respect to the test runner's actions. Leaving things up to the - // regular poller would result in flaky tests. - forEach(pollFns, function(pollFn){ pollFn(); }); + JQLite.prototype[name] = function(arg1, arg2) { + var i, key; - if (outstandingRequestCount === 0) { - callback(); + // JQLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it + // in a way that survives minification. + if (((fn.length == 2 && fn !== JQLiteHasClass) ? arg1 : arg2) === undefined) { + if (isObject(arg1)) { + // we are a write, but the object properties are the key/values + for(i=0; i < this.length; i++) { + for (key in arg1) { + fn(this[i], key, arg1[key]); + } + } + // return self for chaining + return this; + } else { + // we are a read, so read the first child. + if (this.length) + return fn(this[0], arg1, arg2); + } } else { - outstandingRequestCallbacks.push(callback); + // we are a write, so apply to all children + for(i=0; i < this.length; i++) { + fn(this[i], arg1, arg2); + } + // return self for chaining + return this; } + return fn.$dv; }; +}); - ////////////////////////////////////////////////////////////// - // Poll Watcher API - ////////////////////////////////////////////////////////////// - var pollFns = [], - pollTimeout; +////////////////////////////////////////// +// Functions iterating traversal. +// These functions chain results into a single +// selector. +////////////////////////////////////////// +forEach({ + removeData: JQLiteRemoveData, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#addPollFn - * @methodOf angular.service.$browser - * - * @param {function()} fn Poll function to add - * - * @description - * Adds a function to the list of functions that poller periodically executes, - * and starts polling if not started yet. - * - * @returns {function()} the added function - */ - self.addPollFn = function(fn) { - if (isUndefined(pollTimeout)) startPoller(100, setTimeout); - pollFns.push(fn); - return fn; - }; + dealoc: JQLiteDealoc, - /** - * @param {number} interval How often should browser call poll functions (ms) - * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. - * - * @description - * Configures the poller to run in the specified intervals, using the specified - * setTimeout fn and kicks it off. - */ - function startPoller(interval, setTimeout) { - (function check() { - forEach(pollFns, function(pollFn){ pollFn(); }); - pollTimeout = setTimeout(check, interval); - })(); - } + bind: function(element, type, fn){ + var bind = JQLiteData(element, 'bind'); + if (!bind) JQLiteData(element, 'bind', bind = {}); + forEach(type.split(' '), function(type){ + var eventHandler = bind[type]; + if (!eventHandler) { + bind[type] = eventHandler = function(event) { + if (!event.preventDefault) { + event.preventDefault = function() { + event.returnValue = false; //ie + }; + } + if (!event.stopPropagation) { + event.stopPropagation = function() { + event.cancelBubble = true; //ie + }; + } + if (!event.target) { + event.target = event.srcElement || document; + } - ////////////////////////////////////////////////////////////// - // URL API - ////////////////////////////////////////////////////////////// + if (isUndefined(event.defaultPrevented)) { + var prevent = event.preventDefault; + event.preventDefault = function() { + event.defaultPrevented = true; + prevent.call(event); + }; + event.defaultPrevented = false; + } - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#setUrl - * @methodOf angular.service.$browser - * - * @param {string} url New url - * - * @description - * Sets browser's url - */ - self.setUrl = function(url) { + event.isDefaultPrevented = function() { + return event.defaultPrevented; + }; + + forEach(eventHandler.fns, function(fn){ + fn.call(element, event); + }); + }; + eventHandler.fns = []; + addEventListenerFn(element, type, eventHandler); + } + eventHandler.fns.push(fn); + }); + }, + + unbind: function(element, type, fn) { + var bind = JQLiteData(element, 'bind'); + if (!bind) return; //no listeners registered - var existingURL = lastLocationUrl; - if (!existingURL.match(/#/)) existingURL += '#'; - if (!url.match(/#/)) url += '#'; - if (existingURL != url) { - location.href = url; + if (isUndefined(type)) { + forEach(bind, function(eventHandler, type) { + removeEventListenerFn(element, type, eventHandler); + delete bind[type]; + }); + } else { + if (isUndefined(fn)) { + removeEventListenerFn(element, type, bind[type]); + delete bind[type]; + } else { + arrayRemove(bind[type].fns, fn); + } } - }; + }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#getUrl - * @methodOf angular.service.$browser - * - * @description - * Get current browser's url - * - * @returns {string} Browser's url - */ - self.getUrl = function() { - return lastLocationUrl = location.href; - }; + replaceWith: function(element, replaceNode) { + var index, parent = element.parentNode; + JQLiteDealoc(element); + forEach(new JQLite(replaceNode), function(node){ + if (index) { + parent.insertBefore(node, index.nextSibling); + } else { + parent.replaceChild(node, element); + } + index = node; + }); + }, + children: function(element) { + var children = []; + forEach(element.childNodes, function(element){ + if (element.nodeName != '#text') + children.push(element); + }); + return children; + }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#onHashChange - * @methodOf angular.service.$browser - * - * @description - * Detects if browser support onhashchange events and register a listener otherwise registers - * $browser poller. The `listener` will then get called when the hash changes. - * - * The listener gets called with either HashChangeEvent object or simple object that also contains - * `oldURL` and `newURL` properties. - * - * Note: this api is intended for use only by the $location service. Please use the - * {@link angular.service.$location $location service} to monitor hash changes in angular apps. - * - * @param {function(event)} listener Listener function to be called when url hash changes. - * @return {function()} Returns the registered listener fn - handy if the fn is anonymous. - */ - self.onHashChange = function(listener) { - // IE8 comp mode returns true, but doesn't support hashchange event - var dm = window.document.documentMode; - if ('onhashchange' in window && (isUndefined(dm) || dm >= 8)) { - jqLite(window).bind('hashchange', listener); - } else { - var lastBrowserUrl = self.getUrl(); + append: function(element, node) { + forEach(new JQLite(node), function(child){ + if (element.nodeType === 1) + element.appendChild(child); + }); + }, - self.addPollFn(function() { - if (lastBrowserUrl != self.getUrl()) { - listener(); - lastBrowserUrl = self.getUrl(); + prepend: function(element, node) { + if (element.nodeType === 1) { + var index = element.firstChild; + forEach(new JQLite(node), function(child){ + if (index) { + element.insertBefore(child, index); + } else { + element.appendChild(child); + index = child; } }); } - return listener; - }; - - ////////////////////////////////////////////////////////////// - // Cookies API - ////////////////////////////////////////////////////////////// - var lastCookies = {}; - var lastCookieString = ''; + }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#cookies - * @methodOf angular.service.$browser - * - * @param {string=} name Cookie name - * @param {string=} value Cokkie value - * - * @description - * The cookies method provides a 'private' low level access to browser cookies. - * It is not meant to be used directly, use the $cookie service instead. - * - * The return values vary depending on the arguments that the method was called with as follows: - *
    - *
  • cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
  • - *
  • cookies(name, value) -> set name to value, if value is undefined delete the cookie
  • - *
  • cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
  • - *
- * - * @returns {Object} Hash of all cookies (if called without any parameter) - */ - self.cookies = function (name, value) { - var cookieLength, cookieArray, cookie, i, keyValue, index; + remove: function(element) { + JQLiteDealoc(element); + var parent = element.parentNode; + if (parent) parent.removeChild(element); + }, - if (name) { - if (value === undefined) { - rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - rawDocument.cookie = escape(name) + '=' + escape(value); + after: function(element, newElement) { + var index = element, parent = element.parentNode; + forEach(new JQLite(newElement), function(node){ + parent.insertBefore(node, index.nextSibling); + index = node; + }); + }, - cookieLength = name.length + value.length + 1; - if (cookieLength > 4096) { - $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ - cookieLength + " > 4096 bytes)!"); - } - if (lastCookies.length > 20) { - $log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " + - "were already set (" + lastCookies.length + " > 20 )"); - } - } - } - } else { - if (rawDocument.cookie !== lastCookieString) { - lastCookieString = rawDocument.cookie; - cookieArray = lastCookieString.split("; "); - lastCookies = {}; + addClass: JQLiteAddClass, + removeClass: JQLiteRemoveClass, - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1)); - } - } - } - return lastCookies; + toggleClass: function(element, selector, condition) { + if (isUndefined(condition)) { + condition = !JQLiteHasClass(element, selector); } - }; + (condition ? JQLiteAddClass : JQLiteRemoveClass)(element, selector); + }, + parent: function(element) { + var parent = element.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#defer - * @methodOf angular.service.$browser - * @param {function()} fn A function, who's execution should be defered. - * @param {number=} [delay=0] of milliseconds to defer the function execution. - * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. - * - * @description - * Executes a fn asynchroniously via `setTimeout(fn, delay)`. - * - * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using - * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed - * via `$browser.defer.flush()`. - * - */ - self.defer = function(fn, delay) { - var timeoutId; - outstandingRequestCount++; - timeoutId = setTimeout(function() { - delete pendingDeferIds[timeoutId]; - completeOutstandingRequest(fn); - }, delay || 0); - pendingDeferIds[timeoutId] = true; - return timeoutId; - }; + next: function(element) { + return element.nextSibling; + }, + find: function(element, selector) { + return element.getElementsByTagName(selector); + }, + clone: JQLiteClone +}, function(fn, name){ /** - * THIS DOC IS NOT VISIBLE because ngdocs can't process docs for foo#method.method - * - * @name angular.service.$browser#defer.cancel - * @methodOf angular.service.$browser.defer - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. - * - * @description - * Cancels a defered task identified with `deferId`. + * chaining functions */ - - self.defer.cancel = function(deferId) { - if (pendingDeferIds[deferId]) { - delete pendingDeferIds[deferId]; - clearTimeout(deferId); - completeOutstandingRequest(noop); - return true; + JQLite.prototype[name] = function(arg1, arg2) { + var value; + for(var i=0; i < this.length; i++) { + if (value == undefined) { + value = fn(this[i], arg1, arg2); + if (value !== undefined) { + // any function which returns a value needs to be wrapped + value = jqLite(value); + } + } else { + JQLiteAddNodes(value, fn(this[i], arg1, arg2)); + } } + return value == undefined ? this : value; }; +}); +/** + * Computes a hash of an 'obj'. + * Hash of a: + * string is string + * number is number as string + * object is either result of calling $$hashKey function on the object or uniquely generated id, + * that is also assigned to the $$hashKey property of the object. + * + * @param obj + * @returns {string} hash string such that the same input will have the same hash string. + * The resulting string key is in 'type:hashKey' format. + */ +function hashKey(obj) { + var objType = typeof obj, + key; - ////////////////////////////////////////////////////////////// - // Misc API - ////////////////////////////////////////////////////////////// - var hoverListener = noop; - + if (objType == 'object' && obj !== null) { + if (typeof (key = obj.$$hashKey) == 'function') { + // must invoke on object to keep the right this + key = obj.$$hashKey(); + } else if (key === undefined) { + key = obj.$$hashKey = nextUid(); + } + } else { + key = obj; + } + + return objType + ':' + key; +} + +/** + * HashMap which can use objects as keys + */ +function HashMap(array){ + forEach(array, this.put, this); +} +HashMap.prototype = { /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#hover - * @methodOf angular.service.$browser - * - * @description - * Set hover listener. - * - * @param {function(Object, boolean)} listener Function that will be called when a hover event - * occurs. + * Store key value pair + * @param key key to store can be any type + * @param value value to store can be any type */ - self.hover = function(listener) { hoverListener = listener; }; + put: function(key, value) { + this[hashKey(key)] = value; + }, /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#bind - * @methodOf angular.service.$browser - * - * @description - * Register hover function to real browser + * @param key + * @returns the value for the key */ - self.bind = function() { - document.bind("mouseover", function(event){ - hoverListener(jqLite(msie ? event.srcElement : event.target), true); - return true; - }); - document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){ - hoverListener(jqLite(event.target), false); - return true; - }); - }; - + get: function(key) { + return this[hashKey(key)]; + }, /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#addCss - * @methodOf angular.service.$browser - * - * @param {string} url Url to css file - * @description - * Adds a stylesheet tag to the head. + * Remove the key/value pair + * @param key */ - self.addCss = function(url) { - var link = jqLite(rawDocument.createElement('link')); - link.attr('rel', 'stylesheet'); - link.attr('type', 'text/css'); - link.attr('href', url); - body.append(link); - }; - + remove: function(key) { + var value = this[key = hashKey(key)]; + delete this[key]; + return value; + } +}; +/** + * A map where multiple values can be added to the same key such that they form a queue. + * @returns {HashQueueMap} + */ +function HashQueueMap() {} +HashQueueMap.prototype = { /** - * @workInProgress - * @ngdoc method - * @name angular.service.$browser#addJs - * @methodOf angular.service.$browser - * - * @param {string} url Url to js file - * @param {string=} domId Optional id for the script tag - * - * @description - * Adds a script tag to the head. + * Same as array push, but using an array as the value for the hash */ - self.addJs = function(url, domId, done) { - // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: - // - fetches local scripts via XHR and evals them - // - adds and immediately removes script elements from the document - // - // We need addJs to be able to add angular-ie-compat.js which is very special and must remain - // part of the DOM so that the embedded images can reference it. jQuery's append implementation - // (v1.4.2) fubars it. - var script = rawDocument.createElement('script'); - - script.type = 'text/javascript'; - script.src = url; - if (domId) script.id = domId; - - if (msie) { - script.onreadystatechange = function() { - /loaded|complete/.test(script.readyState) && done && done(); - }; + push: function(key, value) { + var array = this[key = hashKey(key)]; + if (!array) { + this[key] = [value]; } else { - if (done) script.onload = script.onerror = done; + array.push(value); } + }, - body[0].appendChild(script); - - return script; - }; -} - -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + /** + * Same as array shift, but using an array as the value for the hash + */ + shift: function(key) { + var array = this[key = hashKey(key)]; + if (array) { + if (array.length == 1) { + delete this[key]; + return array[0]; + } else { + return array.shift(); + } + } + } +}; +/** + * @ngdoc function + * @name angular.module.ng.$anchorScroll + * @requires $window + * @requires $location + * @requires $rootScope * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); + * @description + * When called, it checks current value of `$location.hash()` and scroll to related element, + * according to rules specified in + * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. * + * It also watches the `$location.hash()` and scroll whenever it changes to match any anchor. + * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. */ +function $AnchorScrollProvider() { -// Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, - END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, - ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, - BEGIN_TAG_REGEXP = /^/g, - CDATA_REGEXP = //g, - URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) + var autoScrollingEnabled = true; -// Empty Elements - HTML 4.01 -var emptyElements = makeMap("area,br,col,hr,img"); + this.disableAutoScrolling = function() { + autoScrollingEnabled = false; + }; -// Block Elements - HTML 4.01 -var blockElements = makeMap("address,blockquote,center,dd,del,dir,div,dl,dt,"+ - "hr,ins,li,map,menu,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul"); + this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { + var document = $window.document; + + // helper function to get first anchor from a NodeList + // can't use filter.filter, as it accepts only instances of Array + // and IE can't convert NodeList to an array using [].slice + // TODO(vojta): use filter if we change it to accept lists as well + function getFirstAnchor(list) { + var result = null; + forEach(list, function(element) { + if (!result && lowercase(element.nodeName) === 'a') result = element; + }); + return result; + } -// Inline Elements - HTML 4.01 -var inlineElements = makeMap("a,abbr,acronym,b,bdo,big,br,cite,code,del,dfn,em,font,i,img,"+ - "ins,kbd,label,map,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var"); -// Elements that you can, intentionally, leave open -// (and which close themselves) -var closeSelfElements = makeMap("colgroup,dd,dt,li,p,td,tfoot,th,thead,tr"); -// Special Elements (can contain anything) -var specialElements = makeMap("script,style"); -var validElements = extend({}, emptyElements, blockElements, inlineElements, closeSelfElements); + function scroll() { + var hash = $location.hash(), elm; -//Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,href,longdesc,src,usemap"); -var validAttrs = extend({}, uriAttrs, makeMap( - 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ - 'scope,scrolling,shape,span,start,summary,target,title,type,'+ - 'valign,value,vspace,width')); + // empty hash, scroll to the top of the page + if (!hash) $window.scrollTo(0, 0); -/** - * @example - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * @param {string} html string - * @param {object} handler - */ -function htmlParser( html, handler ) { - var index, chars, match, stack = [], last = html; - stack.last = function(){ return stack[ stack.length - 1 ]; }; + // element with given id + else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); - while ( html ) { - chars = true; + // first anchor with given name :-D + else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); - // Make sure we're not in a script or style element - if ( !stack.last() || !specialElements[ stack.last() ] ) { + // no element and hash == 'top', scroll to the top of the page + else if (hash === 'top') $window.scrollTo(0, 0); + } - // Comment - if ( html.indexOf(""); + // does not scroll when user clicks on anchor link that is currently on + // (no url change, no $locaiton.hash() change), browser native does scroll + if (autoScrollingEnabled) { + $rootScope.$watch(function() {return $location.hash();}, function() { + $rootScope.$evalAsync(scroll); + }); + } - if ( index >= 0 ) { - if (handler.comment) handler.comment( html.substring( 4, index ) ); - html = html.substring( index + 3 ); - chars = false; - } + return scroll; + }]; +} - // end tag - } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { - match = html.match( END_TAG_REGEXP ); +/** + * @ngdoc object + * @name angular.module.ng.$browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link angular.module.ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ +/** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @param {object} body jQuery wrapped document.body. + * @param {function()} XHR XMLHttpRequest constructor. + * @param {object} $log console.log or an object with the same interface. + * @param {object} $sniffer $sniffer service + */ +function Browser(window, document, body, $log, $sniffer) { + var self = this, + rawDocument = document[0], + location = window.location, + history = window.history, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + pendingDeferIds = {}; - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( END_TAG_REGEXP, parseEndTag ); - chars = false; - } + self.isMock = false; - // start tag - } else if ( BEGIN_TAG_REGEXP.test(html) ) { - match = html.match( START_TAG_REGEXP ); + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( START_TAG_REGEXP, parseStartTag ); - chars = false; + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = completeOutstandingRequest; + self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; + + /** + * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` + * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. + */ + function completeOutstandingRequest(fn) { + try { + fn.apply(null, sliceArgs(arguments, 1)); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while(outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + $log.error(e); + } } } + } + } - if ( chars ) { - index = html.indexOf("<"); - - var text = index < 0 ? html : html.substring( 0, index ); - html = index < 0 ? "" : html.substring( index ); - - if (handler.chars) handler.chars( decodeEntities(text) ); - } + /** + * @private + * Note: this method is used only by scenario runner + * TODO(vojta): prefix this method with $$ ? + * @param {function()} callback Function that will be called when no outstanding request + */ + self.notifyWhenNoOutstandingRequests = function(callback) { + // force browser to execute all pollFns - this is needed so that cookies and other pollers fire + // at some deterministic time in respect to the test runner's actions. Leaving things up to the + // regular poller would result in flaky tests. + forEach(pollFns, function(pollFn){ pollFn(); }); + if (outstandingRequestCount === 0) { + callback(); } else { - html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ - text = text. - replace(COMMENT_REGEXP, "$1"). - replace(CDATA_REGEXP, "$1"); - - if (handler.chars) handler.chars( decodeEntities(text) ); - - return ""; - }); - - parseEndTag( "", stack.last() ); + outstandingRequestCallbacks.push(callback); } + }; - if ( html == last ) { - throw "Parse Error: " + html; - } - last = html; - } + ////////////////////////////////////////////////////////////// + // Poll Watcher API + ////////////////////////////////////////////////////////////// + var pollFns = [], + pollTimeout; - // Clean up any remaining tags - parseEndTag(); + /** + * @ngdoc method + * @name angular.module.ng.$browser#addPollFn + * @methodOf angular.module.ng.$browser + * + * @param {function()} fn Poll function to add + * + * @description + * Adds a function to the list of functions that poller periodically executes, + * and starts polling if not started yet. + * + * @returns {function()} the added function + */ + self.addPollFn = function(fn) { + if (isUndefined(pollTimeout)) startPoller(100, setTimeout); + pollFns.push(fn); + return fn; + }; - function parseStartTag( tag, tagName, rest, unary ) { - tagName = lowercase(tagName); - if ( blockElements[ tagName ] ) { - while ( stack.last() && inlineElements[ stack.last() ] ) { - parseEndTag( "", stack.last() ); - } - } + /** + * @param {number} interval How often should browser call poll functions (ms) + * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. + * + * @description + * Configures the poller to run in the specified intervals, using the specified + * setTimeout fn and kicks it off. + */ + function startPoller(interval, setTimeout) { + (function check() { + forEach(pollFns, function(pollFn){ pollFn(); }); + pollTimeout = setTimeout(check, interval); + })(); + } - if ( closeSelfElements[ tagName ] && stack.last() == tagName ) { - parseEndTag( "", tagName ); - } + ////////////////////////////////////////////////////////////// + // URL API + ////////////////////////////////////////////////////////////// - unary = emptyElements[ tagName ] || !!unary; + var lastBrowserUrl = location.href; - if ( !unary ) - stack.push( tagName ); + /** + * @ngdoc method + * @name angular.module.ng.$browser#url + * @methodOf angular.module.ng.$browser + * + * @description + * GETTER: + * Without any argument, this method just returns current value of location.href. + * + * SETTER: + * With at least one argument, this method sets url to new value. + * If html5 history api supported, pushState/replaceState is used, otherwise + * location.href/location.replace is used. + * Returns its own instance to allow chaining + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link angular.module.ng.$location $location service} to change url. + * + * @param {string} url New url (when used as setter) + * @param {boolean=} replace Should new url replace current history record ? + */ + self.url = function(url, replace) { + // setter + if (url) { + lastBrowserUrl = url; + if ($sniffer.history) { + if (replace) history.replaceState(null, '', url); + else history.pushState(null, '', url); + } else { + if (replace) location.replace(url); + else location.href = url; + } + return self; + // getter + } else { + return location.href; + } + }; - var attrs = {}; + var urlChangeListeners = [], + urlChangeInit = false; - rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { - var value = doubleQuotedValue - || singleQoutedValue - || unqoutedValue - || ''; + function fireUrlChange() { + if (lastBrowserUrl == self.url()) return; - attrs[name] = decodeEntities(value); + lastBrowserUrl = self.url(); + forEach(urlChangeListeners, function(listener) { + listener(self.url()); }); - if (handler.start) handler.start( tagName, attrs, unary ); } - function parseEndTag( tag, tagName ) { - var pos = 0, i; - tagName = lowercase(tagName); - if ( tagName ) - // Find the closest opened tag of the same type - for ( pos = stack.length - 1; pos >= 0; pos-- ) - if ( stack[ pos ] == tagName ) - break; + /** + * @ngdoc method + * @name angular.module.ng.$browser#onUrlChange + * @methodOf angular.module.ng.$browser + * @TODO(vojta): refactor to use node's syntax for events + * + * @description + * Register callback function that will be called, when url changes. + * + * It's only called when the url is changed by outside of angular: + * - user types different url into address bar + * - user clicks on history (forward/back) button + * - user clicks on a link + * + * It's not called when url is changed by $browser.url() method + * + * The listener gets called with new url as parameter. + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link angular.module.ng.$location $location service} to monitor url changes in angular apps. + * + * @param {function(string)} listener Listener function to be called when url changes. + * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. + */ + self.onUrlChange = function(callback) { + if (!urlChangeInit) { + // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) + // don't fire popstate when user change the address bar and don't fire hashchange when url + // changed by push/replaceState - if ( pos >= 0 ) { - // Close all the open elements, up the stack - for ( i = stack.length - 1; i >= pos; i-- ) - if (handler.end) handler.end( stack[ i ] ); + // html5 history api - popstate event + if ($sniffer.history) jqLite(window).bind('popstate', fireUrlChange); + // hashchange event + if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); + // polling + else self.addPollFn(fireUrlChange); - // Remove the open elements from the stack - stack.length = pos; + urlChangeInit = true; } - } -} -/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -var hiddenPre=document.createElement("pre"); -function decodeEntities(value) { - hiddenPre.innerHTML=value.replace(//g, '>'); -} + ////////////////////////////////////////////////////////////// + // Cookies API + ////////////////////////////////////////////////////////////// + var lastCookies = {}; + var lastCookieString = ''; -/** - * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.jain('') to get out sanitized html string - * @returns {object} in the form of { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * } - */ -function htmlSanitizeWriter(buf){ - var ignore = false; - var out = bind(buf, buf.push); - return { - start: function(tag, attrs, unary){ - tag = lowercase(tag); - if (!ignore && specialElements[tag]) { - ignore = tag; - } - if (!ignore && validElements[tag] == true) { - out('<'); - out(tag); - forEach(attrs, function(value, key){ - var lkey=lowercase(key); - if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { - out(' '); - out(key); - out('="'); - out(encodeEntities(value)); - out('"'); + /** + * @ngdoc method + * @name angular.module.ng.$browser#cookies + * @methodOf angular.module.ng.$browser + * + * @param {string=} name Cookie name + * @param {string=} value Cokkie value + * + * @description + * The cookies method provides a 'private' low level access to browser cookies. + * It is not meant to be used directly, use the $cookie service instead. + * + * The return values vary depending on the arguments that the method was called with as follows: + *
    + *
  • cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
  • + *
  • cookies(name, value) -> set name to value, if value is undefined delete the cookie
  • + *
  • cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
  • + *
+ * + * @returns {Object} Hash of all cookies (if called without any parameter) + */ + self.cookies = function(name, value) { + var cookieLength, cookieArray, cookie, i, keyValue, index; + + if (name) { + if (value === undefined) { + rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } else { + if (isString(value)) { + rawDocument.cookie = escape(name) + '=' + escape(value); + + cookieLength = name.length + value.length + 1; + if (cookieLength > 4096) { + $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ + cookieLength + " > 4096 bytes)!"); + } + if (lastCookies.length > 20) { + $log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " + + "were already set (" + lastCookies.length + " > 20 )"); } - }); - out(unary ? '/>' : '>'); - } - }, - end: function(tag){ - tag = lowercase(tag); - if (!ignore && validElements[tag] == true) { - out(''); - } - if (tag == ignore) { - ignore = false; } - }, - chars: function(chars){ - if (!ignore) { - out(encodeEntities(chars)); + } + } else { + if (rawDocument.cookie !== lastCookieString) { + lastCookieString = rawDocument.cookie; + cookieArray = lastCookieString.split("; "); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + cookie = cookieArray[i]; + index = cookie.indexOf('='); + if (index > 0) { //ignore nameless cookies + lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1)); + } } } + return lastCookies; + } }; -} -////////////////////////////////// -//JQLite -////////////////////////////////// -/** - * @ngdoc function - * @name angular.element - * @function - * - * @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. - * - * 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. - * - * Note: All element references in Angular are always wrapped with jQuery or jqLite; they are never - * raw DOM references. + /** + * @ngdoc method + * @name angular.module.ng.$browser#defer + * @methodOf angular.module.ng.$browser + * @param {function()} fn A function, who's execution should be defered. + * @param {number=} [delay=0] of milliseconds to defer the function execution. + * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. + * + * @description + * Executes a fn asynchroniously via `setTimeout(fn, delay)`. + * + * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using + * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed + * via `$browser.defer.flush()`. + * + */ + self.defer = function(fn, delay) { + var timeoutId; + outstandingRequestCount++; + timeoutId = setTimeout(function() { + delete pendingDeferIds[timeoutId]; + completeOutstandingRequest(fn); + }, delay || 0); + pendingDeferIds[timeoutId] = true; + return timeoutId; + }; + + + /** + * THIS DOC IS NOT VISIBLE because ngdocs can't process docs for foo#method.method + * + * @name angular.module.ng.$browser#defer.cancel + * @methodOf angular.module.ng.$browser.defer + * + * @description + * Cancels a defered task identified with `deferId`. + * + * @param {*} deferId Token returned by the `$browser.defer` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. + */ + self.defer.cancel = function(deferId) { + if (pendingDeferIds[deferId]) { + delete pendingDeferIds[deferId]; + clearTimeout(deferId); + completeOutstandingRequest(noop); + return true; + } + return false; + }; + + + ////////////////////////////////////////////////////////////// + // Misc API + ////////////////////////////////////////////////////////////// + + /** + * @ngdoc method + * @name angular.module.ng.$browser#addCss + * @methodOf angular.module.ng.$browser + * + * @param {string} url Url to css file + * @description + * Adds a stylesheet tag to the head. + */ + self.addCss = function(url) { + var link = jqLite(rawDocument.createElement('link')); + link.attr('rel', 'stylesheet'); + link.attr('type', 'text/css'); + link.attr('href', url); + body.append(link); + }; + + + /** + * @ngdoc method + * @name angular.module.ng.$browser#addJs + * @methodOf angular.module.ng.$browser + * + * @param {string} url Url to js file + * + * @description + * Adds a script tag to the head. + */ + self.addJs = function(url, done) { + // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: + // - fetches local scripts via XHR and evals them + // - adds and immediately removes script elements from the document + var script = rawDocument.createElement('script'); + + script.type = 'text/javascript'; + script.src = url; + + if (msie) { + script.onreadystatechange = function() { + /loaded|complete/.test(script.readyState) && done && done(); + }; + } else { + if (done) script.onload = script.onerror = done; + } + + body[0].appendChild(script); + + return script; + }; + + /** + * Returns current + * (always relative - without domain) + * + * @returns {string=} + */ + self.baseHref = function() { + var href = document.find('base').attr('href'); + return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : href; + }; +} + +function $BrowserProvider(){ + this.$get = ['$window', '$log', '$sniffer', '$document', + function( $window, $log, $sniffer, $document){ + return new Browser($window, $document, $document.find('body'), $log, $sniffer); + }]; +} +/** + * @ngdoc object + * @name angular.module.ng.$cacheFactory * - * ## Angular's jQuery lite provides the following methods: + * @description + * Factory that constructs cache objects. * - * - [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/bind/) - * - [children()](http://api.jquery.com/children/) - * - [clone()](http://api.jquery.com/clone/) - * - [css()](http://api.jquery.com/css/) - * - [data()](http://api.jquery.com/data/) - * - [hasClass()](http://api.jquery.com/hasClass/) - * - [parent()](http://api.jquery.com/parent/) - * - [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/) - * - [trigger()](http://api.jquery.com/trigger/) - * - [eq()](http://api.jquery.com/eq/) * - * ## In addtion to the above, Angular privides an additional method to both jQuery and jQuery lite: + * @param {string} cacheId Name or id of the newly created cache. + * @param {object=} options Options object that specifies the cache behavior. Properties: * - * - `scope()` - retrieves the current Angular scope of the element. + * - `{number=}` `capacity` — turns the cache into LRU cache. + * + * @returns {object} Newly created cache object with the following set of methods: + * + * - `{string}` `id()` — Returns id or name of the cache. + * - `{number}` `size()` — Returns number of items currently in the cache + * - `{void}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache + * - `{(*}} `get({string} key) — Returns cached value for `key` or undefined for cache miss. + * - `{void}` `remove{string} key) — Removes a key-value pair from the cache. + * - `{void}` `removeAll() — Removes all cached values. * - * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. - * @returns {Object} jQuery object. */ +function $CacheFactoryProvider() { -var jqCache = {}, - jqName = 'ng-' + new Date().getTime(), - jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + this.$get = function() { + var caches = {}; -function jqNextId() { return (jqId++); } + function cacheFactory(cacheId, options) { + if (cacheId in caches) { + throw Error('cacheId ' + cacheId + ' taken'); + } + var size = 0, + stats = extend({}, options, {id: cacheId}), + data = {}, + capacity = (options && options.capacity) || Number.MAX_VALUE, + lruHash = {}, + freshEnd = null, + staleEnd = null; -function getStyle(element) { - var current = {}, style = element[0].style, value, name, i; - if (typeof style.length == 'number') { - for(i = 0; i < style.length; i++) { - name = style[i]; - current[name] = style[name]; - } - } else { - for (name in style) { - value = style[name]; - if (1*name != name && name != 'cssText' && value && typeof value == 'string' && value !='false') - current[name] = value; - } - } - return current; -} + return caches[cacheId] = { -///////////////////////////////////////////// -function jqLiteWrap(element) { - if (isString(element) && element.charAt(0) != '<') { - throw new Error('selectors not implemented'); - } - return new JQLite(element); -} + put: function(key, value) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); -function JQLite(element) { - if (element instanceof JQLite) { - return element; - } else if (isString(element)) { - var div = document.createElement('div'); - // Read about the NoScope elements here: - // http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx - div.innerHTML = '
 
' + element; // IE insanity to make NoScope elements work! - div.removeChild(div.firstChild); // remove the superfluous div - JQLiteAddNodes(this, div.childNodes); - this.remove(); // detach the elements from the temporary DOM div. - } else { - JQLiteAddNodes(this, element); - } -} + refresh(lruEntry); -function JQLiteClone(element) { - return element.cloneNode(true); -} + if (isUndefined(value)) return; + if (!(key in data)) size++; + data[key] = value; -function JQLiteDealoc(element){ - JQLiteRemoveData(element); - for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { - JQLiteDealoc(children[i]); - } -} + if (size > capacity) { + this.remove(staleEnd.key); + } + }, -function JQLiteRemoveData(element) { - var cacheId = element[jqName], - cache = jqCache[cacheId]; - if (cache) { - forEach(cache.bind || {}, function(fn, type){ - removeEventListenerFn(element, type, fn); - }); - delete jqCache[cacheId]; - element[jqName] = undefined; // ie does not allow deletion of attributes on elements. - } -} -function JQLiteData(element, key, value) { - var cacheId = element[jqName], - cache = jqCache[cacheId || -1]; - if (isDefined(value)) { - if (!cache) { - element[jqName] = cacheId = jqNextId(); - cache = jqCache[cacheId] = {}; - } - cache[key] = value; - } else { - return cache ? cache[key] : null; - } -} + get: function(key) { + var lruEntry = lruHash[key]; -function JQLiteHasClass(element, selector, _) { - // the argument '_' is important, since it makes the function have 3 arguments, which - // is needed for delegate function to realize the this is a getter. - var className = " " + selector + " "; - return ((" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1); -} + if (!lruEntry) return; -function JQLiteRemoveClass(element, selector) { - if (selector) { - forEach(selector.split(' '), function(cssClass) { - element.className = trim( - (" " + element.className + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ") - ); - }); - } -} + refresh(lruEntry); -function JQLiteAddClass(element, selector) { - if (selector) { - forEach(selector.split(' '), function(cssClass) { - if (!JQLiteHasClass(element, cssClass)) { - element.className = trim(element.className + ' ' + trim(cssClass)); - } - }); - } -} + return data[key]; + }, -function JQLiteAddNodes(root, elements) { - if (elements) { - elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) - ? elements - : [ elements ]; - for(var i=0; i < elements.length; i++) { - root.push(elements[i]); - } - } -} -////////////////////////////////////////// -// Functions which are declared directly. -////////////////////////////////////////// -var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; + remove: function(key) { + var lruEntry = lruHash[key]; - function trigger() { - if (fired) return; - fired = true; - fn(); - } + if (lruEntry == freshEnd) freshEnd = lruEntry.p; + if (lruEntry == staleEnd) staleEnd = lruEntry.n; + link(lruEntry.n,lruEntry.p); - this.bind('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - jqLiteWrap(window).bind('load', trigger); // fallback to window.onload for others - }, - toString: function() { - var value = []; - forEach(this, function(e){ value.push('' + e);}); - return '[' + value.join(', ') + ']'; - }, - - eq: function(index) { - return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); - }, + delete lruHash[key]; + delete data[key]; + size--; + }, - length: 0, - push: push, - sort: [].sort, - splice: [].splice -}; -////////////////////////////////////////// -// Functions iterating getter/setters. -// these functions return self on setter and -// value on get. -////////////////////////////////////////// -var SPECIAL_ATTR = makeMap("multiple,selected,checked,disabled,readonly"); + removeAll: function() { + data = {}; + size = 0; + lruHash = {}; + freshEnd = staleEnd = null; + }, -forEach({ - data: JQLiteData, - scope: function(element) { - var scope; - while (element && !(scope = jqLite(element).data($$scope))) { - element = element.parentNode; - } - return scope; - }, + destroy: function() { + data = null; + stats = null; + lruHash = null; + delete caches[cacheId]; + }, - removeAttr: function(element,name) { - element.removeAttribute(name); - }, - hasClass: JQLiteHasClass, + info: function() { + return extend({}, stats, {size: size}); + } + }; - css: function(element, name, value) { - if (isDefined(value)) { - element.style[name] = value; - } else { - return element.style[name]; - } - }, - attr: function(element, name, value){ - if (name === 'class') { - if(isDefined(value)) { - element.className = value; - } else { - return element.className; - } - } else if (SPECIAL_ATTR[name]) { - if (isDefined(value)) { - element[name] = !!value; - } else { - return element[name]; - } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) - return ret === null ? undefined : ret; - } - }, + /** + * makes the `entry` the freshEnd of the LRU linked list + */ + function refresh(entry) { + if (entry != freshEnd) { + if (!staleEnd) { + staleEnd = entry; + } else if (staleEnd == entry) { + staleEnd = entry.n; + } - text: extend((msie < 9) - ? function(element, value) { - // NodeType == 3 is text node - if (element.nodeType == 3) { - if (isUndefined(value)) - return element.nodeValue; - element.nodeValue = value; - } else { - if (isUndefined(value)) - return element.innerText; - element.innerText = value; + link(entry.n, entry.p); + link(entry, freshEnd); + freshEnd = entry; + freshEnd.n = null; } } - : function(element, value) { - if (isUndefined(value)) { - return element.textContent; - } - element.textContent = value; - }, {$dv:''}), - - val: function(element, value) { - if (isUndefined(value)) { - return element.value; - } - element.value = value; - }, - html: function(element, value) { - if (isUndefined(value)) { - return element.innerHTML; - } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - JQLiteDealoc(childNodes[i]); - } - element.innerHTML = value; - } -}, function(fn, name){ - /** - * Properties: writes return selection, reads return first value - */ - JQLite.prototype[name] = function(arg1, arg2) { - var i, key; - if ((fn.length == 2 ? arg1 : arg2) === undefined) { - if (isObject(arg1)) { - // we are a write, but the object properties are the key/values - for(i=0; i < this.length; i++) { - for (key in arg1) { - fn(this[i], key, arg1[key]); - } + /** + * bidirectionally links two entries of the LRU linked list + */ + function link(nextEntry, prevEntry) { + if (nextEntry != prevEntry) { + if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify + if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify } - // return self for chaining - return this; - } else { - // we are a read, so read the first child. - if (this.length) - return fn(this[0], arg1, arg2); - } - } else { - // we are a write, so apply to all children - for(i=0; i < this.length; i++) { - fn(this[i], arg1, arg2); } - // return self for chaining - return this; } - return fn.$dv; - }; -}); -////////////////////////////////////////// -// Functions iterating traversal. -// These functions chain results into a single -// selector. -////////////////////////////////////////// -forEach({ - removeData: JQLiteRemoveData, - dealoc: JQLiteDealoc, + cacheFactory.info = function() { + var info = {}; + forEach(caches, function(cache, cacheId) { + info[cacheId] = cache.info(); + }); + return info; + }; - bind: function(element, type, fn){ - var bind = JQLiteData(element, 'bind'); - if (!bind) JQLiteData(element, 'bind', bind = {}); - forEach(type.split(' '), function(type){ - var eventHandler = bind[type]; - if (!eventHandler) { - bind[type] = eventHandler = function(event) { - if (!event.preventDefault) { - event.preventDefault = function(){ - event.returnValue = false; //ie - }; - } - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } - if (!event.target) { - event.target = event.srcElement || document; - } - forEach(eventHandler.fns, function(fn){ - fn.call(element, event); - }); - }; - eventHandler.fns = []; - addEventListenerFn(element, type, eventHandler); - } - eventHandler.fns.push(fn); - }); - }, - replaceWith: function(element, replaceNode) { - var index, parent = element.parentNode; - JQLiteDealoc(element); - forEach(new JQLite(replaceNode), function(node){ - if (index) { - parent.insertBefore(node, index.nextSibling); - } else { - parent.replaceChild(node, element); - } - index = node; - }); - }, + cacheFactory.get = function(cacheId) { + return caches[cacheId]; + }; - children: function(element) { - var children = []; - forEach(element.childNodes, function(element){ - if (element.nodeName != '#text') - children.push(element); - }); - return children; - }, - append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1) - element.appendChild(child); - }); - }, + return cacheFactory; + }; +} - prepend: function(element, node) { - if (element.nodeType === 1) { - var index = element.firstChild; - forEach(new JQLite(node), function(child){ - if (index) { - element.insertBefore(child, index); - } else { - element.appendChild(child); - index = child; - } - }); - } - }, +function $TemplateCacheProvider() { + this.$get = ['$cacheFactory', function($cacheFactory) { + return $cacheFactory('templates'); + }]; +} - remove: function(element) { - JQLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); - }, +function $CompileProvider(){ + this.$get = ['$injector', '$exceptionHandler', '$textMarkup', '$attrMarkup', '$directive', '$widget', + function( $injector, $exceptionHandler, $textMarkup, $attrMarkup, $directive, $widget){ + /** + * Template provides directions an how to bind to a given element. + * It contains a list of init functions which need to be called to + * bind to a new instance of elements. It also provides a list + * of child paths which contain child templates + */ + function Template() { + this.paths = []; + this.children = []; + this.linkFns = []; + this.newScope = false; + } - after: function(element, newElement) { - var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ - parent.insertBefore(node, index.nextSibling); - index = node; - }); - }, + Template.prototype = { + link: function(element, scope) { + var childScope = scope, + locals = {$element: element}; + if (this.newScope) { + childScope = isFunction(this.newScope) ? scope.$new(this.newScope(scope)) : scope.$new(); + element.data($$scope, childScope); + } + forEach(this.linkFns, function(fn) { + try { + if (isArray(fn) || fn.$inject) { + $injector.invoke(fn, childScope, locals); + } else { + fn.call(childScope, element); + } + } catch (e) { + $exceptionHandler(e); + } + }); + var i, + childNodes = element[0].childNodes, + children = this.children, + paths = this.paths, + length = paths.length; + for (i = 0; i < length; i++) { + // sometimes `element` can be modified by one of the linker functions in `this.linkFns` + // and childNodes may be added or removed + // TODO: element structure needs to be re-evaluated if new children added + // if the childNode still exists + if (childNodes[paths[i]]) + children[i].link(jqLite(childNodes[paths[i]]), childScope); + else + delete paths[i]; // if child no longer available, delete path + } + }, - addClass: JQLiteAddClass, - removeClass: JQLiteRemoveClass, - toggleClass: function(element, selector, condition) { - if (isUndefined(condition)) { - condition = !JQLiteHasClass(element, selector); - } - (condition ? JQLiteAddClass : JQLiteRemoveClass)(element, selector); - }, + addLinkFn:function(linkingFn) { + if (linkingFn) { + this.linkFns.push(linkingFn); + } + }, - parent: function(element) { - var parent = element.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - next: function(element) { - return element.nextSibling; - }, + addChild: function(index, template) { + if (template) { + this.paths.push(index); + this.children.push(template); + } + }, - find: function(element, selector) { - return element.getElementsByTagName(selector); - }, + empty: function() { + return this.linkFns.length === 0 && this.paths.length === 0; + } + }; - clone: JQLiteClone -}, function(fn, name){ - /** - * chaining functions - */ - JQLite.prototype[name] = function(arg1, arg2) { - var value; - for(var i=0; i < this.length; i++) { - if (value == undefined) { - value = fn(this[i], arg1, arg2); - if (value !== undefined) { - // any function which returns a value needs to be wrapped - value = jqLite(value); - } - } else { - JQLiteAddNodes(value, fn(this[i], arg1, arg2)); + /////////////////////////////////// + //Compiler + ////////////////////////////////// + + /** + * @ngdoc function + * @name angular.module.ng.$compile + * @function + * + * @description + * Compiles a piece of HTML string or DOM into a template and produces a template function, which + * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together. + * + * The compilation is a process of walking the DOM tree and trying to match DOM elements to + * {@link angular.markup markup}, {@link angular.attrMarkup attrMarkup}, + * {@link angular.widget widgets}, and {@link angular.directive directives}. For each match it + * executes corresponding markup, attrMarkup, widget or directive template function and collects the + * instance functions into a single template function which is then returned. + * + * The template function can then be used once to produce the view or as it is the case with + * {@link angular.widget.@ng:repeat repeater} many-times, in which case each call results in a view + * that is a DOM clone of the original template. + * +
+          angular.injector(['ng']).invoke(function($rootScope, $compile) {
+            // Chose one:
+
+            // A: compile the entire window.document.
+            var element = $compile(window.document)($rootScope);
+
+            // B: compile a piece of html
+            var element = $compile('
click me
')($rootScope); + + // C: compile a piece of html and retain reference to both the dom and scope + var element = $compile('
click me
')(scope); + // at this point template was transformed into a view + }); +
+ * + * + * @param {string|DOMElement} element Element or HTML to compile into a template function. + * @returns {function(scope[, cloneAttachFn])} a template function which is used to bind template + * (a DOM element/tree) to a scope. Where: + * + * * `scope` - A {@link angular.module.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:
`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. + * + * Calling the template function returns the element of the template. It is either the original element + * passed in, or the clone of the element if the `cloneAttachFn` is provided. + * + * It is important to understand that the returned scope is "linked" to the view DOM, but no linking + * (instance) functions registered by {@link angular.directive directives} or + * {@link angular.widget widgets} found in the template have been executed yet. This means that the + * view is likely empty and doesn't contain any values that result from evaluation on the scope. To + * bring the view to life, the scope needs to run through a $digest phase which typically is done by + * Angular automatically, except for the case when an application is being + * {@link guide/dev_guide.bootstrap.manual_bootstrap} manually bootstrapped, in which case the + * $digest phase must be invoked by calling {@link angular.module.ng.$rootScope.Scope#$apply}. + * + * If you need access to the bound view, there are two ways to do it: + * + * - If you are not asking the linking function to clone the template, create the DOM element(s) + * before you send them to the compiler and keep this reference around. + *
+       *     var $injector = angular.injector(['ng']);
+       *     var scope = $injector.invoke(function($rootScope, $compile){
+       *       var element = $compile('

{{total}}

')($rootScope); + * }); + *
+ * + * - if on the other hand, you need the element to be cloned, the view reference from the original + * example would not point to the clone, but rather to the original template that was cloned. In + * this case, you can access the clone via the cloneAttachFn: + *
+       *     var original = angular.element('

{{total}}

'), + * scope = someParentScope.$new(), + * clone; + * + * $compile(original)(scope, function(clonedElement, scope) { + * clone = clonedElement; + * //attach the clone to DOM document at the right place + * }); + * + * //now we have reference to the cloned DOM via `clone` + *
+ * + * + * Compiler Methods For Widgets and Directives: + * + * The following methods are available for use when you write your own widgets, directives, + * and markup. (Recall that the compile function's this is a reference to the compiler.) + * + * `compile(element)` - returns linker - + * Invoke a new instance of the compiler to compile a DOM element and return a linker function. + * You can apply the linker function to the original element or a clone of the original element. + * The linker function returns a scope. + * + * * `comment(commentText)` - returns element - Create a comment element. + * + * * `element(elementName)` - returns element - Create an element by name. + * + * * `text(text)` - returns element - Create a text element. + * + * * `descend([set])` - returns descend state (true or false). Get or set the current descend + * state. If true the compiler will descend to children elements. + * + * * `directives([set])` - returns directive state (true or false). Get or set the current + * directives processing state. The compiler will process directives only when directives set to + * true. + * + * For information on how the compiler works, see the + * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide. + */ + function Compiler(markup, attrMarkup, directives, widgets){ + this.markup = markup; + this.attrMarkup = attrMarkup; + this.directives = directives; + this.widgets = widgets; } - } - return value == undefined ? this : value; - }; -}); -var angularGlobal = { - 'typeOf':function(obj){ - if (obj === null) return $null; - var type = typeof obj; - if (type == $object) { - if (obj instanceof Array) return $array; - if (isDate(obj)) return $date; - if (obj.nodeType == 1) return 'element'; + Compiler.prototype = { + compile: function(templateElement) { + templateElement = jqLite(templateElement); + var index = 0, + template, + parent = templateElement.parent(); + if (templateElement.length > 1) { + // https://github.com/angular/angular.js/issues/338 + throw Error("Cannot compile multiple element roots: " + + jqLite('
').append(templateElement.clone()).html()); + } + if (parent && parent[0]) { + parent = parent[0]; + for(var i = 0; i < parent.childNodes.length; i++) { + if (parent.childNodes[i] == templateElement[0]) { + index = i; + } + } + } + template = this.templatize(templateElement, index) || new Template(); + return function(scope, cloneConnectFn){ + assertArg(scope, 'scope'); + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + var element = cloneConnectFn + ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!! + : templateElement; + element.data($$scope, scope); + scope.$element = element; + (cloneConnectFn||noop)(element, scope); + template.link(element, scope); + return element; + }; + }, + + templatize: function(element, elementIndex){ + var self = this, + widget, + fn, + directiveFns = self.directives, + descend = true, + directives = true, + elementName = nodeName_(element), + elementNamespace = elementName.indexOf(':') > 0 ? lowercase(elementName).replace(':', '-') : '', + template, + locals = {$element: element}, + selfApi = { + compile: bind(self, self.compile), + descend: function(value){ if(isDefined(value)) descend = value; return descend;}, + directives: function(value){ if(isDefined(value)) directives = value; return directives;}, + scope: function(value){ if(isDefined(value)) template.newScope = template.newScope || value; return template.newScope;} + }; + element.addClass(elementNamespace); + template = new Template(); + eachAttribute(element, function(value, name){ + if (!widget) { + if ((widget = self.widgets('@' + name))) { + element.addClass('ng-attr-widget'); + if (isFunction(widget) && !widget.$inject) { + widget.$inject = ['$value', '$element']; + } + locals.$value = value; + } + } + }); + if (!widget) { + if ((widget = self.widgets(elementName))) { + if (elementNamespace) + element.addClass('ng-widget'); + if (isFunction(widget) && !widget.$inject) { + widget.$inject = ['$element']; + } + } + } + if (widget) { + descend = false; + directives = false; + var parent = element.parent(); + template.addLinkFn($injector.invoke(widget, selfApi, locals)); + if (parent && parent[0]) { + element = jqLite(parent[0].childNodes[elementIndex]); + } + } + if (descend){ + // process markup for text nodes only + for(var i=0, child=element[0].childNodes; + i - -
-
- Index of '{{bookName}}' in the list {{books}} is {{books.$indexOf(bookName)}}. -
- - it('should correctly calculate the initial index', function() { - expect(binding('books.$indexOf(bookName)')).toBe('2'); - }); + //at the end of each eval, push cookies + //TODO: this should happen before the "delayed" watches fire, because if some cookies are not + // strings or browser refuses to store some cookies, we update the model in the push fn. + $rootScope.$watch(push); - it('should recalculate', function() { - input('bookName').enter('foo'); - expect(binding('books.$indexOf(bookName)')).toBe('-1'); + return cookies; - input('bookName').enter('Moby Dick'); - expect(binding('books.$indexOf(bookName)')).toBe('0'); - }); - - - */ - 'indexOf': indexOf, + /** + * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. + */ + function push() { + var name, + value, + browserCookies, + updated; + + //delete any cookies deleted in $cookies + for (name in lastCookies) { + if (isUndefined(cookies[name])) { + $browser.cookies(name, undefined); + } + } - /** - * @ngdoc function - * @name angular.Array.sum - * @function - * - * @description - * The `sum` function calculates the sum of all numbers in an `array`. If an `expression` is - * supplied, `sum` evaluates each element in the `array` with the expression and then returns - * the sum of the calculated values. - * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link angular.Array} for more info about Angular arrays. - * - * @param {Array} array The source array. - * @param {(string|function())=} expression Angular expression or a function to be evaluated for - * each element in `array`. The array element becomes the `this` during the evaluation. - * @returns {number} Sum of items in the array. - * - * @example - - - - - - - - - - - - - - - - - -
QtyDescriptionCostTotal
{{item.qty * item.cost | currency}}[X]
add itemTotal:{{invoice.items.$sum('qty*cost') | currency}}
-
- - //TODO: these specs are lame because I had to work around issues #164 and #167 - it('should initialize and calculate the totals', function() { - expect(repeater('.doc-example-live table tr', 'item in invoice.items').count()).toBe(3); - expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(1)). - toEqual(['$99.50']); - expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50'); - expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50'); - }); + //update all cookies updated in $cookies + for(name in cookies) { + value = cookies[name]; + if (!isString(value)) { + if (isDefined(lastCookies[name])) { + cookies[name] = lastCookies[name]; + } else { + delete cookies[name]; + } + } else if (value !== lastCookies[name]) { + $browser.cookies(name, value); + updated = true; + } + } - it('should add an entry and recalculate', function() { - element('.doc-example-live a:contains("add item")').click(); - using('.doc-example-live tr:nth-child(3)').input('item.qty').enter('20'); - using('.doc-example-live tr:nth-child(3)').input('item.cost').enter('100'); + //verify what was actually stored + if (updated){ + updated = false; + browserCookies = $browser.cookies(); - expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(2)). - toEqual(['$2,000.00']); - expect(binding("invoice.items.$sum('qty*cost')")).toBe('$2,099.50'); - }); - -
- */ - 'sum':function(array, expression) { - var fn = angularFunction.compile(expression); - var sum = 0; - for (var i = 0; i < array.length; i++) { - var value = 1 * fn(array[i]); - if (!isNaN(value)){ - sum += value; + for (name in cookies) { + if (cookies[name] !== browserCookies[name]) { + //delete or reset all cookies that the browser dropped from $cookies + if (isUndefined(browserCookies[name])) { + delete cookies[name]; + } else { + cookies[name] = browserCookies[name]; + } + updated = true; + } + } } } - return sum; - }, + }]; +} +/** + * @ngdoc function + * @name angular.module.ng.$defer + * @requires $browser + * + * @description + * Delegates to {@link angular.module.ng.$browser#defer $browser.defer}, but wraps the `fn` function + * into a try/catch block and delegates any exceptions to + * {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. + * + * In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions. + * + * @param {function()} fn A function, who's execution should be deferred. + * @param {number=} [delay=0] of milliseconds to defer the function execution. + * @returns {*} DeferId that can be used to cancel the task via `$defer.cancel()`. + */ - /** - * @ngdoc function - * @name angular.Array.remove - * @function - * - * @description - * Modifies `array` by removing an element from it. The element will be looked up using the - * {@link angular.Array.indexOf indexOf} function on the `array` and only the first instance of - * the element will be removed. - * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link angular.Array} for more information about Angular arrays. - * - * @param {Array} array Array from which an element should be removed. - * @param {*} value Element to be removed. - * @returns {*} The removed element. - * - * @example - - -
    -
  • - {{task}} [X] -
  • -
-
- tasks = {{tasks}} -
- - it('should initialize the task list with for tasks', function() { - expect(repeater('.doc-example-live ul li', 'task in tasks').count()).toBe(4); - expect(repeater('.doc-example-live ul li', 'task in tasks').column('task')). - toEqual(['Learn Angular', 'Read Documentation', 'Check out demos', - 'Build cool applications']); - }); +/** + * @ngdoc function + * @name angular.module.ng.$defer#cancel + * @methodOf angular.module.ng.$defer + * + * @description + * Cancels a defered task identified with `deferId`. + * + * @param {*} deferId Token returned by the `$defer` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. + */ +function $DeferProvider(){ + this.$get = ['$rootScope', '$browser', function($rootScope, $browser) { + function defer(fn, delay) { + return $browser.defer(function() { + $rootScope.$apply(fn); + }, delay); + } - it('should initialize the task list with for tasks', function() { - element('.doc-example-live ul li a:contains("X"):first').click(); - expect(repeater('.doc-example-live ul li', 'task in tasks').count()).toBe(3); + defer.cancel = function(deferId) { + return $browser.defer.cancel(deferId); + }; - element('.doc-example-live ul li a:contains("X"):last').click(); - expect(repeater('.doc-example-live ul li', 'task in tasks').count()).toBe(2); + return defer; + }]; +} - expect(repeater('.doc-example-live ul li', 'task in tasks').column('task')). - toEqual(['Read Documentation', 'Check out demos']); - }); - -
- */ - 'remove':function(array, value) { - var index = indexOf(array, value); - if (index >=0) - array.splice(index, 1); - return value; - }, +/** + * @ngdoc object + * @name angular.module.ng.$document + * @requires $window + * + * @description + * A {@link angular.element jQuery (lite)}-wrapped reference to the browser's `window.document` + * element. + */ +function $DocumentProvider(){ + this.$get = ['$window', function(window){ + return jqLite(window.document); + }]; +} +/** + * @ngdoc function + * @name angular.module.ng.$exceptionHandler + * @requires $log + * + * @description + * 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 overriden by + * {@link angular.module.ngMock.$exceptionHandler mock $exceptionHandler} + */ +function $ExceptionHandlerProvider(){ + this.$get = ['$log', function($log){ + return function(e) { + $log.error(e); + }; + }]; +} - /** - * @ngdoc function - * @name angular.Array.filter - * @function - * - * @description - * Selects a subset of items from `array` and returns it as a new array. - * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link angular.Array} for more information about Angular arrays. - * - * @param {Array} array The source array. - * @param {string|Object|function()} expression The predicate to be used for selecting items from - * `array`. - * - * Can be one of: - * - * - `string`: Predicate that results in a substring match using the value of `expression` - * string. All strings or objects with string properties in `array` that contain this string - * will be returned. The predicate can be negated by prefixing the string with `!`. - * - * - `Object`: A pattern object can be used to filter specific properties on objects contained - * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items - * which have property `name` containing "M" and property `phone` containing "1". A special - * property name `$` can be used (as in `{$:"text"}`) to accept a match against any - * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. - * - * - `function`: A predicate function can be used to write arbitrary filters. The function is - * called for each element of `array`. The final result is an array of those elements that - * the predicate returned true for. - * - * @example - - -
- - Search: - - - - - - -
NamePhone
{{friend.name}}{{friend.phone}}
-
- Any:
- Name only
- Phone only
- - - - - - -
NamePhone
{{friend.name}}{{friend.phone}}
-
- - it('should search across all fields when filtering with a string', function() { - input('searchText').enter('m'); - expect(repeater('#searchTextResults tr', 'friend in friends').column('name')). - toEqual(['Mary', 'Mike', 'Adam']); - - input('searchText').enter('76'); - expect(repeater('#searchTextResults tr', 'friend in friends').column('name')). - toEqual(['John', 'Julie']); - }); +/** + * @ngdoc object + * @name angular.module.ng.$filterProvider + * @description + * + * Filters are just functions which transform input to an output. However filters need to be Dependency Injected. To + * achieve this a filter definition consists of a factory function which is annotated with dependencies and is + * responsible for creating a the filter function. + * + *
+ *   // Filter registration
+ *   function MyModule($provide, $filterProvider) {
+ *     // create a service to demonstrate injection (not always needed)
+ *     $provide.value('greet', function(name){
+ *       return 'Hello ' + name + '!':
+ *     });
+ *
+ *     // register a filter factory which uses the
+ *     // greet service to demonstrate DI.
+ *     $filterProvider.register('greet', function(greet){
+ *       // return the filter function which uses the greet service
+ *       // to generate salutation
+ *       return function(text) {
+ *         // filters need to be forgiving so check input validity
+ *         return text && greet(text) || text;
+ *       };
+ *     };
+ *   }
+ * 
+ * + * The filter function is registered with the `$injector` under the filter name suffixe with `Filter`. + *
+ *   it('should be the same instance', inject(
+ *     function($filterProvider) {
+ *       $filterProvider.register('reverse', function(){
+ *         return ...;
+ *       });
+ *     },
+ *     function($filter, reverseFilter) {
+ *       expect($filter('reverse')).toBe(reverseFilter);
+ *     });
+ * 
+ * + * + * For more information about how angular filters work, and how to create your own filters, see + * {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer + * Guide. + */ +/** + * @ngdoc method + * @name angular.module.ng.$filterProvider#register + * @methodOf angular.module.ng.$filterProvider + * @description + * Register filter factory function. + * + * @param {String} name Name of the filter. + * @param {function} fn The filter factory function which is injectable. + */ - it('should search in specific fields when filtering with a predicate object', function() { - input('search.$').enter('i'); - expect(repeater('#searchObjResults tr', 'friend in friends').column('name')). - toEqual(['Mary', 'Mike', 'Julie']); - }); -
-
- */ - 'filter':function(array, expression) { - var predicates = []; - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } + +/** + * @ngdoc function + * @name angular.module.ng.$filter + * @function + * @description + * Filters are used for formatting data displayed to the user. + * + * The general syntax in templates is as follows: + * + * {{ expression | [ filter_name ] }} + * + * @param {String} name Name of the filter function to retrieve + * @return {Function} the filter function + */ +$FilterProvider.$inject = ['$provide']; +function $FilterProvider($provide) { + var suffix = 'Filter'; + + function register(name, factory) { + return $provide.factory(name + suffix, factory); + } + this.register = register; + + this.$get = ['$injector', function($injector) { + return function(name) { + return $injector.get(name + suffix); + } + }]; + + //////////////////////////////////////// + + register('currency', currencyFilter); + register('date', dateFilter); + register('filter', filterFilter); + register('html', htmlFilter); + register('json', jsonFilter); + register('limitTo', limitToFilter); + register('linky', linkyFilter); + register('lowercase', lowercaseFilter); + register('number', numberFilter); + register('orderBy', orderByFilter); + register('uppercase', uppercaseFilter); +} + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.filter + * @function + * + * @description + * Selects a subset of items from `array` and returns it as a new array. + * + * Note: This function is used to augment the `Array` type in Angular expressions. See + * {@link angular.module.ng.$filter} for more information about Angular arrays. + * + * @param {Array} array The source array. + * @param {string|Object|function()} expression The predicate to be used for selecting items from + * `array`. + * + * Can be one of: + * + * - `string`: Predicate that results in a substring match using the value of `expression` + * string. All strings or objects with string properties in `array` that contain this string + * will be returned. The predicate can be negated by prefixing the string with `!`. + * + * - `Object`: A pattern object can be used to filter specific properties on objects contained + * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items + * which have property `name` containing "M" and property `phone` containing "1". A special + * property name `$` can be used (as in `{$:"text"}`) to accept a match against any + * property of the object. That's equivalent to the simple substring match with a `string` + * as described above. + * + * - `function`: A predicate function can be used to write arbitrary filters. The function is + * called for each element of `array`. The final result is an array of those elements that + * the predicate returned true for. + * + * @example + + +
+ + Search: + + + + + + +
NamePhone
{{friend.name}}{{friend.phone}}
+
+ Any:
+ Name only
+ Phone only
+ + + + + + +
NamePhone
{{friend.name}}{{friend.phone}}
+
+ + it('should search across all fields when filtering with a string', function() { + input('searchText').enter('m'); + expect(repeater('#searchTextResults tr', 'friend in friends').column('name')). + toEqual(['Mary', 'Mike', 'Adam']); + + input('searchText').enter('76'); + expect(repeater('#searchTextResults tr', 'friend in friends').column('name')). + toEqual(['John', 'Julie']); + }); + + it('should search in specific fields when filtering with a predicate object', function() { + input('search.$').enter('i'); + expect(repeater('#searchObjResults tr', 'friend in friends').column('name')). + toEqual(['Mary', 'Mike', 'Julie']); + }); + +
+ */ +function filterFilter() { + return function(array, expression) { + if (!(array instanceof Array)) return array; + var predicates = []; + predicates.check = function(value) { + for (var j = 0; j < predicates.length; j++) { + if(!predicates[j](value)) { + return false; + } + } return true; }; var search = function(obj, text){ @@ -4846,26 +4488,26 @@ var angularArray = { return !search(obj, text.substr(1)); } switch (typeof obj) { - case "boolean": - case "number": - case "string": - return ('' + obj).toLowerCase().indexOf(text) > -1; - case "object": - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; + case "boolean": + case "number": + case "string": + return ('' + obj).toLowerCase().indexOf(text) > -1; + case "object": + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } } - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; + return false; + case "array": + for ( var i = 0; i < obj.length; i++) { + if (search(obj[i], text)) { + return true; + } } - } - return false; - default: - return false; + return false; + default: + return false; } }; switch (typeof expression) { @@ -4876,7 +4518,7 @@ var angularArray = { case "object": for (var key in expression) { if (key == '$') { - (function(){ + (function() { var text = (''+expression[key]).toLowerCase(); if (!text) return; predicates.push(function(value) { @@ -4884,7 +4526,7 @@ var angularArray = { }); })(); } else { - (function(){ + (function() { var path = key; var text = (''+expression[key]).toLowerCase(); if (!text) return; @@ -4909,3577 +4551,5256 @@ var angularArray = { } } return filtered; - }, - + } +} - /** - * @ngdoc function - * @name angular.Array.add - * @function - * - * @description - * The `add` function in Angualar is similar to JavaScript's `Array#push` method in that it - * appends a new element to an array. Angular's function differs from the JavaScript method in - * that specifying a value for the function is optional and the default for the function is an - * empty object. - * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link angular.Array} for more information about Angular arrays. - * - * @param {Array} array The array to be expanded. - * @param {*=} [value={}] The value to be appended. - * @returns {Array} The expanded array. - * - * @TODO simplify the example. - * - * @example - * This example shows how you can use the `$add` method to populate an initially empty array - * with objects created from user input. - - - [add empty] - [add 'John'] - [add 'Mary'] - -
    -
  • - - - [X] -
  • -
-
people = {{people}}
-
- - beforeEach(function() { - expect(binding('people')).toBe('people = []'); - }); +/** + * @ngdoc filter + * @name angular.module.ng.$filter.currency + * @function + * + * @description + * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default + * symbol for current locale is used. + * + * @param {number} amount Input to filter. + * @param {string=} symbol Currency symbol or identifier to be displayed. + * @returns {string} Formatted number. + * + * + * @example + + + +
+
+ default currency symbol ($): {{amount | currency}}
+ custom currency identifier (USD$): {{amount | currency:"USD$"}} +
+
+ + it('should init with 1234.56', function() { + expect(binding('amount | currency')).toBe('$1,234.56'); + expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); + }); + it('should update', function() { + input('amount').enter('-1234'); + expect(binding('amount | currency')).toBe('($1,234.00)'); + expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); + }); + +
+ */ +currencyFilter.$inject = ['$locale']; +function currencyFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(amount, currencySymbol){ + if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; + return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). + replace(/\u00A4/g, currencySymbol); + }; +} - it('should create an empty record when "add empty" is clicked', function() { - element('.doc-example-live a:contains("add empty")').click(); - expect(binding('people')).toBe('people = [{\n "name":"",\n "sex":null}]'); - }); +/** + * @ngdoc filter + * @name angular.module.ng.$filter.number + * @function + * + * @description + * Formats a number as text. + * + * If the input is not a number an empty string is returned. + * + * @param {number|string} number Number to format. + * @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to. + * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. + * + * @example + + + +
+ Enter number:
+ Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}} +
+
+ + it('should format numbers', function() { + expect(binding('val | number')).toBe('1,234.568'); + expect(binding('val | number:0')).toBe('1,235'); + expect(binding('-val | number:4')).toBe('-1,234.5679'); + }); - it('should create a "John" record when "add \'John\'" is clicked', function() { - element('.doc-example-live a:contains("add \'John\'")').click(); - expect(binding('people')).toBe('people = [{\n "name":"John",\n "sex":"male"}]'); - }); + it('should update', function() { + input('val').enter('3374.333'); + expect(binding('val | number')).toBe('3,374.333'); + expect(binding('val | number:0')).toBe('3,374'); + expect(binding('-val | number:4')).toBe('-3,374.3330'); + }); + +
+ */ - it('should create a "Mary" record when "add \'Mary\'" is clicked', function() { - element('.doc-example-live a:contains("add \'Mary\'")').click(); - expect(binding('people')).toBe('people = [{\n "name":"Mary",\n "sex":"female"}]'); - }); - it('should delete a record when "X" is clicked', function() { - element('.doc-example-live a:contains("add empty")').click(); - element('.doc-example-live li a:contains("X"):first').click(); - expect(binding('people')).toBe('people = []'); - }); -
-
- */ - 'add':function(array, value) { - array.push(isUndefined(value)? {} : value); - return array; - }, +numberFilter.$inject = ['$locale']; +function numberFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(number, fractionSize) { + return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); + }; +} +var DECIMAL_SEP = '.'; +function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + if (isNaN(number) || !isFinite(number)) return ''; - /** - * @ngdoc function - * @name angular.Array.count - * @function - * - * @description - * Determines the number of elements in an array. Provides an option for counting only those - * elements for which a specified `condition` evaluates to `true`. - * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link angular.Array} for more information about Angular arrays. - * - * @param {Array} array The array containing the elements to be counted. - * @param {(function()|string)=} condition A function to be evaluated or - * an Angular expression to be compiled and evaluated. The element being - * iterated over is exposed to the `condition` as `this`. - * @returns {number} Number of elements in the array. If a `condition` is specified, returns - * the number of elements whose `condition` evaluates to `true`. - * - * @example - - -

-         
    -
  • - {{item.name}}: points= - -
  • -
-

Number of items which have one point: {{ items.$count('points==1') }}

-

Number of items which have more than one point: - {{items.$count('points>1')}}

-
- - it('should calculate counts', function() { - expect(binding('items.$count(\'points==1\')')).toEqual('2'); - expect(binding('items.$count(\'points>1\')')).toEqual('1'); - }); + var isNegative = number < 0; + number = Math.abs(number); + var numStr = number + '', + formatedText = '', + parts = []; - it('should recalculate when updated', function() { - using('.doc-example-live li:first-child').input('item.points').enter('23'); - expect(binding('items.$count(\'points==1\')')).toEqual('1'); - expect(binding('items.$count(\'points>1\')')).toEqual('2'); - }); - -
- */ - 'count':function(array, condition) { - if (!condition) return array.length; - var fn = angularFunction.compile(condition), count = 0; - forEach(array, function(value){ - if (fn(value)) { - count ++; - } - }); - return count; - }, + if (numStr.indexOf('e') !== -1) { + formatedText = numStr; + } else { + var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + // determine fractionSize if it is not specified + if (isUndefined(fractionSize)) { + fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); + } - /** - * @ngdoc function - * @name angular.Array.orderBy - * @function - * - * @description - * Orders a specified `array` by the `expression` predicate. - * - * Note: this function is used to augment the `Array` type in Angular expressions. See - * {@link angular.Array} for more informaton about Angular arrays. - * - * @param {Array} array The array to sort. - * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be - * used by the comparator to determine the order of elements. - * - * Can be one of: - * - * - `function`: Getter function. The result of this function will be sorted using the - * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). - * - `Array`: An array of function or string predicates. The first predicate in the array - * is used for sorting, but when two items are equivalent, the next predicate is used. - * - * @param {boolean=} reverse Reverse the order the array. - * @returns {Array} Sorted copy of the source array. - * - * @example - - -
+ var pow = Math.pow(10, fractionSize); + number = Math.round(number * pow) / pow; + var fraction = ('' + number).split(DECIMAL_SEP); + var whole = fraction[0]; + fraction = fraction[1] || ''; -
Sorting predicate = {{predicate}}; reverse = {{reverse}}
-
- [ unsorted ] - - - - - - - - - - - -
Name - (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
-
- - it('should be reverse ordered by aged', function() { - expect(binding('predicate')).toBe('Sorting predicate = -age; reverse = '); - expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')). - toEqual(['35', '29', '21', '19', '10']); - expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); - }); + var pos = 0, + lgroup = pattern.lgSize, + group = pattern.gSize; - it('should reorder the table when user selects different predicate', function() { - element('.doc-example-live a:contains("Name")').click(); - expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); - expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')). - toEqual(['35', '10', '29', '19', '21']); - - element('.doc-example-live a:contains("Phone")').click(); - expect(repeater('.doc-example-live table', 'friend in friends').column('friend.phone')). - toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); - expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); - }); - -
- */ - 'orderBy':function(array, sortPredicate, reverseOrder) { - if (!sortPredicate) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; - sortPredicate = map(sortPredicate, function(predicate){ - var descending = false, get = predicate || identity; - if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-'; - predicate = predicate.substring(1); + if (whole.length >= (lgroup + group)) { + pos = whole.length - lgroup; + for (var i = 0; i < pos; i++) { + if ((pos - i)%group === 0 && i !== 0) { + formatedText += groupSep; } - get = expressionCompile(predicate); + formatedText += whole.charAt(i); } - return reverseComparator(function(a,b){ - return compare(get(a),get(b)); - }, descending); - }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + } - function comparator(o1, o2){ + for (i = pos; i < whole.length; i++) { + if ((whole.length - i)%lgroup === 0 && i !== 0) { + formatedText += groupSep; + } + formatedText += whole.charAt(i); + } + + // format fraction part. + while(fraction.length < fractionSize) { + fraction += '0'; + } + + if (fractionSize) formatedText += decimalSep + fraction.substr(0, fractionSize); + } + + parts.push(isNegative ? pattern.negPre : pattern.posPre); + parts.push(formatedText); + parts.push(isNegative ? pattern.negSuf : pattern.posSuf); + return parts.join(''); +} + +function padNumber(num, digits, trim) { + var neg = ''; + if (num < 0) { + neg = '-'; + num = -num; + } + num = '' + num; + while(num.length < digits) num = '0' + num; + if (trim) + num = num.substr(num.length - digits); + return neg + num; +} + + +function dateGetter(name, size, offset, trim) { + return function(date) { + var value = date['get' + name](); + if (offset > 0 || value > -offset) + value += offset; + if (value === 0 && offset == -12 ) value = 12; + return padNumber(value, size, trim); + }; +} + +function dateStrGetter(name, shortForm) { + return function(date, formats) { + var value = date['get' + name](); + var get = uppercase(shortForm ? ('SHORT' + name) : name); + + return formats[get][value]; + }; +} + +function timeZoneGetter(date) { + var offset = date.getTimezoneOffset(); + return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2); +} + +function ampmGetter(date, formats) { + return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; +} + +var DATE_FORMATS = { + yyyy: dateGetter('FullYear', 4), + yy: dateGetter('FullYear', 2, 0, true), + y: dateGetter('FullYear', 1), + MMMM: dateStrGetter('Month'), + MMM: dateStrGetter('Month', true), + MM: dateGetter('Month', 2, 1), + M: dateGetter('Month', 1, 1), + dd: dateGetter('Date', 2), + d: dateGetter('Date', 1), + HH: dateGetter('Hours', 2), + H: dateGetter('Hours', 1), + hh: dateGetter('Hours', 2, -12), + h: dateGetter('Hours', 1, -12), + mm: dateGetter('Minutes', 2), + m: dateGetter('Minutes', 1), + ss: dateGetter('Seconds', 2), + s: dateGetter('Seconds', 1), + EEEE: dateStrGetter('Day'), + EEE: dateStrGetter('Day', true), + a: ampmGetter, + Z: timeZoneGetter +}; + +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, + NUMBER_STRING = /^\d+$/; + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.date + * @function + * + * @description + * Formats `date` to a string based on the requested `format`. + * + * `format` string can be composed of the following elements: + * + * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) + * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) + * * `'MMMM'`: Month in year (January-December) + * * `'MMM'`: Month in year (Jan-Dec) + * * `'MM'`: Month in year, padded (01-12) + * * `'M'`: Month in year (1-12) + * * `'dd'`: Day in month, padded (01-31) + * * `'d'`: Day in month (1-31) + * * `'EEEE'`: Day in Week,(Sunday-Saturday) + * * `'EEE'`: Day in Week, (Sun-Sat) + * * `'HH'`: Hour in day, padded (00-23) + * * `'H'`: Hour in day (0-23) + * * `'hh'`: Hour in am/pm, padded (01-12) + * * `'h'`: Hour in am/pm, (1-12) + * * `'mm'`: Minute in hour, padded (00-59) + * * `'m'`: Minute in hour (0-59) + * * `'ss'`: Second in minute, padded (00-59) + * * `'s'`: Second in minute (0-59) + * * `'a'`: am/pm marker + * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-1200) + * + * `format` string can also be one of the following predefined + * {@link guide/dev_guide.i18n localizable formats}: + * + * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 pm) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Friday, September 3, 2010) + * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010 + * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) + * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * + * `format` string can contain literal values. These need to be quoted with single quotes (e.g. + * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * (e.g. `"h o''clock"`). + * + * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or + * number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ). + * @param {string=} format Formatting rules (see Description). If not specified, + * `mediumDate` is used. + * @returns {string} Formatted string or the input if input is not recognized as date/millis. + * + * @example + + + {{1288323623006 | date:'medium'}}: + {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: + {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+
+ + it('should format date', function() { + expect(binding("1288323623006 | date:'medium'")). + toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); + expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). + toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/); + expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). + toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + }); + +
+ */ +dateFilter.$inject = ['$locale']; +function dateFilter($locale) { + return function(date, format) { + var text = '', + parts = [], + fn, match; + + format = format || 'mediumDate' + format = $locale.DATETIME_FORMATS[format] || format; + if (isString(date)) { + if (NUMBER_STRING.test(date)) { + date = parseInt(date, 10); + } else { + date = jsonStringToDate(date); + } + } + + if (isNumber(date)) { + date = new Date(date); + } + + if (!isDate(date)) { + return date; + } + + while(format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = concat(parts, match, 1); + format = parts.pop(); + } else { + parts.push(format); + format = null; + } + } + + forEach(parts, function(value){ + fn = DATE_FORMATS[value]; + text += fn ? fn(date, $locale.DATETIME_FORMATS) + : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + }); + + return text; + }; +} + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.json + * @function + * + * @description + * Allows you to convert a JavaScript object into JSON string. + * + * This filter is mostly useful for debugging. When using the double curly {{value}} notation + * the binding is automatically converted to JSON. + * + * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @returns {string} JSON string. + * + * @css ng-monospace Always applied to the encapsulating element. + * + * @example: + + +
{{ {'name':'value'} | json }}
+
+ + it('should jsonify filtered objects', function() { + expect(binding('| json')).toBe('{\n "name":"value"}'); + }); + +
+ * + */ +function jsonFilter() { + return function(object) { + return toJson(object, true); + }; +} + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.lowercase + * @function + * @description + * Converts string to lowercase. + * @see angular.lowercase + */ +var lowercaseFilter = valueFn(lowercase); + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.uppercase + * @function + * @description + * Converts string to uppercase. + * @see angular.uppercase + */ +var uppercaseFilter = valueFn(uppercase); + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.html + * @function + * + * @description + * Prevents the input from getting escaped by angular. By default the input is sanitized and + * inserted into the DOM as is. + * + * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. + * + * If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses + * the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this + * option is strongly discouraged and should be used only if you absolutely trust the input being + * filtered and you can't get the content through the sanitizer. + * + * @param {string} html Html input. + * @param {string=} option If 'unsafe' then do not sanitize the HTML input. + * @returns {string} Sanitized or raw html. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
html filter +
<div ng:bind="snippet | html">
</div>
+
+
+
no filter
<div ng:bind="snippet">
</div>
unsafe html filter
<div ng:bind="snippet | html:'unsafe'">
</div>
+
+
+ + it('should sanitize the html snippet ', function() { + expect(using('#html-filter').binding('snippet | html')). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should escape snippet without any filter', function() { + expect(using('#escaped-html').binding('snippet')). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should inline raw snippet if filtered as unsafe', function() { + expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should update', function() { + input('snippet').enter('new text'); + expect(using('#html-filter').binding('snippet | html')).toBe('new text'); + expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>"); + expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new text'); + }); +
+
+ */ +//TODO(misko): turn sensitization into injectable service +function htmlFilter() { + return function(html, option){ + return new HTML(html, option); + }; +} + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.linky + * @function + * + * @description + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. + * + * @param {string} text Input text. + * @returns {string} Html-linkified text. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng:bind="snippet | linky">
</div>
+
+
+
no filter
<div ng:bind="snippet">
</div>
+ + + it('should linkify the snippet with urls', function() { + expect(using('#linky-filter').binding('snippet | linky')). + toBe('Pretty text with some links:\n' + + 'http://angularjs.org/,\n' + + 'us@somewhere.org,\n' + + 'another@somewhere.org,\n' + + 'and one more: ftp://127.0.0.1/.'); + }); + + it ('should not linkify snippet without the linky filter', function() { + expect(using('#escaped-html').binding('snippet')). + toBe("Pretty text with some links:\n" + + "http://angularjs.org/,\n" + + "mailto:us@somewhere.org,\n" + + "another@somewhere.org,\n" + + "and one more: ftp://127.0.0.1/."); + }); + + it('should update', function() { + input('snippet').enter('new http://link.'); + expect(using('#linky-filter').binding('snippet | linky')). + toBe('new http://link.'); + expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); + }); + + + */ +function linkyFilter() { + var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, + MAILTO_REGEXP = /^mailto:/; + + return function(text) { + if (!text) return text; + var match; + var raw = text; + var html = []; + var writer = htmlSanitizeWriter(html); + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/mailto then assume mailto + if (match[2] == match[3]) url = 'mailto:' + url; + i = match.index; + writer.chars(raw.substr(0, i)); + writer.start('a', {href:url}); + writer.chars(match[0].replace(MAILTO_REGEXP, '')); + writer.end('a'); + raw = raw.substring(i + match[0].length); + } + writer.chars(raw); + return new HTML(html.join('')); + }; +}; + +/** + * @ngdoc function + * @name angular.module.ng.$filter.limitTo + * @function + * + * @description + * Creates a new array containing only a specified number of elements in an array. The elements + * are taken from either the beginning or the end of the source array, as specified by the + * value and sign (positive or negative) of `limit`. + * + * Note: This function is used to augment the `Array` type in Angular expressions. See + * {@link angular.module.ng.$filter} for more information about Angular arrays. + * + * @param {Array} array Source array to be limited. + * @param {string|Number} limit The length of the returned array. If the `limit` number is + * positive, `limit` number of items from the beginning of the source array are copied. + * If the number is negative, `limit` number of items from the end of the source array are + * copied. The `limit` will be trimmed if it exceeds `array.length` + * @returns {Array} A new sub-array of length `limit` or less if input array had less than `limit` + * elements. + * + * @example + + + +
+ Limit {{numbers}} to: +

Output: {{ numbers | limitTo:limit | json }}

+
+
+ + it('should limit the numer array to first three items', function() { + expect(element('.doc-example-live input[ng\\:model=limit]').val()).toBe('3'); + expect(binding('numbers | limitTo:limit | json')).toEqual('[1,2,3]'); + }); + + it('should update the output when -3 is entered', function() { + input('limit').enter(-3); + expect(binding('numbers | limitTo:limit | json')).toEqual('[7,8,9]'); + }); + + it('should not exceed the maximum size of input array', function() { + input('limit').enter(100); + expect(binding('numbers | limitTo:limit | json')).toEqual('[1,2,3,4,5,6,7,8,9]'); + }); + +
+ */ +function limitToFilter(){ + return function(array, limit) { + if (!(array instanceof Array)) return array; + limit = parseInt(limit, 10); + var out = [], + i, n; + + // check that array is iterable + if (!array || !(array instanceof Array)) + return out; + + // if abs(limit) exceeds maximum length, trim it + if (limit > array.length) + limit = array.length; + else if (limit < -array.length) + limit = -array.length; + + if (limit > 0) { + i = 0; + n = limit; + } else { + i = array.length + limit; + n = array.length; + } + + for (; i} expression A predicate to be + * used by the comparator to determine the order of elements. + * + * Can be one of: + * + * - `function`: Getter function. The result of this function will be sorted using the + * `<`, `=`, `>` operator. + * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' + * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control + * ascending or descending sort order (for example, +name or -name). + * - `Array`: An array of function or string predicates. The first predicate in the array + * is used for sorting, but when two items are equivalent, the next predicate is used. + * + * @param {boolean=} reverse Reverse the order the array. + * @returns {Array} Sorted copy of the source array. + * + * @example + + + +
+
Sorting predicate = {{predicate}}; reverse = {{reverse}}
+
+ [ unsorted ] + + + + + + + + + + + +
Name + (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
+
+
+ + it('should be reverse ordered by aged', function() { + expect(binding('predicate')).toBe('Sorting predicate = -age; reverse = '); + expect(repeater('table.friend', 'friend in friends').column('friend.age')). + toEqual(['35', '29', '21', '19', '10']); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); + }); + + it('should reorder the table when user selects different predicate', function() { + element('.doc-example-live a:contains("Name")').click(); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); + expect(repeater('table.friend', 'friend in friends').column('friend.age')). + toEqual(['35', '10', '29', '19', '21']); + + element('.doc-example-live a:contains("Phone")').click(); + expect(repeater('table.friend', 'friend in friends').column('friend.phone')). + toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); + }); + +
+ */ +orderByFilter.$inject = ['$parse']; +function orderByFilter($parse){ + return function(array, sortPredicate, reverseOrder) { + if (!(array instanceof Array)) return array; + if (!sortPredicate) return array; + sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; + sortPredicate = map(sortPredicate, function(predicate){ + var descending = false, get = predicate || identity; + if (isString(predicate)) { + if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { + descending = predicate.charAt(0) == '-'; + predicate = predicate.substring(1); + } + get = $parse(predicate); + } + return reverseComparator(function(a,b){ + return compare(get(a),get(b)); + }, descending); + }); + var arrayCopy = []; + for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } + return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + + function comparator(o1, o2){ for ( var i = 0; i < sortPredicate.length; i++) { var comp = sortPredicate[i](o1, o2); if (comp !== 0) return comp; } - return 0; - } - function reverseComparator(comp, descending) { - return toBoolean(descending) - ? function(a,b){return comp(b,a);} - : comp; - } - function compare(v1, v2){ - var t1 = typeof v1; - var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") v1 = v1.toLowerCase(); - if (t1 == "string") v2 = v2.toLowerCase(); - if (v1 === v2) return 0; - return v1 < v2 ? -1 : 1; + return 0; + } + function reverseComparator(comp, descending) { + return toBoolean(descending) + ? function(a,b){return comp(b,a);} + : comp; + } + function compare(v1, v2){ + var t1 = typeof v1; + var t2 = typeof v2; + if (t1 == t2) { + if (t1 == "string") v1 = v1.toLowerCase(); + if (t1 == "string") v2 = v2.toLowerCase(); + if (v1 === v2) return 0; + return v1 < v2 ? -1 : 1; + } else { + return t1 < t2 ? -1 : 1; + } + } + } +} + +/** + * @ngdoc object + * @name angular.module.ng.$formFactory + * + * @description + * Use `$formFactory` to create a new instance of a {@link angular.module.ng.$formFactory.Form Form} + * controller or to find the nearest form instance for a given DOM element. + * + * The form instance is a collection of widgets, and is responsible for life cycle and validation + * of widget. + * + * Keep in mind that both form and widget instances are {@link api/angular.module.ng.$rootScope.Scope scopes}. + * + * @param {Form=} parentForm The form which should be the parent form of the new form controller. + * If none specified default to the `rootForm`. + * @returns {Form} A new {@link angular.module.ng.$formFactory.Form Form} instance. + * + * @example + * + * This example shows how one could write a widget which would enable data-binding on + * `contenteditable` feature of HTML. + * + + + +
+
+
+ HTML:
+ +
+
editorForm = {{editorForm}}
+
+
+ + it('should enter invalid HTML', function() { + expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); + input('html').enter('<'); + expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); + }); + +
+ */ + +/** + * @ngdoc object + * @name angular.module.ng.$formFactory.Form + * @description + * The `Form` is a controller which keeps track of the validity of the widgets contained within it. + */ + +function $FormFactoryProvider() { + var $parse; + this.$get = ['$rootScope', '$parse', function($rootScope, $parse_) { + $parse = $parse_; + /** + * @ngdoc proprety + * @name rootForm + * @propertyOf angular.module.ng.$formFactory + * @description + * Static property on `$formFactory` + * + * Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which + * is the top-level parent of all forms. + */ + formFactory.rootForm = formFactory($rootScope); + + + /** + * @ngdoc method + * @name forElement + * @methodOf angular.module.ng.$formFactory + * @description + * Static method on `$formFactory` service. + * + * Retrieve the closest form for a given element or defaults to the `root` form. Used by the + * {@link angular.widget.form form} element. + * @param {Element} element The element where the search for form should initiate. + */ + formFactory.forElement = function(element) { + return element.inheritedData('$form') || formFactory.rootForm; + }; + return formFactory; + + function formFactory(parent) { + return (parent || formFactory.rootForm).$new(FormController); + } + + }]; + + function propertiesUpdate(widget) { + widget.$valid = !(widget.$invalid = + !(widget.$readonly || widget.$disabled || equals(widget.$error, {}))); + } + + /** + * @ngdoc property + * @name $error + * @propertyOf angular.module.ng.$formFactory.Form + * @description + * Property of the form and widget instance. + * + * Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key, + * then the `$error` object will have a `REQUIRED` key with an array of widgets which have + * emitted this key. `form.$error.REQUIRED == [ widget ]`. + */ + + /** + * @ngdoc property + * @name $invalid + * @propertyOf angular.module.ng.$formFactory.Form + * @description + * Property of the form and widget instance. + * + * True if any of the widgets of the form are invalid. + */ + + /** + * @ngdoc property + * @name $valid + * @propertyOf angular.module.ng.$formFactory.Form + * @description + * Property of the form and widget instance. + * + * True if all of the widgets of the form are valid. + */ + + /** + * @ngdoc event + * @name angular.module.ng.$formFactory.Form#$valid + * @eventOf angular.module.ng.$formFactory.Form + * @eventType listen on form + * @description + * Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid` + * properties of both the widget as well as the from. + * + * @param {string} validationKey The validation key to be used when updating the `$error` object. + * The validation key is what will allow the template to bind to a specific validation error + * such as `
error for key
`. + */ + + /** + * @ngdoc event + * @name angular.module.ng.$formFactory.Form#$invalid + * @eventOf angular.module.ng.$formFactory.Form + * @eventType listen on form + * @description + * Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid` + * properties of both the widget as well as the from. + * + * @param {string} validationKey The validation key to be used when updating the `$error` object. + * The validation key is what will allow the template to bind to a specific validation error + * such as `
error for key
`. + */ + + /** + * @ngdoc event + * @name angular.module.ng.$formFactory.Form#$validate + * @eventOf angular.module.ng.$formFactory.Form + * @eventType emit on widget + * @description + * Emit the `$validate` event on the widget, giving a widget a chance to emit a + * `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the + * model or the view changes. + */ + + /** + * @ngdoc event + * @name angular.module.ng.$formFactory.Form#$viewChange + * @eventOf angular.module.ng.$formFactory.Form + * @eventType listen on widget + * @description + * A widget is responsible for emitting this event whenever the view changes do to user interaction. + * The event takes a `$viewValue` parameter, which is the new value of the view. This + * event triggers a call to `$parseView()` as well as `$validate` event on widget. + * + * @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`. + */ + + function FormController() { + var form = this, + $error = form.$error = {}; + + form.$on('$destroy', function(event){ + var widget = event.targetScope; + if (widget.$widgetId) { + delete form[widget.$widgetId]; + } + forEach($error, removeWidget, widget); + }); + + form.$on('$valid', function(event, error){ + var widget = event.targetScope; + delete widget.$error[error]; + propertiesUpdate(widget); + removeWidget($error[error], error, widget); + }); + + form.$on('$invalid', function(event, error){ + var widget = event.targetScope; + addWidget(error, widget); + widget.$error[error] = true; + propertiesUpdate(widget); + }); + + propertiesUpdate(form); + + function removeWidget(queue, errorKey, widget) { + if (queue) { + widget = widget || this; // so that we can be used in forEach; + for (var i = 0, length = queue.length; i < length; i++) { + if (queue[i] === widget) { + queue.splice(i, 1); + if (!queue.length) { + delete $error[errorKey]; + } + } + } + propertiesUpdate(form); + } + } + + function addWidget(errorKey, widget) { + var queue = $error[errorKey]; + if (queue) { + for (var i = 0, length = queue.length; i < length; i++) { + if (queue[i] === widget) { + return; + } + } + } else { + $error[errorKey] = queue = []; + } + queue.push(widget); + propertiesUpdate(form); + } + } + + + /** + * @ngdoc method + * @name $createWidget + * @methodOf angular.module.ng.$formFactory.Form + * @description + * + * Use form's `$createWidget` instance method to create new widgets. The widgets can be created + * using an alias which makes the accessible from the form and available for data-binding, + * useful for displaying validation error messages. + * + * The creation of a widget sets up: + * + * - `$watch` of `expression` on `model` scope. This code path syncs the model to the view. + * The `$watch` listener will: + * + * - assign the new model value of `expression` to `widget.$modelValue`. + * - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying + * the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data. + * (For example to convert a number into string) + * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` + * event. + * - call `widget.$render()` method on widget. The `$render` method is responsible for + * reading the `widget.$viewValue` and updating the DOM. + * + * - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model. + * The `$viewChange` listener will: + * + * - assign the value to `widget.$viewValue`. + * - call `widget.$parseView` method if present. The `$parseView` is responsible for copying + * the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data. + * (For example to convert a string into number) + * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` + * event. + * - Assign the `widget.$modelValue` to the `expression` on the `model` scope. + * + * - Creates these set of properties on the `widget` which are updated as a response to the + * `$valid` / `$invalid` events: + * + * - `$error` - object - validation errors will be published as keys on this object. + * Data-binding to this property is useful for displaying the validation errors. + * - `$valid` - boolean - true if there are no validation errors + * - `$invalid` - boolean - opposite of `$valid`. + * @param {Object} params Named parameters: + * + * - `scope` - `{Scope}` - The scope to which the model for this widget is attached. + * - `model` - `{string}` - The name of the model property on model scope. + * - `controller` - {WidgetController} - The controller constructor function. + * The controller constructor should create these instance methods. + * - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`. + * The method may fire `$valid`/`$invalid` events. + * - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`. + * The method may fire `$valid`/`$invalid` events. + * - `$render()`: required method which needs to update the DOM of the widget to match the + * `$viewValue`. + * + * - `controllerArgs` - `{Array}` (Optional) - Any extra arguments will be curried to the + * WidgetController constructor. + * - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the + * value. + * - `alias` - `{string}` (Optional) - The name of the form property under which the widget + * instance should be published. The name should be unique for each form. + * @returns {Widget} Instance of a widget scope. + */ + FormController.prototype.$createWidget = function(params) { + var form = this, + modelScope = params.scope, + onChange = params.onChange, + alias = params.alias, + scopeGet = $parse(params.model), + scopeSet = scopeGet.assign, + widget = this.$new(params.controller, params.controllerArgs); + + if (!scopeSet) { + throw Error("Expression '" + params.model + "' is not assignable!"); + }; + + widget.$error = {}; + // Set the state to something we know will change to get the process going. + widget.$modelValue = Number.NaN; + // watch for scope changes and update the view appropriately + modelScope.$watch(scopeGet, function(scope, value) { + if (!equals(widget.$modelValue, value)) { + widget.$modelValue = value; + widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value); + widget.$emit('$validate'); + widget.$render && widget.$render(); + } + }); + + widget.$on('$viewChange', function(event, viewValue){ + if (!equals(widget.$viewValue, viewValue)) { + widget.$viewValue = viewValue; + widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue); + scopeSet(modelScope, widget.$modelValue); + if (onChange) modelScope.$eval(onChange); + widget.$emit('$validate'); + } + }); + + propertiesUpdate(widget); + + // assign the widgetModel to the form + if (alias && !form.hasOwnProperty(alias)) { + form[alias] = widget; + widget.$widgetId = alias; + } else { + alias = null; + } + + return widget; + }; +} + +function $InterpolateProvider(){ + this.$get = ['$parse', function($parse){ + return function(text, templateOnly) { + var bindings = parseBindings(text); + if (hasBindings(bindings) || !templateOnly) { + return compileBindTemplate(text); + } + }; + }]; +} + +var bindTemplateCache = {}; +function compileBindTemplate(template){ + var fn = bindTemplateCache[template]; + if (!fn) { + var bindings = []; + forEach(parseBindings(template), function(text){ + var exp = binding(text); + bindings.push(exp + ? function(scope, element) { return scope.$eval(exp); } + : function() { return text; }); + }); + bindTemplateCache[template] = fn = function(scope, element, prettyPrintJson) { + var parts = [], + hadOwnElement = scope.hasOwnProperty('$element'), + oldElement = scope.$element; + + // TODO(misko): get rid of $element + scope.$element = element; + try { + for (var i = 0; i < bindings.length; i++) { + var value = bindings[i](scope, element); + if (isElement(value)) + value = ''; + else if (isObject(value)) + value = toJson(value, prettyPrintJson); + parts.push(value); + } + return parts.join(''); + } finally { + if (hadOwnElement) { + scope.$element = oldElement; + } else { + delete scope.$element; + } + } + }; + } + return fn; +} + + +function parseBindings(string) { + var results = []; + var lastIndex = 0; + var index; + while((index = string.indexOf('{{', lastIndex)) > -1) { + if (lastIndex < index) + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + + index = string.indexOf('}}', index); + index = index < 0 ? string.length : index + 2; + + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + } + if (lastIndex != string.length) + results.push(string.substr(lastIndex, string.length - lastIndex)); + return results.length === 0 ? [ string ] : results; +} + +function binding(string) { + var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); + return binding ? binding[1] : null; +} + +function hasBindings(bindings) { + return bindings.length > 1 || binding(bindings[0]) !== null; +} + +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, + PATH_MATCH = /^([^\?#]*)?(\?([^#]*))?(#(.*))?$/, + HASH_MATCH = PATH_MATCH, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; + + +/** + * Encode path using encodeUriSegment, ignoring forward slashes + * + * @param {string} path Path to encode + * @returns {string} + */ +function encodePath(path) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = encodeUriSegment(segments[i]); + } + + return segments.join('/'); +} + + +function matchUrl(url, obj) { + var match = URL_MATCH.exec(url); + + match = { + protocol: match[1], + host: match[3], + port: parseInt(match[5], 10) || DEFAULT_PORTS[match[1]] || null, + path: match[6] || '/', + search: match[8], + hash: match[10] + }; + + if (obj) { + obj.$$protocol = match.protocol; + obj.$$host = match.host; + obj.$$port = match.port; + } + + return match; +} + + +function composeProtocolHostPort(protocol, host, port) { + return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port); +} + + +function pathPrefixFromBase(basePath) { + return basePath.substr(0, basePath.lastIndexOf('/')); +} + + +function convertToHtml5Url(url, basePath, hashPrefix) { + var match = matchUrl(url); + + // already html5 url + if (decodeURIComponent(match.path) != basePath || isUndefined(match.hash) || + match.hash.indexOf(hashPrefix) !== 0) { + return url; + // convert hashbang url -> html5 url + } else { + return composeProtocolHostPort(match.protocol, match.host, match.port) + + pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length); + } +} + + +function convertToHashbangUrl(url, basePath, hashPrefix) { + var match = matchUrl(url); + + // already hashbang url + if (decodeURIComponent(match.path) == basePath) { + return url; + // convert html5 url -> hashbang url + } else { + var search = match.search && '?' + match.search || '', + hash = match.hash && '#' + match.hash || '', + pathPrefix = pathPrefixFromBase(basePath), + path = match.path.substr(pathPrefix.length); + + if (match.path.indexOf(pathPrefix) !== 0) { + throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'; + } + + return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath + + '#' + hashPrefix + path + search + hash; + } +} + + +/** + * LocationUrl represents an url + * This object is exposed as $location service when HTML5 mode is enabled and supported + * + * @constructor + * @param {string} url HTML5 url + * @param {string} pathPrefix + */ +function LocationUrl(url, pathPrefix) { + pathPrefix = pathPrefix || ''; + + /** + * Parse given html5 (regular) url string into properties + * @param {string} url HTML5 url + * @private + */ + this.$$parse = function(url) { + var match = matchUrl(url, this); + + if (match.path.indexOf(pathPrefix) !== 0) { + throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'; + } + + this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); + this.$$search = parseKeyValue(match.search); + this.$$hash = match.hash && decodeURIComponent(match.hash) || ''; + + this.$$compose(); + }; + + /** + * Compose url and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + + pathPrefix + this.$$url; + }; + + this.$$parse(url); +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when html5 history api is disabled or not supported + * + * @constructor + * @param {string} url Legacy url + * @param {string} hashPrefix Prefix for hash part (containing path and search) + */ +function LocationHashbangUrl(url, hashPrefix) { + var basePath; + + /** + * Parse given hashbang url into properties + * @param {string} url Hashbang url + * @private + */ + this.$$parse = function(url) { + var match = matchUrl(url, this); + + if (match.hash && match.hash.indexOf(hashPrefix) !== 0) { + throw 'Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !'; + } + + basePath = match.path + (match.search ? '?' + match.search : ''); + match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length)); + if (match[1]) { + this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]); + } else { + this.$$path = ''; + } + + this.$$search = parseKeyValue(match[3]); + this.$$hash = match[5] && decodeURIComponent(match[5]) || ''; + + this.$$compose(); + }; + + /** + * Compose hashbang url and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + + basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); + }; + + this.$$parse(url); +} + + +LocationUrl.prototype = { + + /** + * Has any change been replacing ? + * @private + */ + $$replace: false, + + /** + * @ngdoc method + * @name angular.module.ng.$location#absUrl + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter only. + * + * Return full url representation with all segments encoded according to rules specified in + * {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}. + * + * @return {string} + */ + absUrl: locationGetter('$$absUrl'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#url + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter / setter. + * + * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * + * Change path, search and hash, when called with parameter and return `$location`. + * + * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) + * @return {string} + */ + url: function(url, replace) { + if (isUndefined(url)) + return this.$$url; + + var match = PATH_MATCH.exec(url); + if (match[1]) this.path(decodeURIComponent(match[1])); + if (match[2] || match[1]) this.search(match[3] || ''); + this.hash(match[5] || '', replace); + + return this; + }, + + /** + * @ngdoc method + * @name angular.module.ng.$location#protocol + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter only. + * + * Return protocol of current url. + * + * @return {string} + */ + protocol: locationGetter('$$protocol'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#host + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter only. + * + * Return host of current url. + * + * @return {string} + */ + host: locationGetter('$$host'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#port + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter only. + * + * Return port of current url. + * + * @return {Number} + */ + port: locationGetter('$$port'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#path + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter / setter. + * + * Return path of current url when called without any parameter. + * + * Change path when called with parameter and return `$location`. + * + * Note: Path should always begin with forward slash (/), this method will add the forward slash + * if it is missing. + * + * @param {string=} path New path + * @return {string} + */ + path: locationGetterSetter('$$path', function(path) { + return path.charAt(0) == '/' ? path : '/' + path; + }), + + /** + * @ngdoc method + * @name angular.module.ng.$location#search + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter / setter. + * + * Return search part (as object) of current url when called without any parameter. + * + * Change search part when called with parameter and return `$location`. + * + * @param {string|object=} search New search params - string or hash object + * @param {string=} paramValue If `search` is a string, then `paramValue` will override only a + * single search parameter. If the value is `null`, the parameter will be deleted. + * + * @return {string} + */ + search: function(search, paramValue) { + if (isUndefined(search)) + return this.$$search; + + if (isDefined(paramValue)) { + if (paramValue === null) { + delete this.$$search[search]; + } else { + this.$$search[search] = encodeUriQuery(paramValue); + } + } else { + this.$$search = isString(search) ? parseKeyValue(search) : search; + } + + this.$$compose(); + return this; + }, + + /** + * @ngdoc method + * @name angular.module.ng.$location#hash + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter / setter. + * + * Return hash fragment when called without any parameter. + * + * Change hash fragment when called with parameter and return `$location`. + * + * @param {string=} hash New hash fragment + * @return {string} + */ + hash: locationGetterSetter('$$hash', identity), + + /** + * @ngdoc method + * @name angular.module.ng.$location#replace + * @methodOf angular.module.ng.$location + * + * @description + * If called, all changes to $location during current `$digest` will be replacing current history + * record, instead of adding new one. + */ + replace: function() { + this.$$replace = true; + return this; + } +}; + +LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); + +function locationGetter(property) { + return function() { + return this[property]; + }; +} + + +function locationGetterSetter(property, preprocess) { + return function(value) { + if (isUndefined(value)) + return this[property]; + + this[property] = preprocess(value); + this.$$compose(); + + return this; + }; +} + + +/** + * @ngdoc object + * @name angular.module.ng.$location + * + * @requires $browser + * @requires $sniffer + * @requires $document + * + * @description + * The $location service parses the URL in the browser address bar (based on the {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL available to your application. Changes to the URL in the address bar are reflected into $location service and changes to $location are reflected into the browser address bar. + * + * **The $location service:** + * + * - Exposes the current URL in the browser address bar, so you can + * - Watch and observe the URL. + * - Change the URL. + * - Synchronizes the URL with the browser when the user + * - Changes the address bar. + * - Clicks the back or forward button (or clicks a History link). + * - Clicks on a link. + * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). + * + * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular Services: Using $location} + */ + +/** + * @ngdoc object + * @name angular.module.ng.$locationProvider + * @description + * Use the `$locationProvider` to configure how the application deep linking paths are stored. + */ +function $LocationProvider(){ + var hashPrefix = '', + html5Mode = false; + + /** + * @ngdoc property + * @name angular.module.ng.$locationProvider#hashPrefix + * @methodOf angular.module.ng.$locationProvider + * @description + * @param {string=} prefix Prefix for hash part (containing path and search) + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.hashPrefix = function(prefix) { + if (isDefined(prefix)) { + hashPrefix = prefix; + return this; + } else { + return html5Mode; + } + } + + /** + * @ngdoc property + * @name angular.module.ng.$locationProvider#hashPrefix + * @methodOf angular.module.ng.$locationProvider + * @description + * @param {string=} mode Use HTML5 strategy if available. + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.html5Mode = function(mode) { + if (isDefined(mode)) { + html5Mode = mode; + return this; + } else { + return html5Mode; + } + }; + + this.$get = ['$rootScope', '$browser', '$sniffer', '$document', + function( $rootScope, $browser, $sniffer, $document) { + var currentUrl, + basePath = $browser.baseHref() || '/', + pathPrefix = pathPrefixFromBase(basePath), + initUrl = $browser.url(); + + if (html5Mode) { + if ($sniffer.history) { + currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix); + } else { + currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix), + hashPrefix); + } + + // link rewriting + var u = currentUrl, + absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.port()) + pathPrefix; + + $document.bind('click', function(event) { + // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) + // currently we open nice url link and redirect then + + if (event.ctrlKey || event.metaKey || event.which == 2) return; + + var elm = jqLite(event.target); + + // traverse the DOM up to find first A tag + while (elm.length && lowercase(elm[0].nodeName) !== 'a') { + elm = elm.parent(); + } + + var href = elm.attr('href'); + if (!href || isDefined(elm.attr('ng:ext-link')) || elm.attr('target')) return; + + // remove same domain from full url links (IE7 always returns full hrefs) + href = href.replace(absUrlPrefix, ''); + + // link to different domain (or base path) + if (href.substr(0, 4) == 'http') return; + + // remove pathPrefix from absolute links + href = href.indexOf(pathPrefix) === 0 ? href.substr(pathPrefix.length) : href; + + currentUrl.url(href); + $rootScope.$apply(); + event.preventDefault(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + window.angular['ff-684208-preventDefault'] = true; + }); + } else { + currentUrl = new LocationHashbangUrl(initUrl, hashPrefix); + } + + // rewrite hashbang url <> html5 url + if (currentUrl.absUrl() != initUrl) { + $browser.url(currentUrl.absUrl(), true); + } + + // update $location when $browser url changes + $browser.onUrlChange(function(newUrl) { + if (currentUrl.absUrl() != newUrl) { + currentUrl.$$parse(newUrl); + $rootScope.$apply(); + } + }); + + // update browser + var changeCounter = 0; + $rootScope.$watch(function $locationWatch() { + if ($browser.url() != currentUrl.absUrl()) { + changeCounter++; + $rootScope.$evalAsync(function() { + $browser.url(currentUrl.absUrl(), currentUrl.$$replace); + currentUrl.$$replace = false; + }); + } + + return changeCounter; + }); + + return currentUrl; +}]; +} + +/** + * @ngdoc object + * @name angular.module.ng.$log + * @requires $window + * + * @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. + * + * @example + + + +
+

Reload this page with open console, enter text and hit the log button...

+ Message: + + + + + +
+
+ + +
+ */ + +function $LogProvider(){ + this.$get = ['$window', function($window){ + return { + /** + * @ngdoc method + * @name angular.module.ng.$log#log + * @methodOf angular.module.ng.$log + * + * @description + * Write a log message + */ + log: consoleLog('log'), + + /** + * @ngdoc method + * @name angular.module.ng.$log#warn + * @methodOf angular.module.ng.$log + * + * @description + * Write a warning message + */ + warn: consoleLog('warn'), + + /** + * @ngdoc method + * @name angular.module.ng.$log#info + * @methodOf angular.module.ng.$log + * + * @description + * Write an information message + */ + info: consoleLog('info'), + + /** + * @ngdoc method + * @name angular.module.ng.$log#error + * @methodOf angular.module.ng.$log + * + * @description + * Write an error message + */ + error: consoleLog('error') + }; + + function formatError(arg) { + if (arg instanceof Error) { + if (arg.stack) { + arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? + 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; + } else if (arg.sourceURL) { + arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; + } + } + return arg; + } + + function consoleLog(type) { + var console = $window.console || {}; + var logFn = console[type] || console.log || noop; + if (logFn.apply) { + return function() { + var args = []; + forEach(arguments, function(arg){ + args.push(formatError(arg)); + }); + return logFn.apply(console, args); + }; + } else { + // we are IE, in which case there is nothing we can do + return logFn; + } + } + }]; +} + +/** + * @ngdoc object + * @name angular.module.ng.$resource + * @requires $http + * + * @description + * A factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link angular.module.ng.$http $http} service. + * + * @param {string} url A parameterized URL template with parameters prefixed by `:` as in + * `/user/:username`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + * `actions` methods. + * + * Each key value in the parameter object is first bound to url template if present and then any + * excess keys are appended to the url search query after the `?`. + * + * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * URL `/path/greet?salutation=Hello`. + * + * If the parameter value is prefixed with `@` then the value of that parameter is extracted from + * the data object (useful for non-GET operations). + * + * @param {Object.=} actions Hash with declaration of custom action that should extend the + * default set of resource actions. The declaration should be created in the following format: + * + * {action1: {method:?, params:?, isArray:?, verifyCache:?}, + * action2: {method:?, params:?, isArray:?, verifyCache:?}, + * ...} + * + * Where: + * + * - `action` – {string} – The name of action. This name becomes the name of the method on your + * resource object. + * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, + * and `JSONP` + * - `params` – {object=} – Optional set of pre-bound parameters for this action. + * - isArray – {boolean=} – If true then the returned object for this action is an array, see + * `returns` section. + * - verifyCache – {boolean=} – If true then whenever cache hit occurs, the object is returned and + * an async request will be made to the server and the resources as well as the cache will be + * updated when the response is received. + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + * optionally extended with custom `actions`. The default set contains these actions: + * + * { 'get': {method:'GET'}, + * 'save': {method:'POST'}, + * 'query': {method:'GET', isArray:true}, + * 'remove': {method:'DELETE'}, + * 'delete': {method:'DELETE'} }; + * + * Calling these methods invoke an {@link angular.module.ng.$http} with the specified http method, + * destination and parameters. When the data is returned from the server then the object is an + * instance of the resource class `save`, `remove` and `delete` actions are available on it as + * methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, + * update, delete) on server-side data like this: + *
+        var User = $resource('/user/:userId', {userId:'@id'});
+        var user = User.get({userId:123}, function() {
+          user.abc = true;
+          user.$save();
+        });
+     
+ * + * It is important to realize that invoking a $resource object method immediately returns an + * empty reference (object or array depending on `isArray`). Once the data is returned from the + * server the existing reference is populated with the actual data. This is a useful trick since + * usually the resource is assigned to a model which is then rendered by the view. Having an empty + * object results in no rendering, once the data arrives from the server then the object is + * populated with the data and the view automatically re-renders itself showing the new data. This + * means that in most case one never has to write a callback function for the action methods. + * + * The action methods on the class object or instance object can be invoked with the following + * parameters: + * + * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` + * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * + * + * @example + * + * # Credit card resource + * + *
+     // Define CreditCard class
+     var CreditCard = $resource('/user/:userId/card/:cardId',
+      {userId:123, cardId:'@id'}, {
+       charge: {method:'POST', params:{charge:true}}
+      });
+
+     // We can retrieve a collection from the server
+     var cards = CreditCard.query();
+     // GET: /user/123/card
+     // server returns: [ {id:456, number:'1234', name:'Smith'} ];
+
+     var card = cards[0];
+     // each item is an instance of CreditCard
+     expect(card instanceof CreditCard).toEqual(true);
+     card.name = "J. Smith";
+     // non GET methods are mapped onto the instances
+     card.$save();
+     // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
+     // server returns: {id:456, number:'1234', name: 'J. Smith'};
+
+     // our custom method is mapped as well.
+     card.$charge({amount:9.99});
+     // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
+     // server returns: {id:456, number:'1234', name: 'J. Smith'};
+
+     // we can create an instance as well
+     var newCard = new CreditCard({number:'0123'});
+     newCard.name = "Mike Smith";
+     newCard.$save();
+     // POST: /user/123/card {number:'0123', name:'Mike Smith'}
+     // server returns: {id:789, number:'01234', name: 'Mike Smith'};
+     expect(newCard.id).toEqual(789);
+ * 
+ * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + +
+     var User = $resource('/user/:userId', {userId:'@id'});
+     var user = User.get({userId:123}, function() {
+       user.abc = true;
+       user.$save();
+     });
+   
+ * + * It's worth noting that the success callback for `get`, `query` and other method gets passed + * in the response that came from the server as well as $http header getter function, so one + * could rewrite the above example and get access to http headers as: + * +
+     var User = $resource('/user/:userId', {userId:'@id'});
+     User.get({userId:123}, function(u, getResponseHeaders){
+       u.abc = true;
+       u.$save(function(u, putResponseHeaders) {
+         //u => saved user object
+         //putResponseHeaders => $http header getter
+       });
+     });
+   
+ + * # Buzz client + + Let's look at what a buzz client created with the `$resource` service looks like: + + + + +
+ + +
+
+

+ + {{item.actor.name}} + Expand replies: {{item.links.replies[0].count}} +

+ {{item.object.content | html}} +
+ + {{reply.actor.name}}: {{reply.content | html}} +
+
+
+
+ + +
+ */ +function $ResourceProvider() { + this.$get = ['$http', function($http) { + var resource = new ResourceFactory($http); + return bind(resource, resource.route); + }]; +} + +var OPERATORS = { + 'null':function(self){return null;}, + 'true':function(self){return true;}, + 'false':function(self){return false;}, + $undefined:noop, + '+':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, + '-':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, + '*':function(self, a,b){return a(self)*b(self);}, + '/':function(self, a,b){return a(self)/b(self);}, + '%':function(self, a,b){return a(self)%b(self);}, + '^':function(self, a,b){return a(self)^b(self);}, + '=':noop, + '==':function(self, a,b){return a(self)==b(self);}, + '!=':function(self, a,b){return a(self)!=b(self);}, + '<':function(self, a,b){return a(self)':function(self, a,b){return a(self)>b(self);}, + '<=':function(self, a,b){return a(self)<=b(self);}, + '>=':function(self, a,b){return a(self)>=b(self);}, + '&&':function(self, a,b){return a(self)&&b(self);}, + '||':function(self, a,b){return a(self)||b(self);}, + '&':function(self, a,b){return a(self)&b(self);}, +// '|':function(self, a,b){return a|b;}, + '|':function(self, a,b){return b(self)(self, a(self));}, + '!':function(self, a){return !a(self);} +}; +var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + +function lex(text){ + 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(), + fn = OPERATORS[ch], + fn2 = OPERATORS[ch2]; + 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; } else { - return t1 < t2 ? -1 : 1; + throwError("Unexpected next character ", index, index+1); } } - }, + lastCh = ch; + } + return tokens; + function is(chars) { + return chars.indexOf(ch) != -1; + } - /** - * @ngdoc function - * @name angular.Array.limitTo - * @function - * - * @description - * Creates a new array containing only a specified number of elements in an array. The elements - * are taken from either the beginning or the end of the source array, as specified by the - * value and sign (positive or negative) of `limit`. - * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link angular.Array} for more information about Angular arrays. - * - * @param {Array} array Source array to be limited. - * @param {string|Number} limit The length of the returned array. If the `limit` number is - * positive, `limit` number of items from the beginning of the source array are copied. - * If the number is negative, `limit` number of items from the end of the source array are - * copied. - * @returns {Array} A new sub-array of length `limit`. - * - * @example - - -
- Limit [1,2,3,4,5,6,7,8,9] to: -

Output: {{ numbers.$limitTo(limit) | json }}

-
-
- - it('should limit the numer array to first three items', function() { - expect(element('.doc-example-live input[name=limit]').val()).toBe('3'); - expect(binding('numbers.$limitTo(limit) | json')).toEqual('[1,2,3]'); - }); + function was(chars) { + return chars.indexOf(lastCh) != -1; + } - it('should update the output when -3 is entered', function() { - input('limit').enter(-3); - expect(binding('numbers.$limitTo(limit) | json')).toEqual('[7,8,9]'); - }); - -
- */ - limitTo: function(array, limit) { - limit = parseInt(limit, 10); - var out = [], - i, n; + function peek() { + return index + 1 < text.length ? text.charAt(index + 1) : 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); + } - if (limit > 0) { - i = 0; - n = limit; - } else { - i = array.length + limit; - n = array.length; - } + function throwError(error, start, end) { + end = end || index; + throw Error("Lexer Error: " + error + " at column" + + (isDefined(start) + ? "s " + start + "-" + index + " [" + text.substring(start, end) + "]" + : " " + end) + + " in expression [" + text + "]."); + } - for (; i 0) { + var token = tokens[0]; + var t = token.text; + if (t==e1 || t==e2 || t==e3 || t==e4 || + (!e1 && !e2 && !e3 && !e4)) { + return token; + } } - return string; + return false; } -}; -var angularDate = { - 'toString':function(date){ - if (!date) return date; - - var isoString = date.toISOString ? date.toISOString() : ''; - - return (isoString.length==24) ? - isoString : - padNumber(date.getUTCFullYear(), 4) + '-' + - padNumber(date.getUTCMonth() + 1, 2) + '-' + - padNumber(date.getUTCDate(), 2) + 'T' + - padNumber(date.getUTCHours(), 2) + ':' + - padNumber(date.getUTCMinutes(), 2) + ':' + - padNumber(date.getUTCSeconds(), 2) + '.' + - padNumber(date.getUTCMilliseconds(), 3) + 'Z'; + function expect(e1, e2, e3, e4){ + var token = peek(e1, e2, e3, e4); + if (token) { + if (json && !token.json) { + throwError("is not valid json", token); + } + tokens.shift(); + return token; } - }; - -var angularFunction = { - 'compile': function(expression) { - if (isFunction(expression)){ - return expression; - } else if (expression){ - return expressionCompile(expression); - } else { - return identity; - } + return false; } -}; -/** - * Computes a hash of an 'obj'. - * Hash of a: - * string is string - * number is number as string - * object is either call $hashKey function on object or assign unique hashKey id. - * - * @param obj - * @returns {String} hash string such that the same input will have the same hash string - */ -function hashKey(obj) { - var objType = typeof obj; - var key = obj; - if (objType == 'object') { - if (typeof (key = obj.$hashKey) == 'function') { - // must invoke on object to keep the right this - key = obj.$hashKey(); - } else if (key === undefined) { - key = obj.$hashKey = nextUid(); + function consume(e1){ + if (!expect(e1)) { + throwError("is unexpected, expecting [" + e1 + "]", peek()); } } - return objType + ':' + key; -} - -/** - * HashMap which can use objects as keys - */ -function HashMap(){} -HashMap.prototype = { - /** - * Store key value pair - * @param key key to store can be any type - * @param value value to store can be any type - * @returns old value if any - */ - put: function(key, value) { - var _key = hashKey(key); - var oldValue = this[_key]; - this[_key] = value; - return oldValue; - }, - - /** - * @param key - * @returns the value for the key - */ - get: function(key) { - return this[hashKey(key)]; - }, - /** - * Remove the key/value pair - * @param key - * @returns value associated with key before it was removed - */ - remove: function(key) { - var _key = hashKey(key); - var value = this[_key]; - delete this[_key]; - return value; + function unaryFn(fn, right) { + return function(self) { + return fn(self, right); + }; } -}; - -function defineApi(dst, chain){ - angular[dst] = angular[dst] || {}; - forEach(chain, function(parent){ - extend(angular[dst], parent); - }); -} -defineApi('Global', [angularGlobal]); -defineApi('Collection', [angularGlobal, angularCollection]); -defineApi('Array', [angularGlobal, angularCollection, angularArray]); -defineApi('Object', [angularGlobal, angularCollection, angularObject]); -defineApi('String', [angularGlobal, angularString]); -defineApi('Date', [angularGlobal, angularDate]); -//IE bug -angular.Date.toString = angularDate.toString; -defineApi('Function', [angularGlobal, angularCollection, angularFunction]); - -/** - * @workInProgress - * @ngdoc overview - * @name angular.filter - * @description - * - * Filters are used for formatting data displayed to the user. - * - * The general syntax in templates is as follows: - * - * {{ expression | [ filter_name ] }} - * - * Following is the list of built-in angular filters: - * - * * {@link angular.filter.currency currency} - * * {@link angular.filter.date date} - * * {@link angular.filter.html html} - * * {@link angular.filter.json json} - * * {@link angular.filter.linky linky} - * * {@link angular.filter.lowercase lowercase} - * * {@link angular.filter.number number} - * * {@link angular.filter.uppercase uppercase} - * - * For more information about how angular filters work, and how to create your own filters, see - * {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer - * Guide. - */ - -/** - * @workInProgress - * @ngdoc filter - * @name angular.filter.currency - * @function - * - * @description - * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default - * symbol for current locale is used. - * - * @param {number} amount Input to filter. - * @param {string=} symbol Currency symbol or identifier to be displayed. - * @returns {string} Formatted number. - * - * @css ng-format-negative - * When the value is negative, this css class is applied to the binding making it (by default) red. - * - * @example - - -
- default currency symbol ($): {{amount | currency}}
- custom currency identifier (USD$): {{amount | currency:"USD$"}} -
- - it('should init with 1234.56', function(){ - expect(binding('amount | currency')).toBe('$1,234.56'); - expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); - }); - it('should update', function(){ - input('amount').enter('-1234'); - expect(binding('amount | currency')).toBe('($1,234.00)'); - expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); - expect(element('.doc-example-live .ng-binding').attr('className')). - toMatch(/ng-format-negative/); - }); - -
- */ -angularFilter.currency = function(amount, currencySymbol){ - var formats = this.$service('$locale').NUMBER_FORMATS; - this.$element.toggleClass('ng-format-negative', amount < 0); - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2) - .replace(/\u00A4/g, currencySymbol); -}; - -/** - * @workInProgress - * @ngdoc filter - * @name angular.filter.number - * @function - * - * @description - * Formats a number as text. - * - * If the input is not a number an empty string is returned. - * - * @param {number|string} number Number to format. - * @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. - * - * @example - - - Enter number:
- Default formatting: {{val | number}}
- No fractions: {{val | number:0}}
- Negative number: {{-val | number:4}} -
- - it('should format numbers', function(){ - expect(binding('val | number')).toBe('1,234.568'); - expect(binding('val | number:0')).toBe('1,235'); - expect(binding('-val | number:4')).toBe('-1,234.5679'); - }); - - it('should update', function(){ - input('val').enter('3374.333'); - expect(binding('val | number')).toBe('3,374.333'); - expect(binding('val | number:0')).toBe('3,374'); - expect(binding('-val | number:4')).toBe('-3,374.3330'); - }); - -
- */ -var DECIMAL_SEP = '.'; - -angularFilter.number = function(number, fractionSize) { - if (isNaN(number) || !isFinite(number)) return ''; - var formats = this.$service('$locale').NUMBER_FORMATS; - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, - formats.DECIMAL_SEP, fractionSize); -} + function binaryFn(left, fn, right) { + return function(self) { + return fn(self, left, right); + }; + } -function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - var isNegative = number < 0; - number = Math.abs(number); - var numStr = number + '', - formatedText = '', - parts = []; + function hasTokens () { + return tokens.length > 0; + } - if (numStr.indexOf('e') !== -1) { - formatedText = numStr; - } else { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + function statements() { + var statements = []; + while(true) { + if (tokens.length > 0 && !peek('}', ')', ';', ']')) + statements.push(filterChain()); + if (!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){ + var value; + for ( var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement) + value = statement(self); + } + return value; + }; + } + } + } - //determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); + function _filterChain() { + var left = expression(); + var token; + while(true) { + if ((token = expect('|'))) { + left = binaryFn(left, token.fn, filter()); + } else { + return left; + } } + } - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; + function filter() { + var token = expect(); + var fn = $filter(token.text); + var argsFn = []; + while(true) { + if ((token = expect(':'))) { + argsFn.push(expression()); + } else { + var fnInvoke = function(self, input){ + var args = [input]; + for ( var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](self)); + } + return fn.apply(self, args); + }; + return function() { + return fnInvoke; + }; + } + } + } - var pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; + function expression() { + return assignment(); + } - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (var i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); + function _assignment() { + var left = logicalOR(); + var right; + var token; + if ((token = expect('='))) { + if (!left.assign) { + throwError("implies assignment but [" + + text.substring(0, token.index) + "] can not be assigned to", token); } + right = logicalOR(); + return function(self){ + return left.assign(self, right(self)); + }; + } else { + return left; } + } - for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { - formatedText += groupSep; + function logicalOR() { + var left = logicalAND(); + var token; + while(true) { + if ((token = expect('||'))) { + left = binaryFn(left, token.fn, logicalAND()); + } else { + return left; } - formatedText += whole.charAt(i); } + } - // format fraction part. - while(fraction.length < fractionSize) { - fraction += '0'; + function logicalAND() { + var left = equality(); + var token; + if ((token = expect('&&'))) { + left = binaryFn(left, token.fn, logicalAND()); } - - if (fractionSize) formatedText += decimalSep + fraction.substr(0, fractionSize); + return left; } - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); -} - -function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; + function equality() { + var left = relational(); + var token; + if ((token = expect('==','!='))) { + left = binaryFn(left, token.fn, equality()); + } + return left; } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; -} - - -function dateGetter(name, size, offset, trim) { - return function(date) { - var value = date['get' + name](); - if (offset > 0 || value > -offset) - value += offset; - if (value === 0 && offset == -12 ) value = 12; - return padNumber(value, size, trim); - }; -} -function dateStrGetter(name, shortForm) { - return function(date, formats) { - var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); - - return formats[get][value]; - }; -} - -function timeZoneGetter(date) { - var offset = date.getTimezoneOffset(); - return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2); -} + function relational() { + var left = additive(); + var token; + if ((token = expect('<', '>', '<=', '>='))) { + left = binaryFn(left, token.fn, relational()); + } + return left; + } -function ampmGetter(date, formats) { - return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; -} + function additive() { + var left = multiplicative(); + var token; + while ((token = expect('+','-'))) { + left = binaryFn(left, token.fn, multiplicative()); + } + return left; + } -var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), - MMMM: dateStrGetter('Month'), - MMM: dateStrGetter('Month', true), - MM: dateGetter('Month', 2, 1), - M: dateGetter('Month', 1, 1), - dd: dateGetter('Date', 2), - d: dateGetter('Date', 1), - HH: dateGetter('Hours', 2), - H: dateGetter('Hours', 1), - hh: dateGetter('Hours', 2, -12), - h: dateGetter('Hours', 1, -12), - mm: dateGetter('Minutes', 2), - m: dateGetter('Minutes', 1), - ss: dateGetter('Seconds', 2), - s: dateGetter('Seconds', 1), - EEEE: dateStrGetter('Day'), - EEE: dateStrGetter('Day', true), - a: ampmGetter, - Z: timeZoneGetter -}; + function multiplicative() { + var left = unary(); + var token; + while ((token = expect('*','/','%'))) { + left = binaryFn(left, token.fn, unary()); + } + return left; + } -var GET_TIME_ZONE = /[A-Z]{3}(?![+\-])/; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/ -var OPERA_TOSTRING_PATTERN = /^[\d].*Z$/; -var NUMBER_STRING = /^\d+$/; + function unary() { + 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()); + } else { + return primary(); + } + } + function _functionIdent(fnScope) { + var token = expect(); + var element = token.text.split('.'); + var instance = fnScope; + var key; + for ( var i = 0; i < element.length; i++) { + key = element[i]; + if (instance) + instance = instance[key]; + } + if (!isFunction(instance)) { + throwError("should be a function", token); + } + return instance; + } -/** - * @workInProgress - * @ngdoc filter - * @name angular.filter.date - * @function - * - * @description - * Formats `date` to a string based on the requested `format`. - * - * `format` string can be composed of the following elements: - * - * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) - * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) - * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) - * * `'MMMM'`: Month in year (January-December) - * * `'MMM'`: Month in year (Jan-Dec) - * * `'MM'`: Month in year, padded (01-12) - * * `'M'`: Month in year (1-12) - * * `'dd'`: Day in month, padded (01-31) - * * `'d'`: Day in month (1-31) - * * `'EEEE'`: Day in Week,(Sunday-Saturday) - * * `'EEE'`: Day in Week, (Sun-Sat) - * * `'HH'`: Hour in day, padded (00-23) - * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) - * * `'mm'`: Minute in hour, padded (00-59) - * * `'m'`: Minute in hour (0-59) - * * `'ss'`: Second in minute, padded (00-59) - * * `'s'`: Second in minute (0-59) - * * `'a'`: am/pm marker - * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-1200) - * - * `format` string can also be one of the following predefined - * {@link guide/dev_guide.i18n localizable formats}: - * - * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale - * (e.g. Friday, September 3, 2010) - * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010 - * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) - * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) - * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence - * (e.g. `"h o''clock"`). - * - * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ). - * @param {string=} format Formatting rules (see Description). If not specified, - * Date#toLocaleDateString is used. - * @returns {string} Formatted string or the input if input is not recognized as date/millis. - * - * @example - - - {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
- {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
- {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
-
- - it('should format date', function(){ - expect(binding("1288323623006 | date:'medium'")). - toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/); - expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). - toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); - }); - -
- */ -angularFilter.date = function(date, format) { - var $locale = this.$service('$locale'); - format = $locale.DATETIME_FORMATS[format] || format; - if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = parseInt(date, 10); + function primary() { + var primary; + if (expect('(')) { + var expression = filterChain(); + consume(')'); + primary = expression; + } else if (expect('[')) { + primary = arrayDeclaration(); + } else if (expect('{')) { + primary = object(); } else { - date = angularString.toDate(date); + var token = expect(); + primary = token.fn; + if (!primary) { + throwError("not a primary expression", token); + } } + var next; + while ((next = expect('(', '[', '.'))) { + if (next.text === '(') { + primary = functionCall(primary); + } else if (next.text === '[') { + primary = objectIndex(primary); + } else if (next.text === '.') { + primary = fieldAccess(primary); + } else { + throwError("IMPOSSIBLE"); + } + } + return primary; + } + + function _fieldAccess(object) { + var field = expect().text; + var getter = getterFn(field); + return extend(function(self){ + return getter(object(self)); + }, { + assign:function(self, value){ + return setter(object(self), field, value); + } + }); + } + + function _objectIndex(obj) { + var indexFn = expression(); + consume(']'); + return extend( + function(self){ + var o = obj(self), + i = indexFn(self), + v, p; + + if (!o) return undefined; + v = o[i]; + if (v && v.then) { + p = v; + if (!('$$v' in v)) { + p.$$v = undefined; + p.then(function(val) { p.$$v = val; }); + } + v = v.$$v; + } + return v; + }, { + assign:function(self, value){ + return obj(self)[indexFn(self)] = value; + } + }); } - if (isNumber(date)) { - date = new Date(date); + function _functionCall(fn) { + var argsFn = []; + if (peekToken().text != ')') { + do { + argsFn.push(expression()); + } while (expect(',')); + } + consume(')'); + return function(self){ + var args = []; + for ( var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](self)); + } + var fnPtr = fn(self) || noop; + // IE stupidity! + return fnPtr.apply + ? fnPtr.apply(self, args) + : fnPtr(args[0], args[1], args[2], args[3], args[4]); + }; } - if (!isDate(date)) { - return date; + // This is used with json array declaration + function arrayDeclaration () { + var elementFns = []; + if (peekToken().text != ']') { + do { + elementFns.push(expression()); + } while (expect(',')); + } + consume(']'); + return function(self){ + var array = []; + for ( var i = 0; i < elementFns.length; i++) { + array.push(elementFns[i](self)); + } + return array; + }; } - var text = date.toLocaleDateString(), fn; - if (format && isString(format)) { - text = ''; - var parts = [], match; - while(format) { - match = DATE_FORMATS_SPLIT.exec(format); - if (match) { - parts = concat(parts, match, 1); - format = parts.pop(); - } else { - parts.push(format); - format = null; - } + function object () { + var keyValues = []; + if (peekToken().text != '}') { + do { + var token = expect(), + key = token.string || token.text; + consume(":"); + var value = expression(); + keyValues.push({key:key, value:value}); + } while (expect(',')); } - forEach(parts, function(value){ - fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); - }); + consume('}'); + return function(self){ + var object = {}; + for ( var i = 0; i < keyValues.length; i++) { + var keyValue = keyValues[i]; + var value = keyValue.value(self); + object[keyValue.key] = value; + } + return object; + }; } - return text; -}; - - -/** - * @workInProgress - * @ngdoc filter - * @name angular.filter.json - * @function - * - * @description - * Allows you to convert a JavaScript object into JSON string. - * - * This filter is mostly useful for debugging. When using the double curly {{value}} notation - * the binding is automatically converted to JSON. - * - * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. - * @returns {string} JSON string. - * - * @css ng-monospace Always applied to the encapsulating element. - * - * @example: - - -
{{ {'name':'value'} | json }}
-
- - it('should jsonify filtered objects', function() { - expect(binding('| json')).toBe('{\n "name":"value"}'); - }); - -
- * - */ -angularFilter.json = function(object) { - this.$element.addClass("ng-monospace"); - return toJson(object, true); -}; + function watchDecl () { + var anchorName = expect().text; + consume(":"); + var expressionFn; + if (peekToken().text == '{') { + consume("{"); + expressionFn = statements(); + consume("}"); + } else { + expressionFn = expression(); + } + return function(self) { + return {name:anchorName, fn:expressionFn}; + }; + } +} -/** - * @workInProgress - * @ngdoc filter - * @name angular.filter.lowercase - * @function - * - * @see angular.lowercase - */ -angularFilter.lowercase = lowercase; +////////////////////////////////////////////////// +// Parser helper functions +////////////////////////////////////////////////// +function setter(obj, path, setValue) { + var element = path.split('.'); + for (var i = 0; element.length > 1; i++) { + var key = element.shift(); + var propertyObj = obj[key]; + if (!propertyObj) { + propertyObj = {}; + obj[key] = propertyObj; + } + obj = propertyObj; + } + obj[element.shift()] = setValue; + return setValue; +} /** - * @workInProgress - * @ngdoc filter - * @name angular.filter.uppercase - * @function - * - * @see angular.uppercase + * Return the value accesible from the object by path. Any undefined traversals are ignored + * @param {Object} obj starting object + * @param {string} path path to traverse + * @param {boolean=true} bindFnToScope + * @returns value as accesbile by path */ -angularFilter.uppercase = uppercase; +//TODO(misko): this function needs to be removed +function getter(obj, path, bindFnToScope) { + if (!path) return obj; + var keys = path.split('.'); + var key; + var lastInstance = obj; + var len = keys.length; + for (var i = 0; i < len; i++) { + key = keys[i]; + if (obj) { + obj = (lastInstance = obj)[key]; + } + } + if (!bindFnToScope && isFunction(obj)) { + return bind(lastInstance, obj); + } + return obj; +} -/** - * @workInProgress - * @ngdoc filter - * @name angular.filter.html - * @function - * - * @description - * Prevents the input from getting escaped by angular. By default the input is sanitized and - * inserted into the DOM as is. - * - * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. - * - * If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses - * the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this - * option is strongly discouraged and should be used only if you absolutely trust the input being - * filtered and you can't get the content through the sanitizer. - * - * @param {string} html Html input. - * @param {string=} option If 'unsafe' then do not sanitize the HTML input. - * @returns {string} Sanitized or raw html. - * - * @example - - - Snippet: - - - - - - - - - - - - - - - - - - - - - -
FilterSourceRendered
html filter -
<div ng:bind="snippet | html">
</div>
-
-
-
no filter
<div ng:bind="snippet">
</div>
unsafe html filter
<div ng:bind="snippet | html:'unsafe'">
</div>
-
- - it('should sanitize the html snippet ', function(){ - expect(using('#html-filter').binding('snippet | html')). - toBe('

an html\nclick here\nsnippet

'); - }); +var getterFnCache = {}, + JS_KEYWORDS = {}; - it('should escape snippet without any filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); +forEach( + ("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," + + "delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," + + "if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," + + "protected,public,return,short,static,super,switch,synchronized,this,throw,throws," + + "transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/), + function(key){ JS_KEYWORDS[key] = true;} +); - it('should inline raw snippet if filtered as unsafe', function() { - expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")). - toBe("

an html\n" + - "click here\n" + - "snippet

"); - }); +function getterFn(path) { + var fn = getterFnCache[path]; + if (fn) return fn; - it('should update', function(){ - input('snippet').enter('new text'); - expect(using('#html-filter').binding('snippet | html')).toBe('new text'); - expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>"); - expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new text'); - }); -
-
- */ -angularFilter.html = function(html, option){ - return new HTML(html, option); -}; + var code = 'var l, fn, p;\n'; + forEach(path.split('.'), function(key) { + key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key; + code += 'if(!s) return s;\n' + + 'l=s;\n' + + 's=s' + key + ';\n' + + 'if(typeof s=="function" && !(s instanceof RegExp)) {\n' + + ' fn=function(){ return l' + key + '.apply(l, arguments); };\n' + + ' fn.$unboundFn=s;\n' + + ' s=fn;\n' + + '} else 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'; + }); + code += 'return s;'; + fn = Function('s', code); + fn.toString = function() { return code; }; + return getterFnCache[path] = fn; +} -/** - * @workInProgress - * @ngdoc filter - * @name angular.filter.linky - * @function - * - * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. - * - * @param {string} text Input text. - * @returns {string} Html-linkified text. - * - * @example - - - Snippet: - - - - - - - - - - - - - - - - -
FilterSourceRendered
linky filter -
<div ng:bind="snippet | linky">
</div>
-
-
-
no filter
<div ng:bind="snippet">
</div>
-
- - it('should linkify the snippet with urls', function(){ - expect(using('#linky-filter').binding('snippet | linky')). - toBe('Pretty text with some links:\n' + - 'http://angularjs.org/,\n' + - 'us@somewhere.org,\n' + - 'another@somewhere.org,\n' + - 'and one more: ftp://127.0.0.1/.'); - }); +/////////////////////////////////// - it ('should not linkify snippet without the linky filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("Pretty text with some links:\n" + - "http://angularjs.org/,\n" + - "mailto:us@somewhere.org,\n" + - "another@somewhere.org,\n" + - "and one more: ftp://127.0.0.1/."); - }); +function $ParseProvider() { + var cache = {}; + this.$get = ['$filter', function($filter) { + return function(exp) { + switch(typeof exp) { + case 'string': + return cache.hasOwnProperty(exp) + ? cache[exp] + : cache[exp] = parser(exp, false, $filter); + case 'function': + return exp; + default: + return noop; + } + }; + }]; +} - it('should update', function(){ - input('snippet').enter('new http://link.'); - expect(using('#linky-filter').binding('snippet | linky')). - toBe('new http://link.'); - expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); - }); - -
- */ -var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, - MAILTO_REGEXP = /^mailto:/; -angularFilter.linky = function(text) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var writer = htmlSanitizeWriter(html); - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/mailto then assume mailto - if (match[2] == match[3]) url = 'mailto:' + url; - i = match.index; - writer.chars(raw.substr(0, i)); - writer.start('a', {href:url}); - writer.chars(match[0].replace(MAILTO_REGEXP, '')); - writer.end('a'); - raw = raw.substring(i + match[0].length); - } - writer.chars(raw); - return new HTML(html.join('')); +// This is a special access for JSON parser which bypasses the injector +var parseJson = function(json) { + return parser(json, true); }; /** - * @workInProgress - * @ngdoc overview - * @name angular.formatter + * @ngdoc service + * @name angular.module.ng.$q + * @requires $rootScope + * * @description + * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). * - * Formatters are used for translating data formats between those used for display and those used - * for storage. + * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an + * interface for interacting with an object that represents the result of an action that is + * performed asynchronously, and may or may not be finished at any given point in time. * - * Following is the list of built-in angular formatters: + * From the perspective of dealing with error handling, deferred and promise apis are to + * asynchronous programing what `try`, `catch` and `throw` keywords are to synchronous programing. * - * * {@link angular.formatter.boolean boolean} - Formats user input in boolean format - * * {@link angular.formatter.json json} - Formats user input in JSON format - * * {@link angular.formatter.list list} - Formats user input string as an array - * * {@link angular.formatter.number number} - Formats user input strings as a number - * * {@link angular.formatter.trim trim} - Trims extras spaces from end of user input + *
+ *   // for the purpose of this example let's assume that variables `$q` and `scope` are
+ *   // available in the current lexical scope (they could have been injected or passed in).
+ *
+ *   function asyncGreet(name) {
+ *     var deferred = $q.defer();
+ *
+ *     setTimeout(function() {
+ *       // since this fn executes async in a future turn of the event loop, we need to wrap
+ *       // our code into an $apply call so that the model changes are properly observed.
+ *       scope.$apply(function() {
+ *         if (okToGreet(name)) {
+ *           deferred.resolve('Hello, ' + name + '!');
+ *         } else {
+ *           deferred.reject('Greeting ' + name + ' is not allowed.');
+ *         }
+ *       });
+ *     }, 1000);
+ *
+ *     return deferred.promise;
+ *   }
+ *
+ *   var promise = asyncGreet('Robin Hood');
+ *   promise.then(function(greeting) {
+ *     alert('Success: ' + greeting);
+ *   }, function(reason) {
+ *     alert('Failed: ' + reason);
+ *   );
+ * 
* - * For more information about how angular formatters work, and how to create your own formatters, - * see {@link guide/dev_guide.templates.formatters Understanding Angular Formatters} in the angular - * Developer Guide. - */ - -function formatter(format, parse) {return {'format':format, 'parse':parse || format};} -function toString(obj) { - return (isDefined(obj) && obj !== null) ? "" + obj : obj; -} - -var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/; - -angularFormatter.noop = formatter(identity, identity); - -/** - * @workInProgress - * @ngdoc formatter - * @name angular.formatter.json + * At first it might not be obvious why this extra complexity is worth the trouble. The payoff + * comes in the way of + * [guarantees that promise and deferred apis make](https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md). * - * @description - * Formats the user input as JSON text. + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the + * section on serial or parallel joining of promises. * - * @returns {?string} A JSON string representation of the model. * - * @example - - -
- -
data={{data}}
-
-
- - it('should format json', function(){ - expect(binding('data')).toEqual('data={\n \"name\":\"misko\",\n \"project\":\"angular\"}'); - input('data').enter('{}'); - expect(binding('data')).toEqual('data={\n }'); - }); - -
- */ -angularFormatter.json = formatter(toJson, function(value){ - return fromJson(value || 'null'); -}); - -/** - * @workInProgress - * @ngdoc formatter - * @name angular.formatter.boolean + * # The Deferred API * - * @description - * Use boolean formatter if you wish to store the data as boolean. + * A new instance of deferred is constructed by calling `$q.defer()`. * - * @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`. + * The purpose of the deferred object is to expose the associated Promise instance as well as apis + * that can be used for signaling the successful or unsuccessful completion of the task. * - * @example - - - Enter truthy text: - - -
value={{value}}
-
- - it('should format boolean', function(){ - expect(binding('value')).toEqual('value=false'); - input('value').enter('truthy'); - expect(binding('value')).toEqual('value=true'); - }); - -
- */ -angularFormatter['boolean'] = formatter(toString, toBoolean); - -/** - * @workInProgress - * @ngdoc formatter - * @name angular.formatter.number + * **Methods** * - * @description - * Use number formatter if you wish to convert the user entered string to a number. + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + * constructed via `$q.reject`, the promise will be rejected instead. + * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to + * resolving it with a rejection constructed via `$q.reject`. * - * @returns {number} Number from the parsed string. + * **Properties** * - * @example - - - Enter valid number: - -
value={{value}}
-
- - it('should format numbers', function(){ - expect(binding('value')).toEqual('value=1234'); - input('value').enter('5678'); - expect(binding('value')).toEqual('value=5678'); - }); - -
- */ -angularFormatter.number = formatter(toString, function(obj){ - if (obj == null || NUMBER.exec(obj)) { - return obj===null || obj === '' ? null : 1*obj; - } else { - throw "Not a number"; - } -}); - -/** - * @workInProgress - * @ngdoc formatter - * @name angular.formatter.list + * - promise – `{Promise}` – promise object associated with this deferred. * - * @description - * Use list formatter if you wish to convert the user entered string to an array. * - * @returns {Array} Array parsed from the entered string. + * # The Promise API * - * @example - - - Enter a list of items: - - -
value={{value}}
-
- - it('should format lists', function(){ - expect(binding('value')).toEqual('value=["chair","table"]'); - this.addFutureAction('change to XYZ', function($window, $document, done){ - $document.elements('.doc-example-live :input:last').val(',,a,b,').trigger('change'); - done(); - }); - expect(binding('value')).toEqual('value=["a","b"]'); - }); - -
- */ -angularFormatter.list = formatter( - function(obj) { return obj ? obj.join(", ") : obj; }, - function(value) { - var list = []; - forEach((value || '').split(','), function(item){ - item = trim(item); - if (item) list.push(item); - }); - return list; - } -); - -/** - * @workInProgress - * @ngdoc formatter - * @name angular.formatter.trim + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. * - * @description - * Use trim formatter if you wish to trim extra spaces in user text. + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. * - * @returns {String} Trim excess leading and trailing space. + * **Methods** * - * @example - - - Enter text with leading/trailing spaces: - - -
value={{value|json}}
-
- - it('should format trim', function(){ - expect(binding('value')).toEqual('value="book"'); - this.addFutureAction('change to XYZ', function($window, $document, done){ - $document.elements('.doc-example-live :input:last').val(' text ').trigger('change'); - done(); - }); - expect(binding('value')).toEqual('value="text"'); - }); - -
- */ -angularFormatter.trim = formatter( - function(obj) { return obj ? trim("" + obj) : ""; } -); - -/** - * @workInProgress - * @ngdoc overview - * @name angular.validator - * @description + * - `then(successCallback, errorCallback)` – regardless of when the promise was or will be resolved + * or rejected calls one of the success or error callbacks asynchronously as soon as the result + * is available. The callbacks are called with a single argument the result or rejection reason. * - * Most of the built-in angular validators are used to check user input against defined types or - * patterns. You can easily create your own custom validators as well. + * This method *returns a new promise* which is resolved or rejected via the return value of the + * `successCallback` or `errorCallback`. * - * Following is the list of built-in angular validators: * - * * {@link angular.validator.asynchronous asynchronous()} - Provides asynchronous validation via a - * callback function. - * * {@link angular.validator.date date()} - Checks user input against default date format: - * "MM/DD/YYYY" - * * {@link angular.validator.email email()} - Validates that user input is a well-formed email - * address. - * * {@link angular.validator.integer integer()} - Validates that user input is an integer - * * {@link angular.validator.json json()} - Validates that user input is valid JSON - * * {@link angular.validator.number number()} - Validates that user input is a number - * * {@link angular.validator.phone phone()} - Validates that user input matches the pattern - * "1(123)123-1234" - * * {@link angular.validator.regexp regexp()} - Restricts valid input to a specified regular - * expression pattern - * * {@link angular.validator.url url()} - Validates that user input is a well-formed URL. + * # Chaining promises * - * For more information about how angular validators work, and how to create your own validators, - * see {@link guide/dev_guide.templates.validators Understanding Angular Validators} in the angular - * Developer Guide. + * Because calling `then` api of a promise returns a new derived promise, it is easily possible + * to create a chain of promises: + * + *
+ *   promiseB = promiseA.then(function(result) {
+ *     return result + 1;
+ *   });
+ *
+ *   // promiseB will be resolved immediately after promiseA is resolved and it's value will be
+ *   // the result of promiseA incremented by 1
+ * 
+ * + * It is possible to create chains of any length and since a promise can be resolved with another + * promise (which will defer its resolution further), it is possible to pause/defer resolution of + * the promises at any point in the chain. This makes it possible to implement powerful apis like + * $http's response interceptors. + * + * + * # Differences between Kris Kowal's Q and $q + * + * There are three main differences: + * + * - $q is integrated with the {@link angular.module.ng.$rootScope.Scope} Scope model observation + * mechanism in angular, which means faster propagation of resolution or rejection into your + * models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - $q promises are recognized by the templating engine in angular, which means that in templates + * you can treat promises attached to a scope as if they were the resulting values. + * - Q has many more features that $q, but that comes at a cost of bytes. $q is tiny, but contains + * all the important functionality needed for common async tasks. */ +function $QProvider() { + + this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { + return qFactory(function(callback) { + $rootScope.$evalAsync(callback); + }, $exceptionHandler); + }]; +} + + +/** + * Constructs a promise manager. + * + * @param {function(function)} nextTick Function for executing functions in the next turn. + * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for + * debugging purposes. + * @returns {object} Promise manager. + */ +function qFactory(nextTick, exceptionHandler) { -extend(angularValidator, { - 'noop': function() { return null; }, - - /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.regexp - * @description - * Use regexp validator to restrict the input to any Regular Expression. - * - * @param {string} value value to validate - * @param {string|regexp} expression regular expression. - * @param {string=} msg error message to display. - * @css ng-validation-error - * - * @example - - - - Enter valid SSN: -
- -
-
- - it('should invalidate non ssn', function(){ - var textBox = element('.doc-example-live :input'); - expect(textBox.attr('className')).not().toMatch(/ng-validation-error/); - expect(textBox.val()).toEqual('123-45-6789'); - input('ssn').enter('123-45-67890'); - expect(textBox.attr('className')).toMatch(/ng-validation-error/); - }); - -
- * - */ - 'regexp': function(value, regexp, msg) { - if (!value.match(regexp)) { - return msg || - "Value does not match expected format " + regexp + "."; - } else { - return null; - } - }, - - /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.number - * @description - * Use number validator to restrict the input to numbers with an - * optional range. (See integer for whole numbers validator). - * - * @param {string} value value to validate - * @param {int=} [min=MIN_INT] minimum value. - * @param {int=} [max=MAX_INT] maximum value. - * @css ng-validation-error - * - * @example - - - Enter number:
- Enter number greater than 10:
- Enter number between 100 and 200:
-
- - it('should invalidate number', function(){ - var n1 = element('.doc-example-live :input[name=n1]'); - expect(n1.attr('className')).not().toMatch(/ng-validation-error/); - input('n1').enter('1.x'); - expect(n1.attr('className')).toMatch(/ng-validation-error/); - var n2 = element('.doc-example-live :input[name=n2]'); - expect(n2.attr('className')).not().toMatch(/ng-validation-error/); - input('n2').enter('9'); - expect(n2.attr('className')).toMatch(/ng-validation-error/); - var n3 = element('.doc-example-live :input[name=n3]'); - expect(n3.attr('className')).not().toMatch(/ng-validation-error/); - input('n3').enter('201'); - expect(n3.attr('className')).toMatch(/ng-validation-error/); - }); - -
- * - */ - 'number': function(value, min, max) { - var num = 1 * value; - if (num == value) { - if (typeof min != $undefined && num < min) { - return "Value can not be less than " + min + "."; - } - if (typeof min != $undefined && num > max) { - return "Value can not be greater than " + max + "."; - } - return null; - } else { - return "Not a number"; - } - }, - - /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.integer - * @description - * Use number validator to restrict the input to integers with an - * optional range. (See integer for whole numbers validator). - * - * @param {string} value value to validate - * @param {int=} [min=MIN_INT] minimum value. - * @param {int=} [max=MAX_INT] maximum value. - * @css ng-validation-error - * - * @example - - - Enter integer:
- Enter integer equal or greater than 10:
- Enter integer between 100 and 200 (inclusive):
-
- - it('should invalidate integer', function(){ - var n1 = element('.doc-example-live :input[name=n1]'); - expect(n1.attr('className')).not().toMatch(/ng-validation-error/); - input('n1').enter('1.1'); - expect(n1.attr('className')).toMatch(/ng-validation-error/); - var n2 = element('.doc-example-live :input[name=n2]'); - expect(n2.attr('className')).not().toMatch(/ng-validation-error/); - input('n2').enter('10.1'); - expect(n2.attr('className')).toMatch(/ng-validation-error/); - var n3 = element('.doc-example-live :input[name=n3]'); - expect(n3.attr('className')).not().toMatch(/ng-validation-error/); - input('n3').enter('100.1'); - expect(n3.attr('className')).toMatch(/ng-validation-error/); - }); - -
- */ - 'integer': function(value, min, max) { - var numberError = angularValidator['number'](value, min, max); - if (numberError) return numberError; - if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) { - return "Not a whole number"; - } - return null; - }, - - /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.date - * @description - * Use date validator to restrict the user input to a valid date - * in format in format MM/DD/YYYY. - * - * @param {string} value value to validate - * @css ng-validation-error - * - * @example - - - Enter valid date: - - - - it('should invalidate date', function(){ - var n1 = element('.doc-example-live :input'); - expect(n1.attr('className')).not().toMatch(/ng-validation-error/); - input('text').enter('123/123/123'); - expect(n1.attr('className')).toMatch(/ng-validation-error/); - }); - - - * - */ - 'date': function(value) { - var fields = /^(\d\d?)\/(\d\d?)\/(\d\d\d\d)$/.exec(value); - var date = fields ? new Date(fields[3], fields[1]-1, fields[2]) : 0; - return (date && - date.getFullYear() == fields[3] && - date.getMonth() == fields[1]-1 && - date.getDate() == fields[2]) - ? null - : "Value is not a date. (Expecting format: 12/31/2009)."; - }, - - /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.email - * @description - * Use email validator if you wist to restrict the user input to a valid email. - * - * @param {string} value value to validate - * @css ng-validation-error - * - * @example - - - Enter valid email: - - - - it('should invalidate email', function(){ - var n1 = element('.doc-example-live :input'); - expect(n1.attr('className')).not().toMatch(/ng-validation-error/); - input('text').enter('a@b.c'); - expect(n1.attr('className')).toMatch(/ng-validation-error/); - }); - - - * - */ - 'email': function(value) { - if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) { - return null; - } - return "Email needs to be in username@host.com format."; - }, - /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.phone + * @ngdoc + * @name angular.module.ng.$q#defer + * @methodOf angular.module.ng.$q * @description - * Use phone validator to restrict the input phone numbers. - * - * @param {string} value value to validate - * @css ng-validation-error - * - * @example - - - Enter valid phone number: - - - - it('should invalidate phone', function(){ - var n1 = element('.doc-example-live :input'); - expect(n1.attr('className')).not().toMatch(/ng-validation-error/); - input('text').enter('+12345678'); - expect(n1.attr('className')).toMatch(/ng-validation-error/); - }); - - + * Creates a `Deferred` object which represents a task which will finish in the future. * + * @returns {Deferred} Returns a new instance of deferred. */ - 'phone': function(value) { - if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) { - return null; - } - if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) { - return null; - } - return "Phone number needs to be in 1(987)654-3210 format in North America " + - "or +999 (123) 45678 906 internationally."; - }, + var defer = function() { + var pending = [], + value, deferred; + + deferred = { + + resolve: function(val) { + if (pending) { + var callbacks = pending; + pending = undefined; + value = ref(val); + + if (callbacks.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + value.then(callback[0], callback[1]); + } + }); + } + } + }, - /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.url - * @description - * Use phone validator to restrict the input URLs. - * - * @param {string} value value to validate - * @css ng-validation-error - * - * @example - - - Enter valid URL: - - - - it('should invalidate url', function(){ - var n1 = element('.doc-example-live :input'); - expect(n1.attr('className')).not().toMatch(/ng-validation-error/); - input('text').enter('abc://server/path'); - expect(n1.attr('className')).toMatch(/ng-validation-error/); + + reject: function(reason) { + deferred.resolve(reject(reason)); + }, + + + promise: { + then: function(callback, errback) { + var result = defer(); + + var wrappedCallback = function(value) { + try { + result.resolve((callback || defaultCallback)(value)); + } catch(e) { + exceptionHandler(e); + result.reject(e); + } + }; + + var wrappedErrback = function(reason) { + try { + result.resolve((errback || defaultErrback)(reason)); + } catch(e) { + exceptionHandler(e); + result.reject(e); + } + }; + + if (pending) { + pending.push([wrappedCallback, wrappedErrback]); + } else { + value.then(wrappedCallback, wrappedErrback); + } + + return result.promise; + } + } + }; + + return deferred; + }; + + + var ref = function(value) { + if (value && value.then) return value; + return { + then: function(callback) { + var result = defer(); + nextTick(function() { + result.resolve(callback(value)); }); - - - * - */ - 'url': function(value) { - if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) { - return null; - } - return "URL needs to be in http://server[:port]/path format."; - }, + return result.promise; + } + }; + }; + /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.json + * @ngdoc + * @name angular.module.ng.$q#reject + * @methodOf angular.module.ng.$q * @description - * Use json validator if you wish to restrict the user input to a valid JSON. + * Creates a promise that is resolved as rejected with the specified `reason`. This api should be + * used to forward rejection in a chain of promises. If you are dealing with the last promise in + * a promise chain, you don't need to worry about it. * - * @param {string} value value to validate - * @css ng-validation-error - * - * @example - - - - - - it('should invalidate json', function(){ - var n1 = element('.doc-example-live :input'); - expect(n1.attr('className')).not().toMatch(/ng-validation-error/); - input('json').enter('{name}'); - expect(n1.attr('className')).toMatch(/ng-validation-error/); - }); - - + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * current promise, you have to "rethrow" the error by returning a rejection constructed via + * `reject`. * + *
+   *   promiseB = promiseA.then(function(result) {
+   *     // success: do something and resolve promiseB
+   *     //          with the old or a new result
+   *     return result;
+   *   }, function(reason) {
+   *     // error: handle the error if possible and
+   *     //        resolve promiseB with newPromiseOrValue,
+   *     //        otherwise forward the rejection to promiseB
+   *     if (canHandle(reason)) {
+   *      // handle the error and recover
+   *      return newPromiseOrValue;
+   *     }
+   *     return $q.reject(reason);
+   *   });
+   * 
+ * + * @param {*} reason Constant, message, exception or an object representing the rejection reason. + * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - 'json': function(value) { - try { - fromJson(value); - return null; - } catch (e) { - return e.toString(); - } - }, + var reject = function(reason) { + return { + then: function(callback, errback) { + var result = defer(); + nextTick(function() { + result.resolve(errback(reason)); + }); + return result.promise; + } + }; + }; + /** - * @workInProgress - * @ngdoc validator - * @name angular.validator.asynchronous + * @ngdoc + * @name angular.module.ng.$q#when + * @methodOf angular.module.ng.$q * @description - * Use asynchronous validator if the validation can not be computed - * immediately, but is provided through a callback. The widget - * automatically shows a spinning indicator while the validity of - * the widget is computed. This validator caches the result. - * - * @param {string} value value to validate - * @param {function(inputToValidate,validationDone)} validate function to call to validate the state - * of the input. - * @param {function(data)=} [update=noop] function to call when state of the - * validator changes - * - * @paramDescription - * The `validate` function (specified by you) is called as - * `validate(inputToValidate, validationDone)`: - * - * * `inputToValidate`: value of the input box. - * * `validationDone`: `function(error, data){...}` - * * `error`: error text to display if validation fails - * * `data`: data object to pass to update function - * - * The `update` function is optionally specified by you and is - * called by on input change. Since the - * asynchronous validator caches the results, the update - * function can be called without a call to `validate` - * function. The function is called as `update(data)`: - * - * * `data`: data object as passed from validate function - * - * @css ng-input-indicator-wait, ng-validation-error - * - * @example - - - - This input is validated asynchronously: -
- -
-
- - it('should change color in delayed way', function(){ - var textBox = element('.doc-example-live :input'); - expect(textBox.attr('className')).not().toMatch(/ng-input-indicator-wait/); - expect(textBox.attr('className')).not().toMatch(/ng-validation-error/); - input('text').enter('X'); - expect(textBox.attr('className')).toMatch(/ng-input-indicator-wait/); - sleep(.6); - expect(textBox.attr('className')).not().toMatch(/ng-input-indicator-wait/); - expect(textBox.attr('className')).toMatch(/ng-validation-error/); - }); - -
- * - */ - /* - * cache is attached to the element - * cache: { - * inputs : { - * 'user input': { - * response: server response, - * error: validation error - * }, - * current: 'current input' - * } - * } - * + * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. + * This is useful when you are dealing with on object that might or might not be a promise, or if + * the promise comes from a source that can't be trusted. + * + * @param {*} value Value or a promise + * @returns {Promise} Returns a single promise that will be resolved with an array of values, + * each value coresponding to the promise at the same index in the `promises` array. If any of + * the promises is resolved with a rejection, this resulting promise will be resolved with the + * same rejection. */ - 'asynchronous': function(input, asynchronousFn, updateFn) { - if (!input) return; - var scope = this; - var element = scope.$element; - var cache = element.data('$asyncValidator'); - if (!cache) { - element.data('$asyncValidator', cache = {inputs:{}}); - } + var when = function(value, callback, errback) { + var result = defer(), + done; - cache.current = input; + var wrappedCallback = function(value) { + try { + return (callback || defaultCallback)(value); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; - var inputState = cache.inputs[input], - $invalidWidgets = scope.$service('$invalidWidgets'); + var wrappedErrback = function(reason) { + try { + return (errback || defaultErrback)(reason); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; - if (!inputState) { - cache.inputs[input] = inputState = { inFlight: true }; - $invalidWidgets.markInvalid(scope.$element); - element.addClass('ng-input-indicator-wait'); - asynchronousFn(input, function(error, data) { - inputState.response = data; - inputState.error = error; - inputState.inFlight = false; - if (cache.current == input) { - element.removeClass('ng-input-indicator-wait'); - $invalidWidgets.markValid(element); - } - element.data($$validate)(); + nextTick(function() { + ref(value).then(function(value) { + if (done) return; + done = true; + result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); + }, function(reason) { + if (done) return; + done = true; + result.resolve(wrappedErrback(reason)); }); - } else if (inputState.inFlight) { - // request in flight, mark widget invalid, but don't show it to user - $invalidWidgets.markInvalid(scope.$element); - } else { - (updateFn||noop)(inputState.response); - } - return inputState.error; + }); + + return result.promise; + }; + + + function defaultCallback(value) { + return value; } -}); + + function defaultErrback(reason) { + return reject(reason); + } + + + /** + * @ngdoc + * @name angular.module.ng.$q#all + * @methodOf angular.module.ng.$q + * @description + * Combines multiple promises into a single promise that is resolved when all of the input + * promises are resolved. + * + * @param {Array.} promises An array of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array of values, + * each value coresponding to the promise at the same index in the `promises` array. If any of + * the promises is resolved with a rejection, this resulting promise will be resolved with the + * same rejection. + */ + function all(promises) { + var deferred = defer(), + counter = promises.length, + results = []; + + forEach(promises, function(promise, index) { + promise.then(function(value) { + if (index in results) return; + results[index] = value; + if (!(--counter)) deferred.resolve(results); + }, function(reason) { + if (index in results) return; + deferred.reject(reason); + }); + }); + + return deferred.promise; + } + + return { + defer: defer, + reject: reject, + when: when, + all: all + }; +} /** - * @workInProgress - * @ngdoc service - * @name angular.service.$cookieStore - * @requires $cookies + * @ngdoc object + * @name angular.module.ng.$route + * @requires $location + * @requires $routeParams + * + * @property {Object} current Reference to the current route definition. + * @property {Array.} routes Array of all configured routes. * * @description - * Provides a key-value (string-object) storage, that is backed by session cookies. - * Objects put or retrieved from this storage are automatically serialized or - * deserialized by angular's toJson/fromJson. + * Watches `$location.url()` and tries to map the path to an existing route + * definition. It is used for deep-linking URLs to controllers and views (HTML partials). + * + * The `$route` service is typically used in conjunction with {@link angular.widget.ng:view ng:view} + * widget and the {@link angular.module.ng.$routeParams $routeParams} service. + * * @example - */ -angularServiceInject('$cookieStore', function($store) { + This example shows how changing the URL hash causes the $route + to match a route against the URL, and the [[ng:include]] pulls in the partial. - return { + + + + +
+ Choose: + Moby | + Moby: Ch1 | + Gatsby | + Gatsby: Ch4
+
$location.path() = {{$location.path()}}
+
$route.current.template = {{$route.current.template}}
+
$route.current.params = {{$route.current.params}}
+
$route.current.scope.name = {{$route.current.scope.name}}
+
$routeParams = {{$routeParams}}
+
+ +
+
+ + +
+ */ +function $RouteProvider(){ + this.$get = ['$rootScope', '$location', '$routeParams', + function( $rootScope, $location, $routeParams) { /** - * @workInProgress - * @ngdoc method - * @name angular.service.$cookieStore#get - * @methodOf angular.service.$cookieStore - * + * @ngdoc event + * @name angular.module.ng.$route#$beforeRouteChange + * @eventOf angular.module.ng.$route + * @eventType broadcast on root scope * @description - * Returns the value of given cookie key + * Broadcasted before a route change. + * + * @param {Route} next Future route information. + * @param {Route} current Current route information. + * + * The `Route` object extends the route definition with the following properties. + * + * * `scope` - The instance of the route controller. + * * `params` - The current {@link angular.module.ng.$routeParams params}. * - * @param {string} key Id to use for lookup. - * @returns {Object} Deserialized cookie value. */ - get: function(key) { - return fromJson($store[key]); - }, /** - * @workInProgress - * @ngdoc method - * @name angular.service.$cookieStore#put - * @methodOf angular.service.$cookieStore - * + * @ngdoc event + * @name angular.module.ng.$route#$afterRouteChange + * @eventOf angular.module.ng.$route + * @eventType broadcast on root scope * @description - * Sets a value for given cookie key + * Broadcasted after a route change. + * + * @param {Route} current Current route information. + * @param {Route} previous Previous route information. + * + * The `Route` object extends the route definition with the following properties. + * + * * `scope` - The instance of the route controller. + * * `params` - The current {@link angular.module.ng.$routeParams params}. * - * @param {string} key Id for the `value`. - * @param {Object} value Value to be stored. */ - put: function(key, value) { - $store[key] = toJson(value); - }, /** - * @workInProgress - * @ngdoc method - * @name angular.service.$cookieStore#remove - * @methodOf angular.service.$cookieStore - * + * @ngdoc event + * @name angular.module.ng.$route#$routeUpdate + * @eventOf angular.module.ng.$route + * @eventType emit on the current route scope * @description - * Remove given cookie * - * @param {string} key Id of the key-value pair to delete. + * The `reloadOnSearch` property has been set to false, and we are reusing the same + * instance of the Controller. */ - remove: function(key) { - delete $store[key]; - } - }; -}, ['$cookies']); + var routes = {}, + matcher = switchRouteMatcher, + parentScope = $rootScope, + dirty = 0, + forceReload = false, + $route = { + routes: routes, + + /** + * @ngdoc method + * @name angular.module.ng.$route#parent + * @methodOf angular.module.ng.$route + * + * @param {Scope} [scope=rootScope] Scope to be used as parent for newly created + * `$route.current.scope` scopes. + * + * @description + * Sets a scope to be used as the parent scope for scopes created on route change. If not + * set, defaults to the root scope. + */ + parent: function(scope) { + if (scope) parentScope = scope; + }, -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$cookies - * @requires $browser - * - * @description - * Provides read/write access to browser's cookies. - * - * Only a simple Object is exposed and by adding or removing properties to/from - * this object, new cookies are created/deleted at the end of current $eval. - * - * @example - */ -angularServiceInject('$cookies', function($browser) { - var rootScope = this, - cookies = {}, - lastCookies = {}, - lastBrowserCookies, - runEval = false; + /** + * @ngdoc method + * @name angular.module.ng.$route#when + * @methodOf angular.module.ng.$route + * + * @param {string} path Route path (matched against `$location.hash`) + * @param {Object} route Mapping information to be assigned to `$route.current` on route + * match. + * + * Object properties: + * + * - `controller` – `{function()=}` – Controller fn that should be associated with newly + * created scope. + * - `template` – `{string=}` – path to an html template that should be used by + * {@link angular.widget.ng:view ng:view} or + * {@link angular.widget.ng:include ng:include} widgets. + * - `redirectTo` – {(string|function())=} – value to update + * {@link angular.module.ng.$location $location} path with and trigger route redirection. + * + * If `redirectTo` is a function, it will be called with the following parameters: + * + * - `{Object.}` - route parameters extracted from the current + * `$location.path()` by applying the current route template. + * - `{string}` - current `$location.path()` + * - `{Object}` - current `$location.search()` + * + * The custom `redirectTo` function is expected to return a string which will be used + * to update `$location.path()` and `$location.search()`. + * + * - `[reloadOnSearch=true]` - {boolean=} - reload route when only $location.search() + * changes. + * + * If the option is set to false and url in the browser changes, then + * $routeUpdate event is emited on the current route scope. You can use this event to + * react to {@link angular.module.ng.$routeParams} changes: + * + * function MyCtrl($route, $routeParams) { + * this.$on('$routeUpdate', function() { + * // do stuff with $routeParams + * }); + * } + * + * @returns {Object} route object + * + * @description + * Adds a new route definition to the `$route` service. + */ + when: function(path, route) { + var routeDef = routes[path]; + if (!routeDef) routeDef = routes[path] = {reloadOnSearch: true}; + if (route) extend(routeDef, route); // TODO(im): what the heck? merge two route definitions? + dirty++; + return routeDef; + }, - //creates a poller fn that copies all cookies from the $browser to service & inits the service - $browser.addPollFn(function() { - var currentCookies = $browser.cookies(); - if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl - lastBrowserCookies = currentCookies; - copy(currentCookies, lastCookies); - copy(currentCookies, cookies); - if (runEval) rootScope.$apply(); - } - })(); + /** + * @ngdoc method + * @name angular.module.ng.$route#otherwise + * @methodOf angular.module.ng.$route + * + * @description + * Sets route definition that will be used on route change when no other route definition + * is matched. + * + * @param {Object} params Mapping information to be assigned to `$route.current`. + */ + otherwise: function(params) { + $route.when(null, params); + }, - runEval = true; + /** + * @ngdoc method + * @name angular.module.ng.$route#reload + * @methodOf angular.module.ng.$route + * + * @description + * Causes `$route` service to reload (and recreate the `$route.current` scope) upon the next + * eval even if {@link angular.module.ng.$location $location} hasn't changed. + */ + reload: function() { + dirty++; + forceReload = true; + } + }; - //at the end of each eval, push cookies - //TODO: this should happen before the "delayed" watches fire, because if some cookies are not - // strings or browser refuses to store some cookies, we update the model in the push fn. - this.$watch(push); + $rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute); - return cookies; + return $route; + ///////////////////////////////////////////////////// - /** - * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. - */ - function push(){ - var name, - value, - browserCookies, - updated; + function switchRouteMatcher(on, when) { + // TODO(i): this code is convoluted and inefficient, we should construct the route matching + // regex only once and then reuse it + var regex = '^' + when.replace(/([\.\\\(\)\^\$])/g, "\\$1") + '$', + params = [], + dst = {}; + forEach(when.split(/\W/), function(param) { + if (param) { + var paramRegExp = new RegExp(":" + param + "([\\W])"); + if (regex.match(paramRegExp)) { + regex = regex.replace(paramRegExp, "([^\\/]*)$1"); + params.push(param); + } + } + }); + var match = on.match(new RegExp(regex)); + if (match) { + forEach(params, function(name, index) { + dst[name] = match[index + 1]; + }); + } + return match ? dst : null; + } + + function updateRoute() { + var next = parseRoute(), + last = $route.current, + Controller; - //delete any cookies deleted in $cookies - for (name in lastCookies) { - if (isUndefined(cookies[name])) { - $browser.cookies(name, undefined); + if (next && last && next.$route === last.$route + && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { + next.scope = last.scope; + $route.current = next; + copy(next.params, $routeParams); + last.scope && last.scope.$emit('$routeUpdate'); + } else { + forceReload = false; + $rootScope.$broadcast('$beforeRouteChange', next, last); + last && last.scope && last.scope.$destroy(); + $route.current = next; + if (next) { + if (next.redirectTo) { + if (isString(next.redirectTo)) { + $location.path(interpolate(next.redirectTo, next.params)).search(next.params) + .replace(); + } else { + $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) + .replace(); + } + } else { + copy(next.params, $routeParams); + (Controller = next.controller) && inferInjectionArgs(Controller); + next.scope = parentScope.$new(Controller); + } + } + $rootScope.$broadcast('$afterRouteChange', next, last); } } - //update all cookies updated in $cookies - for(name in cookies) { - value = cookies[name]; - if (!isString(value)) { - if (isDefined(lastCookies[name])) { - cookies[name] = lastCookies[name]; + + /** + * @returns the current active route, by matching it against the URL + */ + function parseRoute() { + // Match a route + var params, match; + forEach(routes, function(route, path) { + if (!match && (params = matcher($location.path(), path))) { + match = inherit(route, { + params: extend({}, $location.search(), params), + pathParams: params}); + match.$route = route; + } + }); + // No route matched; fallback to "otherwise" route + return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); + } + + /** + * @returns interpolation of the redirect path with the parametrs + */ + function interpolate(string, params) { + var result = []; + forEach((string||'').split(':'), function(segment, i) { + if (i == 0) { + result.push(segment); } else { - delete cookies[name]; + var segmentMatch = segment.match(/(\w+)(.*)/); + var key = segmentMatch[1]; + result.push(params[key]); + result.push(segmentMatch[2] || ''); + delete params[key]; } - } else if (value !== lastCookies[name]) { - $browser.cookies(name, value); - updated = true; - } + }); + return result.join(''); } - //verify what was actually stored - if (updated){ - updated = false; - browserCookies = $browser.cookies(); - for (name in cookies) { - if (cookies[name] !== browserCookies[name]) { - //delete or reset all cookies that the browser dropped from $cookies - if (isUndefined(browserCookies[name])) { - delete cookies[name]; - } else { - cookies[name] = browserCookies[name]; - } - updated = true; - } - } - } - } -}, ['$browser']); + }]; +} /** - * @workInProgress - * @ngdoc service - * @name angular.service.$defer - * @requires $browser + * @ngdoc object + * @name angular.module.ng.$routeParams + * @requires $route * * @description - * Delegates to {@link angular.service.$browser $browser.defer}, but wraps the `fn` function - * into a try/catch block and delegates any exceptions to - * {@link angular.service.$exceptionHandler $exceptionHandler} service. + * Current set of route parameters. The route parameters are a combination of the + * {@link angular.module.ng.$location $location} `search()`, and `path()`. The `path` parameters + * are extracted when the {@link angular.module.ng.$route $route} path is matched. * - * In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions. + * In case of parameter name collision, `path` params take precedence over `search` params. * - * @param {function()} fn A function, who's execution should be deferred. - * @param {number=} [delay=0] of milliseconds to defer the function execution. - */ -angularServiceInject('$defer', function($browser) { - var scope = this; - return function(fn, delay) { - $browser.defer(function() { - scope.$apply(fn); - }, delay); - }; -}, ['$browser']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$document - * @requires $window + * The service guarantees that the identity of the `$routeParams` object will remain unchanged + * (but its properties will likely change) even when a route change occurs. * - * @description - * A {@link angular.element jQuery (lite)}-wrapped reference to the browser's `window.document` - * element. + * @example + *
+ *  // Given:
+ *  // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
+ *  // Route: /Chapter/:chapterId/Section/:sectionId
+ *  //
+ *  // Then
+ *  $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
+ * 
*/ -angularServiceInject("$document", function(window){ - return jqLite(window.document); -}, ['$window']); +function $RouteParamsProvider() { + this.$get = valueFn({}); +} /** - * @workInProgress - * @ngdoc service - * @name angular.service.$exceptionHandler - * @requires $log + * DESIGN NOTES * - * @description - * 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. + * The design decisions behind the scope ware heavily favored for speed and memory consumption. * - * In unit tests, if `angular-mocks.js` is loaded, this service is overriden by - * {@link angular.mock.service.$exceptionHandler mock $exceptionHandler} + * The typical use of scope is to watch the expressions, which most of the time return the same + * value as last time so we optimize the operation. * - * @example - */ -var $exceptionHandlerFactory; //reference to be used only in tests -angularServiceInject('$exceptionHandler', $exceptionHandlerFactory = function($log){ - return function(e) { - $log.error(e); - }; -}, ['$log']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$hover - * @requires $browser - * @requires $document + * Closures construction is expensive from speed as well as memory: + * - no closures, instead ups prototypical inheritance for API + * - Internal state needs to be stored on scope directly, which means that private state is + * exposed as $$____ properties * - * @description + * Loop operations are optimized by using while(count--) { ... } + * - this means that in order to keep the same order of execution as addition we have to add + * items to the array at the begging (shift) instead of at the end (push) * - * @example + * Child scopes are created and removed often + * - Using array would be slow since inserts in meddle are expensive so we use linked list + * + * There are few watches then a lot of observers. This is why you don't want the observer to be + * implemented in the same way as watch. Watch requires return of initialization function which + * are expensive to construct. */ -angularServiceInject("$hover", function(browser, document) { - var tooltip, self = this, error, width = 300, arrowWidth = 10, body = jqLite(document[0].body); - browser.hover(function(element, show){ - if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) { - if (!tooltip) { - tooltip = { - callout: jqLite('
'), - arrow: jqLite('
'), - title: jqLite('
'), - content: jqLite('
') - }; - tooltip.callout.append(tooltip.arrow); - tooltip.callout.append(tooltip.title); - tooltip.callout.append(tooltip.content); - body.append(tooltip.callout); - } - var docRect = body[0].getBoundingClientRect(), - elementRect = element[0].getBoundingClientRect(), - leftSpace = docRect.right - elementRect.right - arrowWidth; - tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."); - tooltip.content.text(error); - if (leftSpace < width) { - tooltip.arrow.addClass('ng-arrow-right'); - tooltip.arrow.css({left: (width + 1)+'px'}); - tooltip.callout.css({ - position: 'fixed', - left: (elementRect.left - arrowWidth - width - 4) + "px", - top: (elementRect.top - 3) + "px", - width: width + "px" - }); - } else { - tooltip.arrow.addClass('ng-arrow-left'); - tooltip.callout.css({ - position: 'fixed', - left: (elementRect.right + arrowWidth) + "px", - top: (elementRect.top - 3) + "px", - width: width + "px" - }); - } - } else if (tooltip) { - tooltip.callout.remove(); - tooltip = null; - } - }); -}, ['$browser', '$document'], true); /** - * @workInProgress - * @ngdoc service - * @name angular.service.$invalidWidgets - * + * @ngdoc object + * @name angular.module.ng.$rootScope * @description - * Keeps references to all invalid widgets found during validation. - * Can be queried to find whether there are any invalid widgets currently displayed. * - * @example + * Every application has a single root {@link angular.module.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/dev_guide.scopes developer guide on scopes}. */ -angularServiceInject("$invalidWidgets", function(){ - var invalidWidgets = []; - - - /** Remove an element from the array of invalid widgets */ - invalidWidgets.markValid = function(element){ - var index = indexOf(invalidWidgets, element); - if (index != -1) - invalidWidgets.splice(index, 1); - }; - - - /** Add an element to the array of invalid widgets */ - invalidWidgets.markInvalid = function(element){ - var index = indexOf(invalidWidgets, element); - if (index === -1) - invalidWidgets.push(element); - }; - +function $RootScopeProvider(){ + this.$get = ['$injector', '$exceptionHandler', '$parse', + function( $injector, $exceptionHandler, $parse) { - /** Return count of all invalid widgets that are currently visible */ - invalidWidgets.visible = function() { - var count = 0; - forEach(invalidWidgets, function(widget){ - count = count + (isVisible(widget) ? 1 : 0); - }); - return count; - }; - - - /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */ - this.$watch(function() { - for(var i = 0; i < invalidWidgets.length;) { - var widget = invalidWidgets[i]; - if (isOrphan(widget[0])) { - invalidWidgets.splice(i, 1); - if (widget.dealoc) widget.dealoc(); - } else { - i++; - } + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope + * + * @description + * A root scope can be retrieved using the {@link angular.module.ng.$rootScope $rootScope} key from the + * {@link angular.module.AUTO.$injector $injector}. Child scopes are created using the + * {@link angular.module.ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when + * compiled HTML template is executed.) + * + * Here is a simple scope snippet to show how you can interact with the scope. + *
+        angular.injector(['ng']).invoke(function($rootScope) {
+           var scope = $rootScope.$new();
+           scope.salutation = 'Hello';
+           scope.name = 'World';
+
+           expect(scope.greeting).toEqual(undefined);
+
+           scope.$watch('name', function() {
+             this.greeting = this.salutation + ' ' + this.name + '!';
+           }); // initialize the watch
+
+           expect(scope.greeting).toEqual(undefined);
+           scope.name = 'Misko';
+           // still old value, since watches have not been called yet
+           expect(scope.greeting).toEqual(undefined);
+
+           scope.$digest(); // fire all  the watches
+           expect(scope.greeting).toEqual('Hello Misko!');
+        });
+     * 
+ * + * # Inheritance + * A scope can inherit from a parent scope, as in this example: + *
+         var parent = $rootScope;
+         var child = parent.$new();
+
+         parent.salutation = "Hello";
+         child.name = "World";
+         expect(child.salutation).toEqual('Hello');
+
+         child.salutation = "Welcome";
+         expect(child.salutation).toEqual('Welcome');
+         expect(parent.salutation).toEqual('Hello');
+     * 
+ * + * # Dependency Injection + * See {@link guide/dev_guide.di dependency injection}. + * + * + * @param {Object.=} providers Map of service factory which need to be provided + * for the current scope. Defaults to {@link angular.module.ng}. + * @param {Object.=} instanceCache Provides pre-instantiated services which should + * append/override services provided by `providers`. This is handy when unit-testing and having + * the need to override a default service. + * @returns {Object} Newly created scope. + * + */ + function Scope() { + this.$id = nextUid(); + this.$$phase = this.$parent = this.$$watchers = + this.$$nextSibling = this.$$prevSibling = + this.$$childHead = this.$$childTail = null; + this.$destructor = noop; + this['this'] = this.$root = this; + this.$$asyncQueue = []; + this.$$listeners = {}; } - }); - - - /** - * Traverses DOM element's (widget's) parents and considers the element to be an orphan if one of - * it's parents isn't the current window.document. - */ - function isOrphan(widget) { - if (widget == window.document) return false; - var parent = widget.parentNode; - return !parent || isOrphan(parent); - } - - return invalidWidgets; -}); - -var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, - HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/, - DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$location - * @requires $browser - * - * @property {string} href The full URL of the current location. - * @property {string} protocol The protocol part of the URL (e.g. http or https). - * @property {string} host The host name, ip address or FQDN of the current location. - * @property {number} port The port number of the current location (e.g. 80, 443, 8080). - * @property {string} path The path of the current location (e.g. /myapp/inbox). - * @property {Object.} search Map of query parameters (e.g. {user:"foo", page:23}). - * @property {string} hash The fragment part of the URL of the current location (e.g. #foo). - * @property {string} hashPath Similar to `path`, but located in the `hash` fragment - * (e.g. ../foo#/some/path => /some/path). - * @property {Object.} hashSearch Similar to `search` but located in `hash` - * fragment (e.g. .../foo#/some/path?hashQuery=param => {hashQuery: "param"}). - * - * @description - * Parses the browser location url and makes it available to your application. - * Any changes to the url are reflected into `$location` service and changes to - * `$location` are reflected in the browser location url. - * - * Notice that using browser's forward/back buttons changes the $location. - * - * @example - - -
- test hash| - reset hash
- -
$location = {{$location}}
-
-
- - it('should initialize the input field', function() { - expect(using('.doc-example-live').input('$location.hash').val()). - toBe('!/api/angular.service.$location'); - }); + /** + * @ngdoc property + * @name angular.module.ng.$rootScope.Scope#$id + * @propertyOf angular.module.ng.$rootScope.Scope + * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for + * debugging. + */ - it('should bind $location.hash to the input field', function() { - using('.doc-example-live').input('$location.hash').enter('foo'); - expect(browser().location().hash()).toBe('foo'); - }); + Scope.prototype = { + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$new + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Creates a new child {@link angular.module.ng.$rootScope.Scope scope}. The new scope can optionally behave as a + * controller. The parent scope will propagate the {@link angular.module.ng.$rootScope.Scope#$digest $digest()} and + * {@link angular.module.ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the scope + * hierarchy using {@link angular.module.ng.$rootScope.Scope#$destroy $destroy()}. + * + * {@link angular.module.ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is desired for + * the scope and its child scopes to be permanently detached from the parent and thus stop + * participating in model change detection and listener notification by invoking. + * + * @param {function()=} Class Constructor function which the scope should be applied to the scope. + * @param {...*} curryArguments Any additional arguments which are curried into the constructor. + * See {@link guide/dev_guide.di dependency injection}. + * @returns {Object} The newly created child scope. + * + */ + $new: function(Class, curryArguments) { + var 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 + // name in the debugger. + var child; + Child.prototype = this; + child = new Child(); + child['this'] = child; + child.$$listeners = {}; + child.$parent = this; + child.$id = nextUid(); + child.$$asyncQueue = []; + child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; + child.$$prevSibling = this.$$childTail; + if (this.$$childHead) { + this.$$childTail.$$nextSibling = child; + this.$$childTail = child; + } else { + this.$$childHead = this.$$childTail = child; + } + // short circuit if we have no class + if (Class) { + // can't use forEach, we need speed! + var ClassPrototype = Class.prototype; + for(var key in ClassPrototype) { + child[key] = bind(child, ClassPrototype[key]); + } + $injector.invoke(Class, child, curryArguments); + } + return child; + }, - it('should set the hash to a test string with test link is presed', function() { - using('.doc-example-live').element('#ex-test').click(); - expect(using('.doc-example-live').input('$location.hash').val()). - toBe('myPath?name=misko'); - }); + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$watch + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Registers a `listener` callback to be executed whenever the `watchExpression` changes. + * + * - The `watchExpression` is called on every call to {@link angular.module.ng.$rootScope.Scope#$digest $digest()} and + * should return the value which will be watched. (Since {@link angular.module.ng.$rootScope.Scope#$digest $digest()} + * reruns when it detects changes the `watchExpression` can execute multiple times per + * {@link angular.module.ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) + * - The `listener` is called only when the value from the current `watchExpression` and the + * previous call to `watchExpression' are not equal (with the exception of the initial run + * see below). The inequality is determined according to + * {@link angular.equals} function. To save the value of the object for later comparison + * {@link angular.copy} function is used. It also means that watching complex options will + * have adverse memory and performance implications. + * - The watch `listener` may change the model, which may trigger other `listener`s to fire. This + * is achieved by rerunning the watchers until no changes are detected. The rerun iteration + * limit is 100 to prevent infinity loop deadlock. + * + * + * If you want to be notified whenever {@link angular.module.ng.$rootScope.Scope#$digest $digest} is called, + * you can register an `watchExpression` function with no `listener`. (Since `watchExpression`, + * can execute multiple times per {@link angular.module.ng.$rootScope.Scope#$digest $digest} cycle when a change is + * detected, be prepared for multiple calls to your listener.) + * + * After a watcher is registered with the scope, the `listener` fn is called asynchronously + * (via {@link angular.module.ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the + * watcher. In rare cases, this is undesirable because the listener is called when the result + * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you + * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the + * listener was called due to initialization. + * + * + * # Example +
+           var scope = angular.module.ng.$rootScope.Scope();
+           scope.name = 'misko';
+           scope.counter = 0;
+
+           expect(scope.counter).toEqual(0);
+           scope.$watch('name', function(scope, newValue, oldValue) { counter = counter + 1; });
+           expect(scope.counter).toEqual(0);
+
+           scope.$digest();
+           // no variable change
+           expect(scope.counter).toEqual(0);
+
+           scope.name = 'adam';
+           scope.$digest();
+           expect(scope.counter).toEqual(1);
+         
+ * + * + * + * @param {(function()|string)} watchExpression Expression that is evaluated on each + * {@link angular.module.ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers a + * call to the `listener`. + * + * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} + * - `function(scope)`: called with current `scope` as a parameter. + * @param {(function()|string)=} listener Callback called whenever the return value of + * the `watchExpression` changes. + * + * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} + * - `function(scope, newValue, oldValue)`: called with current `scope` an previous and + * current values as parameters. + * @returns {function()} Returns a deregistration function for this listener. + */ + $watch: function(watchExp, listener) { + var scope = this, + get = compileToFn(watchExp, 'watch'), + listenFn = compileToFn(listener || noop, 'listener'), + array = scope.$$watchers, + watcher = { + fn: listenFn, + last: initWatchVal, + get: get, + exp: watchExp + }; - it('should reset $location when reset link is pressed', function() { - using('.doc-example-live').input('$location.hash').enter('foo'); - using('.doc-example-live').element('#ex-reset').click(); - expect(using('.doc-example-live').input('$location.hash').val()). - toBe('!/api/angular.service.$location'); - }); + if (!array) { + array = scope.$$watchers = []; + } + // we use unshift since we use a while loop in $digest for speed. + // the while loop reads in reverse order. + array.unshift(watcher); -
-
- */ -angularServiceInject("$location", function($browser) { - var location = {update: update, updateHash: updateHash}; - var lastLocation = {}; // last state since last update(). + return function() { + arrayRemove(array, watcher); + }; + }, - $browser.onHashChange(bind(this, this.$apply, function() { //register - update($browser.getUrl()); - }))(); //initialize + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$digest + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Process all of the {@link angular.module.ng.$rootScope.Scope#$watch watchers} of the current scope and its children. + * Because a {@link angular.module.ng.$rootScope.Scope#$watch watcher}'s listener can change the model, the + * `$digest()` keeps calling the {@link angular.module.ng.$rootScope.Scope#$watch watchers} until no more listeners are + * firing. This means that it is possible to get into an infinite loop. This function will throw + * `'Maximum iteration limit exceeded.'` if the number of iterations exceeds 100. + * + * Usually you don't call `$digest()` directly in + * {@link angular.directive.ng:controller controllers} or in {@link angular.directive directives}. + * Instead a call to {@link angular.module.ng.$rootScope.Scope#$apply $apply()} (typically from within a + * {@link angular.directive directive}) will force a `$digest()`. + * + * If you want to be notified whenever `$digest()` is called, + * you can register a `watchExpression` function with {@link angular.module.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. + * + * # Example +
+           var scope = ...;
+           scope.name = 'misko';
+           scope.counter = 0;
+
+           expect(scope.counter).toEqual(0);
+           scope.$watch('name', function(scope, newValue, oldValue) {
+             counter = counter + 1;
+           });
+           expect(scope.counter).toEqual(0);
+
+           scope.$digest();
+           // no variable change
+           expect(scope.counter).toEqual(0);
+
+           scope.name = 'adam';
+           scope.$digest();
+           expect(scope.counter).toEqual(1);
+         
+ * + */ + $digest: function() { + var watch, value, last, + watchers, + asyncQueue, + length, + dirty, ttl = 100, + next, current, target = this, + watchLog = [], + logIdx, logMsg; + + flagPhase(target, '$digest'); + + do { + dirty = false; + current = target; + do { + asyncQueue = current.$$asyncQueue; + while(asyncQueue.length) { + try { + current.$eval(asyncQueue.shift()); + } catch (e) { + $exceptionHandler(e); + } + } + if ((watchers = current.$$watchers)) { + // process our watches + length = watchers.length; + while (length--) { + try { + watch = watchers[length]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if ((value = watch.get(current)) !== (last = watch.last) && !equals(value, last)) { + dirty = true; + watch.last = copy(value); + watch.fn(current, value, ((last === initWatchVal) ? value : last)); + if (ttl < 5) { + logIdx = 4 - ttl; + if (!watchLog[logIdx]) watchLog[logIdx] = []; + logMsg = (isFunction(watch.exp)) + ? 'fn: ' + (watch.exp.name || watch.exp.toString()) + : watch.exp; + logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); + watchLog[logIdx].push(logMsg); + } + } + } catch (e) { + $exceptionHandler(e); + } + } + } - this.$watch(sync); + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $broadcast + if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { + while(current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } while ((current = next)); - return location; + if(dirty && !(ttl--)) { + throw Error('100 $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); + } + } while (dirty || asyncQueue.length); - // PUBLIC METHODS + this.$root.$$phase = null; + }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$location#update - * @methodOf angular.service.$location - * - * @description - * Updates the location object. - * Does not immediately update the browser - * Browser is updated at the end of $digest() - * - * Does not immediately update the browser. Instead the browser is updated at the end of $eval() - * cycle. - * - *
-       $location.update('http://www.angularjs.org/path#hash?search=x');
-       $location.update({host: 'www.google.com', protocol: 'https'});
-       $location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}});
-     
- * - * @param {string|Object} href Full href as a string or object with properties - */ - function update(href) { - if (isString(href)) { - extend(location, parseHref(href)); - } else { - if (isDefined(href.hash)) { - extend(href, isString(href.hash) ? parseHash(href.hash) : href.hash); - } + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$destroy + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Remove the current scope (and all of its children) from the parent scope. Removal implies + * that calls to {@link angular.module.ng.$rootScope.Scope#$digest $digest()} will no longer propagate to the current + * scope and its children. Removal also implies that the current scope is eligible for garbage + * collection. + * + * The destructing scope emits an `$destroy` {@link angular.module.ng.$rootScope.Scope#$emit event}. + * + * The `$destroy()` is usually used by directives such as + * {@link angular.widget.@ng:repeat ng:repeat} for managing the unrolling of the loop. + * + */ + $destroy: function() { + if (this.$root == this) return; // we can't remove the root node; + this.$emit('$destroy'); + var parent = this.$parent; + + if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; + if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; + if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + }, - extend(location, href); + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$eval + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Executes the `expression` on the current scope returning the result. Any exceptions in the + * expression are propagated (uncaught). This is useful when evaluating engular expressions. + * + * # Example +
+           var scope = angular.module.ng.$rootScope.Scope();
+           scope.a = 1;
+           scope.b = 2;
+
+           expect(scope.$eval('a+b')).toEqual(3);
+           expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
+         
+ * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $eval: function(expr) { + return $parse(expr)(this); + }, - if (isDefined(href.hashPath || href.hashSearch)) { - location.hash = composeHash(location); - } + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$evalAsync + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Executes the expression on the current scope at a later point in time. + * + * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that: + * + * - it will execute in the current script execution context (before any DOM rendering). + * - at least one {@link angular.module.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 angular.module.ng.$exceptionHandler $exceptionHandler} service. + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + */ + $evalAsync: function(expr) { + this.$$asyncQueue.push(expr); + }, - location.href = composeHref(location); - } - $browser.setUrl(location.href); - copy(location, lastLocation); - } + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$apply + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @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 + * of {@link angular.module.ng.$exceptionHandler exception handling}, + * {@link angular.module.ng.$rootScope.Scope#$digest executing watches}. + * + * ## Life cycle + * + * # Pseudo-Code of `$apply()` + function $apply(expr) { + try { + return $eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + $root.$digest(); + } + } + * + * + * Scope's `$apply()` method transitions through the following stages: + * + * 1. The {@link guide/dev_guide.expressions expression} is executed using the + * {@link angular.module.ng.$rootScope.Scope#$eval $eval()} method. + * 2. Any exceptions from the execution of the expression are forwarded to the + * {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. + * 3. The {@link angular.module.ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the expression + * was executed using the {@link angular.module.ng.$rootScope.Scope#$digest $digest()} method. + * + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $apply: function(expr) { + try { + flagPhase(this, '$apply'); + return this.$eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + this.$root.$$phase = null; + this.$root.$digest(); + } + }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$location#updateHash - * @methodOf angular.service.$location - * - * @description - * Updates the hash fragment part of the url. - * - * @see update() - * - *
-       scope.$location.updateHash('/hp')
-         ==> update({hashPath: '/hp'})
-       scope.$location.updateHash({a: true, b: 'val'})
-         ==> update({hashSearch: {a: true, b: 'val'}})
-       scope.$location.updateHash('/hp', {a: true})
-         ==> update({hashPath: '/hp', hashSearch: {a: true}})
-     
- * - * @param {string|Object} path A hashPath or hashSearch object - * @param {Object=} search A hashSearch object - */ - function updateHash(path, search) { - var hash = {}; + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$on + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Listen on events of a given type. See {@link angular.module.ng.$rootScope.Scope#$emit $emit} for discussion of + * event life cycle. + * + * @param {string} name Event name to listen on. + * @param {function(event)} listener Function to call when the event is emitted. + * @returns {function()} Returns a deregistration function for this listener. + * + * The event listener function format is: `function(event)`. The `event` object passed into the + * listener has the following attributes + * - `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. + * - `cancel` - {function=}: calling `cancel` function will cancel further event propagation + * (available only for events that were `$emit`-ed). + */ + $on: function(name, listener) { + var namedListeners = this.$$listeners[name]; + if (!namedListeners) { + this.$$listeners[name] = namedListeners = []; + } + namedListeners.push(listener); - if (isString(path)) { - hash.hashPath = path; - hash.hashSearch = search || {}; - } else - hash.hashSearch = path; + return function() { + arrayRemove(namedListeners, listener); + }; + }, - hash.hash = composeHash(hash); - update({hash: hash}); - } + /** + * @ngdoc function + * @name angular.module.ng.$rootScope.Scope#$emit + * @methodOf angular.module.ng.$rootScope.Scope + * @function + * + * @description + * Dispatches an event `name` upwards through the scope hierarchy notifying the + * registered {@link angular.module.ng.$rootScope.Scope#$on} listeners. + * + * The event life cycle starts at the scope on which `$emit` was called. All + * {@link angular.module.ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get notified. + * Afterwards, the event traverses upwards toward the root scope and calls all registered + * listeners along the way. The event will stop propagating if one of the listeners cancels it. + * + * Any exception emmited from the {@link angular.module.ng.$rootScope.Scope#$on listeners} will be passed + * onto the {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to emit. + * @param {...*} args Optional set of arguments which will be passed onto the event listeners. + */ + $emit: function(name, args) { + var empty = [], + namedListeners, + canceled = false, + scope = this, + event = { + name: name, + targetScope: scope, + cancel: function() {canceled = true;} + }, + listenerArgs = concat([event], arguments, 1), + i, length; + + do { + namedListeners = scope.$$listeners[name] || empty; + event.currentScope = scope; + for (i=0, length=namedListeners.length; i - -

Reload this page with open console, enter text and hit the log button...

- Message: - - - - - -
- - - + * @description + * This is very simple implementation of testing browser's features. */ -var $logFactory; //reference to be used only in tests -angularServiceInject("$log", $logFactory = function($window){ - return { - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$log#log - * @methodOf angular.service.$log - * - * @description - * Write a log message - */ - log: consoleLog('log'), - - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$log#warn - * @methodOf angular.service.$log - * - * @description - * Write a warning message - */ - warn: consoleLog('warn'), - - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$log#info - * @methodOf angular.service.$log - * - * @description - * Write an information message - */ - info: consoleLog('info'), - - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$log#error - * @methodOf angular.service.$log - * - * @description - * Write an error message - */ - error: consoleLog('error') - }; +function $SnifferProvider(){ + this.$get = ['$window', function($window){ + if ($window.Modernizr) return $window.Modernizr; - function consoleLog(type) { - var console = $window.console || {}; - var logFn = console[type] || console.log || noop; - if (logFn.apply) { - return function(){ - var args = []; - forEach(arguments, function(arg){ - args.push(formatError(arg)); - }); - return logFn.apply(console, args); - }; - } else { - // we are IE, in which case there is nothing we can do - return logFn; - } - } -}, ['$window']); + return { + history: !!($window.history && $window.history.pushState), + hashchange: 'onhashchange' in $window && + // IE8 compatible mode lies + (!$window.document.documentMode || $window.document.documentMode > 7) + }; + }]; +} /** - * @workInProgress - * @ngdoc service - * @name angular.service.$resource - * @requires $xhr.cache + * @ngdoc object + * @name angular.module.ng.$window * * @description - * A factory which creates a resource object that lets you interact with - * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. - * - * The returned resource object has action methods which provide high-level behaviors without - * the need to interact with the low level {@link angular.service.$xhr $xhr} service or - * raw XMLHttpRequest. - * - * @param {string} url A parameterized URL template with parameters prefixed by `:` as in - * `/user/:username`. - * - * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in - * `actions` methods. - * - * Each key value in the parameter object is first bound to url template if present and then any - * excess keys are appended to the url search query after the `?`. - * - * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in - * URL `/path/greet?salutation=Hello`. - * - * If the parameter value is prefixed with `@` then the value of that parameter is extracted from - * the data object (useful for non-GET operations). - * - * @param {Object.=} actions Hash with declaration of custom action that should extend the - * default set of resource actions. The declaration should be created in the following format: - * - * {action1: {method:?, params:?, isArray:?, verifyCache:?}, - * action2: {method:?, params:?, isArray:?, verifyCache:?}, - * ...} - * - * Where: - * - * - `action` – {string} – The name of action. This name becomes the name of the method on your - * resource object. - * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, - * and `JSON` (also known as JSONP). - * - `params` – {object=} – Optional set of pre-bound parameters for this action. - * - isArray – {boolean=} – If true then the returned object for this action is an array, see - * `returns` section. - * - verifyCache – {boolean=} – If true then whenever cache hit occurs, the object is returned and - * an async request will be made to the server and the resources as well as the cache will be - * updated when the response is received. - * - * @returns {Object} A resource "class" object with methods for the default set of resource actions - * optionally extended with custom `actions`. The default set contains these actions: - * - * { 'get': {method:'GET'}, - * 'save': {method:'POST'}, - * 'query': {method:'GET', isArray:true}, - * 'remove': {method:'DELETE'}, - * 'delete': {method:'DELETE'} }; - * - * Calling these methods invoke an {@link angular.service.$xhr} with the specified http method, - * destination and parameters. When the data is returned from the server then the object is an - * instance of the resource class `save`, `remove` and `delete` actions are available on it as - * methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, - * update, delete) on server-side data like this: - *
-        var User = $resource('/user/:userId', {userId:'@id'});
-        var user = User.get({userId:123}, function(){
-          user.abc = true;
-          user.$save();
-        });
-     
- * - * It is important to realize that invoking a $resource object method immediately returns an - * empty reference (object or array depending on `isArray`). Once the data is returned from the - * server the existing reference is populated with the actual data. This is a useful trick since - * usually the resource is assigned to a model which is then rendered by the view. Having an empty - * object results in no rendering, once the data arrives from the server then the object is - * populated with the data and the view automatically re-renders itself showing the new data. This - * means that in most case one never has to write a callback function for the action methods. - * - * The action methods on the class object or instance object can be invoked with the following - * parameters: - * - * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` - * - non-GET "class" actions: `Resource.action(postData, [parameters], [success], [error])` - * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * A reference to the browser's `window` object. While `window` + * is globally available in JavaScript, it causes testability problems, because + * it is a global variable. In angular we always refer to it through the + * `$window` service, so it may be overriden, removed or mocked for testing. * + * All expressions are evaluated with respect to current scope so they don't + * suffer from window globality. * * @example - * - * # Credit card resource - * - *
-     // Define CreditCard class
-     var CreditCard = $resource('/user/:userId/card/:cardId',
-      {userId:123, cardId:'@id'}, {
-       charge: {method:'POST', params:{charge:true}}
-      });
-
-     // We can retrieve a collection from the server
-     var cards = CreditCard.query();
-     // GET: /user/123/card
-     // server returns: [ {id:456, number:'1234', name:'Smith'} ];
-
-     var card = cards[0];
-     // each item is an instance of CreditCard
-     expect(card instanceof CreditCard).toEqual(true);
-     card.name = "J. Smith";
-     // non GET methods are mapped onto the instances
-     card.$save();
-     // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
-     // server returns: {id:456, number:'1234', name: 'J. Smith'};
-
-     // our custom method is mapped as well.
-     card.$charge({amount:9.99});
-     // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
-     // server returns: {id:456, number:'1234', name: 'J. Smith'};
+   
+     
+       
+       
+     
+     
+     
+   
+ */
+function $WindowProvider(){
+  this.$get = valueFn(window);
+}
 
-     // we can create an instance as well
-     var newCard = new CreditCard({number:'0123'});
-     newCard.name = "Mike Smith";
-     newCard.$save();
-     // POST: /user/123/card {number:'0123', name:'Mike Smith'}
-     // server returns: {id:789, number:'01234', name: 'Mike Smith'};
-     expect(newCard.id).toEqual(789);
- * 
- * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. +/** + * Parse headers into key value object * - * Calling these methods invoke `$xhr` on the `url` template with the given `method` and `params`. - * When the data is returned from the server then the object is an instance of the resource type and - * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD - * operations (create, read, update, delete) on server-side data. + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ +function parseHeaders(headers) { + var parsed = {}, key, val, i; -
-     var User = $resource('/user/:userId', {userId:'@id'});
-     var user = User.get({userId:123}, function(){
-       user.abc = true;
-       user.$save();
-     });
-   
- * - * It's worth noting that the success callback for `get`, `query` and other method gets passed - * in the response that came from the server, so one could rewrite the above example as: - * -
-     var User = $resource('/user/:userId', {userId:'@id'});
-     User.get({userId:123}, function(u){
-       u.abc = true;
-       u.$save();
-     });
-   
+ if (!headers) return parsed; - * # Buzz client + forEach(headers.split('\n'), function(line) { + i = line.indexOf(':'); + key = lowercase(trim(line.substr(0, i))); + val = trim(line.substr(i + 1)); - Let's look at what a buzz client created with the `$resource` service looks like: - - - + return parsed; +} -
- - -
-
-

- - {{item.actor.name}} - Expand replies: {{item.links.replies[0].count}} -

- {{item.object.content | html}} -
- - {{reply.actor.name}}: {{reply.content | html}} -
-
-
-
- - -
- */ -angularServiceInject('$resource', function($xhr){ - var resource = new ResourceFactory($xhr); - return bind(resource, resource.route); -}, ['$xhr.cache']); /** - * @workInProgress - * @ngdoc service - * @name angular.service.$route - * @requires $location - * @requires $routeParams - * - * @property {Object} current Reference to the current route definition. - * @property {Array.} routes Array of all configured routes. + * Returns a function that provides access to parsed headers. * - * @description - * Watches `$location.hashPath` and tries to map the hash to an existing route - * definition. It is used for deep-linking URLs to controllers and views (HTML partials). + * Headers are lazy parsed when first requested. + * @see parseHeaders * - * The `$route` service is typically used in conjunction with {@link angular.widget.ng:view ng:view} - * widget and the {@link angular.service.$routeParams $routeParams} service. + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: * - * @example - This example shows how changing the URL hash causes the $route - to match a route against the URL, and the [[ng:include]] pulls in the partial. - Try changing the URL in the input box to see changes. + * - if called with single an argument returns a single header value or null + * - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headers) { + var headersObj = isObject(headers) ? headers : undefined; - - - -
- Choose: - Moby | - Moby: Ch1 | - Gatsby | - Gatsby: Ch4
- $location.hashPath: -
$route.current.template = {{$route.current.template}}
-
$route.current.params = {{$route.current.params}}
-
$route.current.scope.name = {{$route.current.scope.name}}
-
$routeParams = {{$routeParams}}
-
- -
-
- - -
+/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function(string=)} headers Http headers getter fn. + * @param {(function|Array.)} fns Function or an array of functions. + * @returns {*} Transformed data. */ -angularServiceInject('$route', function($location, $routeParams) { - /** - * @workInProgress - * @ngdoc event - * @name angular.service.$route#$beforeRouteChange - * @eventOf angular.service.$route - * @eventType broadcast on root scope - * @description - * Broadcasted before a route change. - * - * @param {Route} next Future route information. - * @param {Route} current Current route information. - * - * The `Route` object extends the route definition with the following properties. - * - * * `scope` - The instance of the route controller. - * * `params` - The current {@link angular.service.$routeParams params}. - * - */ +function transformData(data, headers, fns) { + if (isFunction(fns)) + return fns(data, headers); - /** - * @workInProgress - * @ngdoc event - * @name angular.service.$route#$afterRouteChange - * @eventOf angular.service.$route - * @eventType broadcast on root scope - * @description - * Broadcasted after a route change. - * - * @param {Route} current Current route information. - * @param {Route} previous Previous route information. - * - * The `Route` object extends the route definition with the following properties. - * - * * `scope` - The instance of the route controller. - * * `params` - The current {@link angular.service.$routeParams params}. - * - */ + forEach(fns, function(fn) { + data = fn(data, headers); + }); - /** - * @workInProgress - * @ngdoc event - * @name angular.service.$route#$routeUpdate - * @eventOf angular.service.$route - * @eventType emit on the current route scope - * @description - * - * The `reloadOnSearch` property has been set to false, and we are reusing the same - * instance of the Controller. - */ + return data; +} - var routes = {}, - matcher = switchRouteMatcher, - parentScope = this, - rootScope = this, - dirty = 0, - allowReload = true, - $route = { - routes: routes, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$route#parent - * @methodOf angular.service.$route - * - * @param {Scope} [scope=rootScope] Scope to be used as parent for newly created - * `$route.current.scope` scopes. - * - * @description - * Sets a scope to be used as the parent scope for scopes created on route change. If not - * set, defaults to the root scope. - */ - parent: function(scope) { - if (scope) parentScope = scope; - }, +function isSuccess(status) { + return 200 <= status && status < 300; +} - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$route#when - * @methodOf angular.service.$route - * - * @param {string} path Route path (matched against `$location.hash`) - * @param {Object} route Mapping information to be assigned to `$route.current` on route - * match. - * - * Object properties: - * - * - `controller` – `{function()=}` – Controller fn that should be associated with newly - * created scope. - * - `template` – `{string=}` – path to an html template that should be used by - * {@link angular.widget.ng:view ng:view} or - * {@link angular.widget.ng:include ng:include} widgets. - * - `redirectTo` – {(string|function())=} – value to update - * {@link angular.service.$location $location} hash with and trigger route redirection. - * - * If `redirectTo` is a function, it will be called with the following parameters: - * - * - `{Object.}` - route parameters extracted from the current - * `$location.hashPath` by applying the current route template. - * - `{string}` - current `$location.hash` - * - `{string}` - current `$location.hashPath` - * - `{string}` - current `$location.hashSearch` - * - * The custom `redirectTo` function is expected to return a string which will be used - * to update `$location.hash`. - * - * - `[reloadOnSearch=true]` - {boolean=} - reload route when $location.hashSearch - * changes. - * - * If the option is set to false and url in the browser changes, then - * $routeUpdate event is emited on the current route scope. You can use this event to - * react to {@link angular.service.$routeParams} changes: - * - * function MyCtrl($route, $routeParams) { - * this.$on('$routeUpdate', function() { - * // do stuff with $routeParams - * }); - * } - * - * @returns {Object} route object - * - * @description - * Adds a new route definition to the `$route` service. - */ - when:function (path, route) { - if (isUndefined(path)) return routes; //TODO(im): remove - not needed! - var routeDef = routes[path]; - if (!routeDef) routeDef = routes[path] = {reloadOnSearch: true}; - if (route) extend(routeDef, route); //TODO(im): what the heck? merge two route definitions? - dirty++; - return routeDef; - }, - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$route#otherwise - * @methodOf angular.service.$route - * - * @description - * Sets route definition that will be used on route change when no other route definition - * is matched. - * - * @param {Object} params Mapping information to be assigned to `$route.current`. - */ - otherwise: function(params) { - $route.when(null, params); - }, +function $HttpProvider() { + var JSON_START = /^\s*(\[|\{[^\{])/, + JSON_END = /[\}\]]\s*$/, + PROTECTION_PREFIX = /^\)\]\}',?\n/; - /** - * @workInProgress - * @ngdoc method - * @name angular.service.$route#reload - * @methodOf angular.service.$route - * - * @description - * Causes `$route` service to reload (and recreate the `$route.current` scope) upon the next - * eval even if {@link angular.service.$location $location} hasn't changed. - */ - reload: function() { - dirty++; - allowReload = false; - } - }; + var $config = this.defaults = { + // transform incoming response data + transformResponse: function(data) { + if (isString(data)) { + // strip json vulnerability protection prefix + data = data.replace(PROTECTION_PREFIX, ''); + if (JSON_START.test(data) && JSON_END.test(data)) + data = fromJson(data, true); + } + return data; + }, + // transform outgoing request data + transformRequest: function(d) { + return isObject(d) ? toJson(d) : d; + }, + // default headers + headers: { + common: { + 'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest' + }, + post: {'Content-Type': 'application/json'}, + put: {'Content-Type': 'application/json'} + } + }; - this.$watch(function(){ return dirty + $location.hash; }, updateRoute); + var providerResponseInterceptors = this.responseInterceptors = []; - return $route; + this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { - ///////////////////////////////////////////////////// + var defaultCache = $cacheFactory('$http'), + responseInterceptors = []; - function switchRouteMatcher(on, when, dstName) { - var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$', - params = [], - dst = {}; - forEach(when.split(/\W/), function(param){ - if (param) { - var paramRegExp = new RegExp(":" + param + "([\\W])"); - if (regex.match(paramRegExp)) { - regex = regex.replace(paramRegExp, "([^\/]*)$1"); - params.push(param); - } - } + forEach(providerResponseInterceptors, function(interceptor) { + responseInterceptors.push( + isString(interceptor) + ? $injector.get(interceptor) + : $injector.invoke(interceptor) + ); }); - var match = on.match(new RegExp(regex)); - if (match) { - forEach(params, function(name, index){ - dst[name] = match[index + 1]; + + + /** + * @ngdoc function + * @name angular.module.ng.$http + * @requires $httpBacked + * @requires $browser + * @requires $cacheFactory + * @requires $rootScope + * @requires $q + * @requires $injector + * + * @description + * The `$http` service is a core Angular service that is responsible for communication with the + * remote HTTP servers via browser's {@link https://developer.mozilla.org/en/xmlhttprequest + * XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. + * + * For unit testing applications that use `$http` service, see + * {@link angular.module.ngMock.$httpBackend $httpBackend mock}. + * + * For a higher level of abstraction, please check out the {@link angular.module.ng.$resource + * $resource} service. + * + * + * # General usage + * The `$http` service is a function which takes a single argument — a configuration object — + * that is used to generate an http request and returns a {@link angular.module.ng.$q promise} + * with two $http specific methods: `success` and `error`. + * + *
+     *   $http({method: 'GET', url: '/someUrl'}).
+     *     success(function(data, status, headers, config) {
+     *       // this callback will be called asynchronously
+     *       // when the response is available
+     *     }).
+     *     error(function(data, status, headers, config) {
+     *       // called asynchronously if an error occurs
+     *       // or server returns response with status
+     *       // code outside of the <200, 400) range
+     *     });
+     * 
+ * + * Since the returned value is a Promise object, you can also use the `then` method to register + * callbacks, and these callbacks will receive a single argument – an object representing the + * response. See the api signature and type info below for more details. + * + * + * # Shortcut methods + * + * Since all invocation of the $http service require definition of the http method and url and + * POST and PUT requests require response body/data to be provided as well, shortcut methods + * were created to simplify using the api: + * + *
+     *   $http.get('/someUrl').success(successCallback);
+     *   $http.post('/someUrl', data).success(successCallback);
+     * 
+ * + * Complete list of shortcut methods: + * + * - {@link angular.module.ng.$http#get $http.get} + * - {@link angular.module.ng.$http#head $http.head} + * - {@link angular.module.ng.$http#post $http.post} + * - {@link angular.module.ng.$http#put $http.put} + * - {@link angular.module.ng.$http#delete $http.delete} + * - {@link angular.module.ng.$http#jsonp $http.jsonp} + * + * + * # HTTP Headers + * + * The $http service will automatically add certain http headers to all requests. These defaults + * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration + * object, which currently contains this default configuration: + * + * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): + * - `Accept: application/json, text/plain, * / *` + * - `X-Requested-With: XMLHttpRequest` + * - `$httpProvider.defaults.headers.post: (header defaults for HTTP POST requests) + * - `Content-Type: application/json` + * - `$httpProvider.defaults.headers.put` (header defaults for HTTP PUT requests) + * - `Content-Type: application/json` + * + * To add or overwrite these defaults, simply add or remove a property from this configuration + * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object + * with name equal to the lower-cased http method name, e.g. + * `$httpProvider.defaults.headers.get['My-Header']='value'`. + * + * + * # Request / Response transformations + * + * Both requests and responses can be transformed using transform functions. By default, Angular + * applies these transformations: + * + * Request transformations: + * + * - if the `data` property of the request config object contains an object, serialize it into + * JSON format. + * + * Response transformations: + * + * - if XSRF prefix is detected, strip it (see Security Considerations section below) + * - if json response is detected, deserialize it using a JSON parser + * + * These transformations can be overridden locally by specifying transform functions as + * `transformRequest` and/or `transformResponse` properties of the config object. To globally + * override the default transforms, override the `$httpProvider.defaults.transformRequest` and + * `$httpProvider.defaults.transformResponse` properties of the `$httpProvider`. + * + * + * # Caching + * + * You can enable caching by setting the configuration property `cache` to `true`. When the + * cache is enabled, `$http` stores the response from the server in local cache. Next time the + * response is served from the cache without sending a request to the server. + * + * Note that even if the response is served from cache, delivery of the data is asynchronous in + * the same way that real requests are. + * + * If there are multiple GET requests for the same url that should be cached using the same + * cache, but the cache is not populated yet, only one request to the server will be made and + * the remaining requests will be fulfilled using the response for the first request. + * + * + * # Response interceptors + * + * For purposes of global error handling, authentication or any kind of synchronous or + * asynchronous preprocessing of received responses, it is desirable to be able to intercept + * responses for http requests before they are handed over to the application code that + * initiated these requests. The response interceptors leverage the {@link angular.module.ng.$q + * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. + * + * The interceptors are service factories that are registered with the $httpProvider by + * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor — a function that + * takes a {@link angular.module.ng.$q promise} and returns the original or a new promise. + * + * Before you start creating interceptors, be sure to understand the + * {@link angular.module.ng.$q $q and deferred/promise APIs}. + * + *
+     *   // register the interceptor as a service
+     *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
+     *     return function(promise) {
+     *       return promise.then(function(response) {
+     *         // do something on success
+     *       }, function(response) {
+     *         // do something on error
+     *         if (canRecover(response)) {
+     *           return responseOrNewPromise
+     *         }
+     *         return $q.reject(response);
+     *       });
+     *     }
+     *   });
+     *
+     *   $httpProvider.responseInterceptors.push('myHttpInterceptor');
+     *
+     *
+     *   // register the interceptor via an anonymous factory
+     *   $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) {
+     *     return function(promise) {
+     *       // same as above
+     *     }
+     *   });
+     * 
+ * + * + * # Security Considerations + * + * When designing web applications your design needs to consider security threats from + * {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + * JSON Vulnerability} and {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF}. + * Both server and the client must cooperate in order to eliminate these threats. Angular comes + * pre-configured with strategies that address these issues, but for this to work backend server + * cooperation is required. + * + * ## JSON Vulnerability Protection + * + * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + * JSON Vulnerability} allows third party web-site to turn your JSON resource URL into + * {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To + * counter this your server can prefix all JSON requests with following string `")]}',\n"`. + * Angular will automatically strip the prefix before processing it as JSON. + * + * For example if your server needs to return: + *
+     * ['one','two']
+     * 
+ * + * which is vulnerable to attack, your server can return: + *
+     * )]}',
+     * ['one','two']
+     * 
+ * + * Angular will strip the prefix, before processing the JSON. + * + * + * ## Cross Site Request Forgery (XSRF) Protection + * + * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which + * an unauthorized site can gain your user's private data. Angular provides following mechanism + * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie + * called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that + * runs on your domain could read the cookie, your server can be assured that the XHR came from + * JavaScript running on your domain. + * + * To take advantage of this, your server needs to set a token in a JavaScript readable session + * cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the + * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure + * that only JavaScript running on your domain could have read the token. The token must be + * unique for each user and must be verifiable by the server (to prevent the JavaScript making + * up its own tokens). We recommend that the token is a digest of your site's authentication + * cookie with {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}. + * + * + * @param {object} config Object describing the request to be made and how it should be + * processed. The object has following properties: + * + * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) + * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **data** – `{string|Object}` – Data to be sent as the request message data. + * - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. + * - **transformRequest** – `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * - **transformResponse** – `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * response body and headers and returns its transformed (typically deserialized) version. + * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link angular.module.ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **timeout** – `{number}` – timeout in milliseconds. + * + * @returns {HttpPromise} Returns a {@link angular.module.ng.$q promise} object with the + * standard `then` method and two http specific methods: `success` and `error`. The `then` + * method takes two arguments a success and an error callback which will be called with a + * response object. The `success` and `error` methods take a single argument - a function that + * will be called when the request succeeds or fails respectively. The arguments passed into + * these functions are destructured representation of the response object passed into the + * `then` method. The response object has these properties: + * + * - **data** – `{string|Object}` – The response body transformed with the transform functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used to generate the request. + * + * @property {Array.} pendingRequests Array of config objects for currently pending + * requests. This is primarily meant to be used for debugging purposes. + * + * + * @example + + + +
+ + +
+ + + +
http status code: {{status}}
+
http response data: {{data}}
+
+
+ + it('should make an xhr GET request', function() { + element(':button:contains("Sample GET")').click(); + element(':button:contains("fetch")').click(); + expect(binding('status')).toBe('http status code: 200'); + expect(binding('data')).toBe('http response data: Hello, $http!\n'); + }); + + it('should make a JSONP request to angularjs.org', function() { + element(':button:contains("Sample JSONP")').click(); + element(':button:contains("fetch")').click(); + expect(binding('status')).toBe('http status code: 200'); + expect(binding('data')).toMatch(/Super Hero!/); + }); + + it('should make JSONP request to invalid URL and invoke the error handler', + function() { + element(':button:contains("Invalid JSONP")').click(); + element(':button:contains("fetch")').click(); + expect(binding('status')).toBe('http status code: 0'); + expect(binding('data')).toBe('http response data: Request failed'); + }); + +
+ */ + function $http(config) { + config.method = uppercase(config.method); + + var reqTransformFn = config.transformRequest || $config.transformRequest, + respTransformFn = config.transformResponse || $config.transformResponse, + defHeaders = $config.headers, + reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, + defHeaders.common, defHeaders[lowercase(config.method)], config.headers), + reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn), + promise; + + + // send request + promise = sendReq(config, reqData, reqHeaders); + + + // transform future response + promise = promise.then(transformResponse, transformResponse); + + // apply interceptors + forEach(responseInterceptors, function(interceptor) { + promise = interceptor(promise); }); - if (dstName) this.$set(dstName, dst); - } - return match ? dst : null; - } - function updateRoute(){ - var next = parseRoute(), - last = $route.current; + promise.success = function(fn) { + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; - if (next && last && next.$route === last.$route - && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && allowReload) { - $route.current = next; - copy(next.params, $routeParams); - last.scope && last.scope.$emit('$routeUpdate'); - } else { - allowReload = true; - rootScope.$broadcast('$beforeRouteChange', next, last); - last && last.scope && last.scope.$destroy(); - $route.current = next; - if (next) { - if (next.redirectTo) { - $location.update(isString(next.redirectTo) - ? {hashSearch: next.params, hashPath: interpolate(next.redirectTo, next.params)} - : {hash: next.redirectTo(next.pathParams, - $location.hash, $location.hashPath, $location.hashSearch)}); - } else { - copy(next.params, $routeParams); - next.scope = parentScope.$new(next.controller); - } + promise.error = function(fn) { + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + return promise; + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response, { + data: transformData(response.data, response.headers, respTransformFn) + }); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); } - rootScope.$broadcast('$afterRouteChange', next, last); } - } + $http.pendingRequests = []; - /** - * @returns the current active route, by matching it against the URL - */ - function parseRoute(){ - // Match a route - var params, match; - forEach(routes, function(route, path) { - if (!match && (params = matcher($location.hashPath, path))) { - match = inherit(route, { - params: extend({}, $location.hashSearch, params), - pathParams: params}); - match.$route = route; - } - }); - // No route matched; fallback to "otherwise" route - return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); - } + /** + * @ngdoc method + * @name angular.module.ng.$http#get + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `GET` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ - /** - * @returns interpolation of the redirect path with the parametrs - */ - function interpolate(string, params) { - var result = []; - forEach((string||'').split(':'), function(segment, i) { - if (i == 0) { - result.push(segment); - } else { - var segmentMatch = segment.match(/(\w+)(.*)/); - var key = segmentMatch[1]; - result.push(params[key]); - result.push(segmentMatch[2] || ''); - delete params[key]; - } - }); - return result.join(''); - } + /** + * @ngdoc method + * @name angular.module.ng.$http#delete + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `DELETE` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + /** + * @ngdoc method + * @name angular.module.ng.$http#head + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `HEAD` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object + */ -}, ['$location', '$routeParams']); + /** + * @ngdoc method + * @name angular.module.ng.$http#jsonp + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `JSONP` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request. + * Should contain `JSON_CALLBACK` string. + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object + */ + createShortMethods('get', 'delete', 'head', 'jsonp'); -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$routeParams - * @requires $route - * - * @description - * Current set of route parameters. The route parameters are a combination of the - * {@link angular.service.$location $location} `hashSearch`, and `path`. The `path` parameters - * are extracted when the {@link angular.service.$route $route} path is matched. - * - * In case of parameter name collision, `path` params take precedence over `hashSearch` params. - * - * The service guarantees that the identity of the `$routeParams` object will remain unchanged - * (but its properties will likely change) even when a route change occurs. - * - * @example - *
- *  // Given:
- *  // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
- *  // Route: /Chapter/:chapterId/Section/:sectionId
- *  //
- *  // Then
- *  $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
- * 
- */ -angularService('$routeParams', function(){ - return {}; -}); + /** + * @ngdoc method + * @name angular.module.ng.$http#post + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `POST` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name angular.module.ng.$http#put + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `PUT` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object + */ + createShortMethodsWithData('post', 'put'); + + + return $http; + + + function createShortMethods(names) { + forEach(arguments, function(name) { + $http[name] = function(url, config) { + return $http(extend(config || {}, { + method: name, + url: url + })); + }; + }); + } -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$window - * - * @description - * A reference to the browser's `window` object. While `window` - * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the - * `$window` service, so it may be overriden, removed or mocked for testing. - * - * All expressions are evaluated with respect to current scope so they don't - * suffer from window globality. - * - * @example - - - - - - - - - */ -angularServiceInject("$window", bind(window, identity, window)); -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr.bulk - * @requires $xhr - * @requires $xhr.error - * @requires $log - * - * @description - * - * @example - */ -angularServiceInject('$xhr.bulk', function($xhr, $error, $log){ - var requests = [], - scope = this; - function bulkXHR(method, url, post, success, error) { - if (isFunction(post)) { - error = success; - success = post; - post = null; + function createShortMethodsWithData(name) { + forEach(arguments, function(name) { + $http[name] = function(url, data, config) { + return $http(extend(config || {}, { + method: name, + url: url, + data: data + })); + }; + }); } - var currentQueue; - forEach(bulkXHR.urls, function(queue){ - if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) { - currentQueue = queue; + + + /** + * Makes the request + * + * !!! ACCESSES CLOSURE VARS: + * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests + */ + function sendReq(config, reqData, reqHeaders) { + var deferred = $q.defer(), + promise = deferred.promise, + cache, + cachedResp; + + $http.pendingRequests.push(config); + promise.then(removePendingReq, removePendingReq); + + + if (config.cache && config.method == 'GET') { + cache = isObject(config.cache) ? config.cache : defaultCache; } - }); - if (currentQueue) { - if (!currentQueue.requests) currentQueue.requests = []; - var request = { - method: method, - url: url, - data: post, - success: success}; - if (error) request.error = error; - currentQueue.requests.push(request); - } else { - $xhr(method, url, post, success, error); - } - } - bulkXHR.urls = {}; - bulkXHR.flush = function(success, errorback) { - assertArgFn(success = success || noop, 0); - assertArgFn(errorback = errorback || noop, 1); - forEach(bulkXHR.urls, function(queue, url) { - var currentRequests = queue.requests; - if (currentRequests && currentRequests.length) { - queue.requests = []; - queue.callbacks = []; - $xhr('POST', url, {requests: currentRequests}, - function(code, response) { - forEach(response, function(response, i) { - try { - if (response.status == 200) { - (currentRequests[i].success || noop)(response.status, response.response); - } else if (isFunction(currentRequests[i].error)) { - currentRequests[i].error(response.status, response.response); - } else { - $error(currentRequests[i], response); - } - } catch(e) { - $log.error(e); - } - }); - success(); - }, - function(code, response) { - forEach(currentRequests, function(request, i) { - try { - if (isFunction(request.error)) { - request.error(code, response); - } else { - $error(request, response); - } - } catch(e) { - $log.error(e); - } - }); - noop(); - }); + + if (cache) { + cachedResp = cache.get(config.url); + if (cachedResp) { + if (cachedResp.then) { + // cached request has already been sent, but there is no response yet + cachedResp.then(removePendingReq, removePendingReq); + return cachedResp; + } else { + // serving from cache + if (isArray(cachedResp)) { + resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); + } else { + resolvePromise(cachedResp, 200, {}); + } + } + } else { + // put the promise for the non-transformed response into cache as a placeholder + cache.put(config.url, promise); + } } - }); - }; - this.$watch(function(){ bulkXHR.flush(); }); - return bulkXHR; -}, ['$xhr', '$xhr.error', '$log']); -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr.cache - * @function - * - * @requires $xhr.bulk - * @requires $defer - * @requires $xhr.error - * @requires $log - * - * @description - * Acts just like the {@link angular.service.$xhr $xhr} service but caches responses for `GET` - * requests. All cache misses are delegated to the $xhr service. - * - * @property {function()} delegate Function to delegate all the cache misses to. Defaults to - * the {@link angular.service.$xhr $xhr} service. - * @property {object} data The hashmap where all cached entries are stored. - * - * @param {string} method HTTP method. - * @param {string} url Destination URL. - * @param {(string|Object)=} post Request body. - * @param {function(number, (string|Object))} success Response success callback. - * @param {function(number, (string|Object))=} error Response error callback. - * @param {boolean=} [verifyCache=false] If `true` then a result is immediately returned from cache - * (if present) while a request is sent to the server for a fresh response that will update the - * cached entry. The `success` function will be called when the response is received. - * @param {boolean=} [sync=false] in case of cache hit execute `success` synchronously. - */ -angularServiceInject('$xhr.cache', function($xhr, $defer, $error, $log) { - var inflight = {}, self = this; - function cache(method, url, post, success, error, verifyCache, sync) { - if (isFunction(post)) { - if (!isFunction(success)) { - verifyCache = success; - sync = error; - error = null; - } else { - sync = verifyCache; - verifyCache = error; - error = success; + // if we won't have the response in cache, send the request to the backend + if (!cachedResp) { + $httpBackend(config.method, config.url, reqData, done, reqHeaders, config.timeout); } - success = post; - post = null; - } else if (!isFunction(error)) { - sync = verifyCache; - verifyCache = error; - error = null; - } - if (method == 'GET') { - var data, dataCached; - if ((dataCached = cache.data[url])) { + return promise; - if (sync) { - success(200, copy(dataCached.value)); - } else { - $defer(function() { success(200, copy(dataCached.value)); }); + + /** + * Callback registered to $httpBackend(): + * - caches the response if desired + * - resolves the raw $http promise + * - calls $apply + */ + function done(status, response, headersString) { + if (cache) { + if (isSuccess(status)) { + cache.put(config.url, [status, response, parseHeaders(headersString)]); + } else { + // remove promise from the cache + cache.remove(config.url); + } } - if (!verifyCache) - return; + resolvePromise(response, status, headersString); + $rootScope.$apply(); } - if ((data = inflight[url])) { - data.successes.push(success); - data.errors.push(error); - } else { - inflight[url] = {successes: [success], errors: [error]}; - cache.delegate(method, url, post, - function(status, response) { - if (status == 200) - cache.data[url] = {value: response}; - var successes = inflight[url].successes; - delete inflight[url]; - forEach(successes, function(success) { - try { - (success||noop)(status, copy(response)); - } catch(e) { - $log.error(e); - } - }); - }, - function(status, response) { - var errors = inflight[url].errors, - successes = inflight[url].successes; - delete inflight[url]; - forEach(errors, function(error, i) { - try { - if (isFunction(error)) { - error(status, copy(response)); - } else { - $error( - {method: method, url: url, data: post, success: successes[i]}, - {status: status, body: response}); - } - } catch(e) { - $log.error(e); - } - }); - }); + /** + * Resolves the raw $http promise. + */ + function resolvePromise(response, status, headers) { + // normalize internal statuses to 0 + status = Math.max(status, 0); + + (isSuccess(status) ? deferred.resolve : deferred.reject)({ + data: response, + status: status, + headers: headersGetter(headers), + config: config + }); } - } else { - cache.data = {}; - cache.delegate(method, url, post, success, error); + + function removePendingReq() { + var idx = indexOf($http.pendingRequests, config); + if (idx !== -1) $http.pendingRequests.splice(idx, 1); + } } - } - cache.data = {}; - cache.delegate = $xhr; - return cache; -}, ['$xhr.bulk', '$defer', '$xhr.error', '$log']); + }]; +} +var XHR = window.XMLHttpRequest || function() { + try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} + try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} + try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} + throw new Error("This browser does not support XMLHttpRequest."); +}; -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr.error - * @function - * @requires $log - * - * @description - * Error handler for {@link angular.service.$xhr $xhr service}. An application can replaces this - * service with one specific for the application. The default implementation logs the error to - * {@link angular.service.$log $log.error}. - * - * @param {Object} request Request object. - * - * The object has the following properties - * - * - `method` – `{string}` – The http request method. - * - `url` – `{string}` – The request destination. - * - `data` – `{(string|Object)=} – An optional request body. - * - `success` – `{function()}` – The success callback function - * - * @param {Object} response Response object. - * - * The response object has the following properties: - * - * - status – {number} – Http status code. - * - body – {string|Object} – Body of the response. - * - * @example - - - fetch a non-existent file and log an error in the console: - - - - */ -angularServiceInject('$xhr.error', function($log){ - return function(request, response){ - $log.error('ERROR: XHR: ' + request.url, request, response); - }; -}, ['$log']); /** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr - * @function - * @requires $browser $xhr delegates all XHR requests to the `$browser.xhr()`. A mock version - * of the $browser exists which allows setting expectations on XHR requests - * in your tests - * @requires $xhr.error $xhr delegates all non `2xx` response code to this service. - * @requires $log $xhr delegates all exceptions to `$log.error()`. + * @ngdoc object + * @name angular.module.ng.$httpBackend + * @requires $browser + * @requires $window + * @requires $document * * @description - * Generates an XHR request. The $xhr service delegates all requests to - * {@link angular.service.$browser $browser.xhr()} and adds error handling and security features. - * While $xhr service provides nicer api than raw XmlHttpRequest, it is still considered a lower - * level api in angular. For a higher level abstraction that utilizes `$xhr`, please check out the - * {@link angular.service.$resource $resource} service. - * - * # Error handling - * If no `error callback` is specified, XHR response with response code other then `2xx` will be - * delegated to {@link angular.service.$xhr.error $xhr.error}. The `$xhr.error` can intercept the - * request and process it in application specific way, or resume normal execution by calling the - * request `success` method. - * - * # HTTP Headers - * The $xhr service will automatically add certain http headers to all requests. These defaults can - * be fully configured by accessing the `$xhr.defaults.headers` configuration object, which - * currently contains this default configuration: - * - * - `$xhr.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, *\/*` - * - `X-Requested-With: XMLHttpRequest` - * - `$xhr.defaults.headers.post` (header defaults for HTTP POST requests): - * - `Content-Type: application/x-www-form-urlencoded` - * - * To add or overwrite these defaults, simple add or remove a property from this configuration - * object. To add headers for an HTTP method other than POST, simple create a new object with name - * equal to the lowercased http method name, e.g. `$xhr.defaults.headers.get['My-Header']='value'`. - * - * - * # Security Considerations - * When designing web applications your design needs to consider security threats from - * {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON Vulnerability} and {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF}. - * Both server and the client must cooperate in order to eliminate these threats. Angular comes - * pre-configured with strategies that address these issues, but for this to work backend server - * cooperation is required. - * - * ## JSON Vulnerability Protection - * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON Vulnerability} allows third party web-site to turn your JSON resource URL into - * {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To - * counter this your server can prefix all JSON requests with following string `")]}',\n"`. - * Angular will automatically strip the prefix before processing it as JSON. - * - * For example if your server needs to return: - *
- * ['one','two']
- * 
- * - * which is vulnerable to attack, your server can return: - *
- * )]}',
- * ['one','two']
- * 
+ * HTTP backend used by the {@link angular.module.ng.$http service} that delegates to + * XMLHttpRequest object or JSONP and deals with browser incompatibilities. * - * angular will strip the prefix, before processing the JSON. - * - * - * ## Cross Site Request Forgery (XSRF) Protection - * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which an - * unauthorized site can gain your user's private data. Angular provides following mechanism to - * counter XSRF. When performing XHR requests, the $xhr service reads a token from a cookie - * called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that - * runs on your domain could read the cookie, your server can be assured that the XHR came from - * JavaScript running on your domain. - * - * To take advantage of this, your server needs to set a token in a JavaScript readable session - * cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the server - * can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure that only - * JavaScript running on your domain could have read the token. The token must be unique for each - * user and must be verifiable by the server (to prevent the JavaScript making up its own tokens). - * We recommend that the token is a digest of your site's authentication cookie with - * {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}. - * - * @param {string} method HTTP method to use. Valid values are: `GET`, `POST`, `PUT`, `DELETE`, and - * `JSON`. `JSON` is a special case which causes a - * [JSONP](http://en.wikipedia.org/wiki/JSON#JSONP) cross domain request using script tag - * insertion. - * @param {string} url Relative or absolute URL specifying the destination of the request. For - * `JSON` requests, `url` should include `JSON_CALLBACK` string to be replaced with a name of an - * angular generated callback function. - * @param {(string|Object)=} post Request content as either a string or an object to be stringified - * as JSON before sent to the server. - * @param {function(number, (string|Object))} success A function to be called when the response is - * received. The success function will be called with: - * - * - {number} code [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) of - * the response. This will currently always be 200, since all non-200 responses are routed to - * {@link angular.service.$xhr.error} service (or custom error callback). - * - {string|Object} response Response object as string or an Object if the response was in JSON - * format. - * @param {function(number, (string|Object))} error A function to be called if the response code is - * not 2xx.. Accepts the same arguments as success, above. + * You should never need to use this service directly, instead use the higher-level abstractions: + * {@link angular.module.ng.$http $http} or {@link angular.module.ng.$resource $resource}. * - * @example - - - -
- - -
- - - -
code={{code}}
-
response={{response}}
-
-
- - it('should make xhr GET request', function() { - element(':button:contains("Sample GET")').click(); - element(':button:contains("fetch")').click(); - expect(binding('code')).toBe('code=200'); - expect(binding('response')).toMatch(/angularjs.org/); - }); +function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locationProtocol) { + // TODO(vojta): fix the signature + return function(method, url, post, callback, headers, timeout) { + $browser.$$incOutstandingRequestCount(); - it('should make JSONP request to the Buzz API', function() { - element(':button:contains("Buzz API")').click(); - element(':button:contains("fetch")').click(); - expect(binding('code')).toBe('code=200'); - expect(binding('response')).toMatch(/buzz-feed/); - }); + if (lowercase(method) == 'jsonp') { + var callbackId = '_' + (callbacks.counter++).toString(36); + callbacks[callbackId] = function(data) { + callbacks[callbackId].data = data; + }; - it('should make JSONP request to invalid URL and invoke the error handler', - function() { - element(':button:contains("Invalid JSONP")').click(); - element(':button:contains("fetch")').click(); - expect(binding('code')).toBe('code='); - expect(binding('response')).toBe('response=Request failed'); - }); - -
- */ -angularServiceInject('$xhr', function($browser, $error, $log){ - var rootScope = this; - var xhrHeaderDefaults = { - common: { - "Accept": "application/json, text/plain, */*", - "X-Requested-With": "XMLHttpRequest" - }, - post: {'Content-Type': 'application/x-www-form-urlencoded'}, - get: {}, // all these empty properties are needed so that client apps can just do: - head: {}, // $xhr.defaults.headers.head.foo="bar" without having to create head object - put: {}, // it also means that if we add a header for these methods in the future, it - 'delete': {}, // won't be easily silently lost due to an object assignment. - patch: {} - }; + var script = $browser.addJs(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), + function() { + if (callbacks[callbackId].data) { + completeRequest(callback, 200, callbacks[callbackId].data); + } else { + completeRequest(callback, -2); + } + delete callbacks[callbackId]; + body.removeChild(script); + }); + } else { + var xhr = new XHR(); + xhr.open(method, url, true); + forEach(headers, function(value, key) { + if (value) xhr.setRequestHeader(key, value); + }); - function xhr(method, url, post, success, error) { - if (isFunction(post)) { - error = success; - success = post; - post = null; - } - if (post && isObject(post)) { - post = toJson(post); - } + var status; - $browser.xhr(method, url, post, function(code, response){ - try { - if (isString(response)) { - if (response.match(/^\)\]\}',\n/)) response=response.substr(6); - if (/^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { - response = fromJson(response, true); - } + // In IE6 and 7, this might be called synchronously when xhr.send below is called and the + // response is in the cache. the promise api will ensure that to the app code the api is + // always async + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + completeRequest( + callback, status || xhr.status, xhr.responseText, xhr.getAllResponseHeaders()); } - rootScope.$apply(function(){ - if (200 <= code && code < 300) { - success(code, response); - } else if (isFunction(error)) { - error(code, response); - } else { - $error( - {method: method, url: url, data: post, success: success}, - {status: code, body: response}); - } - }); - } catch (e) { - $log.error(e); + }; + + xhr.send(post || ''); + + if (timeout > 0) { + $browserDefer(function() { + status = -1; + xhr.abort(); + }, timeout); } - }, extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, - xhrHeaderDefaults.common, - xhrHeaderDefaults[lowercase(method)])); - } + } - xhr.defaults = {headers: xhrHeaderDefaults}; - return xhr; -}, ['$browser', '$xhr.error', '$log']); + function completeRequest(callback, status, response, headersString) { + // URL_MATCH is defined in src/service/location.js + var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1]; + + // fix status code for file protocol (it's always 0) + status = (protocol == 'file') ? (response ? 200 : 404) : status; + + // normalize IE bug (http://bugs.jquery.com/ticket/1450) + status = status == 1223 ? 204 : status; + + callback(status, response, headersString); + $browser.$$completeOutstandingRequest(noop); + } + }; +} /** - * @ngdoc service - * @name angular.service.$locale + * @ngdoc object + * @name angular.module.ng.$locale * * @description * $locale service provides localization rules for various Angular components. As of right now the @@ -8487,67 +9808,69 @@ angularServiceInject('$xhr', function($browser, $error, $log){ * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -angularServiceInject('$locale', function() { - return { - id: 'en-us', - - NUMBER_FORMATS: { - DECIMAL_SEP: '.', - GROUP_SEP: ',', - PATTERNS: [ - { // Decimal Pattern - minInt: 1, - minFrac: 0, - maxFrac: 3, - posPre: '', - posSuf: '', - negPre: '-', - negSuf: '', - gSize: 3, - lgSize: 3 - },{ //Currency Pattern - minInt: 1, - minFrac: 2, - maxFrac: 2, - posPre: '\u00A4', - posSuf: '', - negPre: '(\u00A4', - negSuf: ')', - gSize: 3, - lgSize: 3 - } - ], - CURRENCY_SYM: '$' - }, +function $LocaleProvider(){ + this.$get = function() { + return { + id: 'en-us', + + NUMBER_FORMATS: { + DECIMAL_SEP: '.', + GROUP_SEP: ',', + PATTERNS: [ + { // Decimal Pattern + minInt: 1, + minFrac: 0, + maxFrac: 3, + posPre: '', + posSuf: '', + negPre: '-', + negSuf: '', + gSize: 3, + lgSize: 3 + },{ //Currency Pattern + minInt: 1, + minFrac: 2, + maxFrac: 2, + posPre: '\u00A4', + posSuf: '', + negPre: '(\u00A4', + negSuf: ')', + gSize: 3, + lgSize: 3 + } + ], + CURRENCY_SYM: '$' + }, - DATETIME_FORMATS: { - MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December' - .split(','), - SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), - DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), - SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), - AMPMS: ['AM','PM'], - medium: 'MMM d, y h:mm:ss a', - short: 'M/d/yy h:mm a', - fullDate: 'EEEE, MMMM d, y', - longDate: 'MMMM d, y', - mediumDate: 'MMM d, y', - shortDate: 'M/d/yy', - mediumTime: 'h:mm:ss a', - shortTime: 'h:mm a' - }, + DATETIME_FORMATS: { + MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December' + .split(','), + SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), + DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), + SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), + AMPMS: ['AM','PM'], + medium: 'MMM d, y h:mm:ss a', + short: 'M/d/yy h:mm a', + fullDate: 'EEEE, MMMM d, y', + longDate: 'MMMM d, y', + mediumDate: 'MMM d, y', + shortDate: 'M/d/yy', + mediumTime: 'h:mm:ss a', + shortTime: 'h:mm a' + }, - pluralCat: function(num) { - if (num === 1) { - return 'one'; + pluralCat: function(num) { + if (num === 1) { + return 'one'; + } + return 'other'; } - return 'other'; - } + }; }; -}); +} /** - * @ngdoc overview + * @ngdoc function * @name angular.directive * @description * @@ -8555,42 +9878,30 @@ angularServiceInject('$locale', function() { * behavior of the element in which it is specified. Do not use directives to add elements to the * DOM; instead, use {@link angular.widget widgets} to add DOM elements. * - * Following is the list of built-in Angular directives: - * - * * {@link angular.directive.ng:autobind ng:autobind} - An Angular bootstrap parameter that can - * act as a directive. - * * {@link angular.directive.ng:bind ng:bind} - Creates a data-binding between an HTML text value - * and a data model. - * * {@link angular.directive.ng:bind-attr ng:bind-attr} - Creates a data-binding in a way similar - * to `ng:bind`, but uses JSON key / value pairs to do so. - * * {@link angular.directive.ng:bind-template ng:bind-template} - Replaces the text value of an - * element with a specified template. - * * {@link angular.directive.ng:change ng:change} - Executes an expression when the value of an - * input widget changes. - * * {@link angular.directive.ng:class ng:class} - Conditionally set a CSS class on an element. - * * {@link angular.directive.ng:class-even ng:class-even} - Like `ng:class`, but works in - * conjunction with {@link angular.widget.@ng:repeat} to affect even rows in a collection. - * * {@link angular.directive.ng:class-odd ng:class-odd} - Like `ng:class`, but works with {@link - * angular.widget.@ng:repeat} to affect odd rows. - * * {@link angular.directive.ng:click ng:click} - Executes custom behavior when an element is - * clicked. - * * {@link angular.directive.ng:controller ng:controller} - Creates a scope object linked to the - * DOM element and assigns behavior to the scope. - * * {@link angular.directive.ng:hide ng:hide} - Conditionally hides a portion of HTML. - * * {@link angular.directive.ng:href ng:href} - Places an href in the Angular namespace. - * * {@link angular.directive.ng:init} - Initialization tasks run before a template is executed. - * * {@link angular.directive.ng:show ng:show} - Conditionally displays a portion of HTML. - * * {@link angular.directive.ng:src ng:src} - Places a `src` attribute into the Angular namespace. - * * {@link angular.directive.ng:style ng:style} - Conditionally set CSS styles on an element. - * * {@link angular.directive.ng:submit} - Binds Angular expressions to `onSubmit` events. - * * For more information about how Angular directives work, and to learn how to create your own * directives, see {@link guide/dev_guide.compiler.directives Understanding Angular Directives} in * the Angular Developer Guide. + * + * @param {string} name Directive identifier (case insensitive). + * @param {function(string, Element)} compileFn Also called "template function" is a function called + * during compilation of the template when the compiler comes across the directive being + * registered. The string value of the element attribute representing the directive and + * jQuery/jqLite wrapped DOM element are passed as arguments to this function. + * + * The `compileFn` function may return a linking function also called an instance function. + * This function is called during the linking phase when a Scope is being associated with the + * template or template clone (see repeater notes below). The signature of the linking function + * is: `function(Element)` where Element is jQuery/jqLite wrapped DOM Element that is being + * linked. + * + * The biggest differenciator between the compile and linking functions is how they are being called + * when a directive is present within an {@link angular.widget.@ng:repeat ng:repeat}. In this case, + * the compile function gets called once per occurence of the directive in the template. On the + * other hand the linking function gets called once for each repeated clone of the template (times + * number of occurences of the directive in the repeated template). */ /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:init * @@ -8609,7 +9920,7 @@ angularServiceInject('$locale', function() { - it('should check greeting', function(){ + it('should check greeting', function() { expect(binding('greeting')).toBe('Hello'); expect(binding('person')).toBe('World'); }); @@ -8623,7 +9934,6 @@ angularDirective("ng:init", function(expression){ }); /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:controller * @@ -8638,7 +9948,7 @@ angularDirective("ng:init", function(expression){ * * Controller — The `ng:controller` directive specifies a Controller class; the class has * methods that typically express the business logic behind the application. * - * Note that an alternative way to define controllers is via the `{@link angular.service.$route}` + * Note that an alternative way to define controllers is via the `{@link angular.module.ng.$route}` * service. * * @element ANY @@ -8663,14 +9973,14 @@ angularDirective("ng:init", function(expression){ {type:'email', value:'john.smith@example.org'} ]; } SettingsController.prototype = { - greet: function(){ + greet: function() { alert(this.name); }, - addContact: function(){ + addContact: function() { this.contacts.push({type:'email', value:'yourname@example.org'}); }, removeContact: function(contactToRemove) { - angular.Array.remove(this.contacts, contactToRemove); + angular.module.ng.$filter.remove(this.contacts, contactToRemove); }, clearContact: function(contact) { contact.type = 'phone'; @@ -8679,16 +9989,16 @@ angularDirective("ng:init", function(expression){ };
- Name: + Name: [ greet ]
Contact:
  • - - + [ clear | X ]
  • @@ -8697,18 +10007,18 @@ angularDirective("ng:init", function(expression){
- it('should check controller', function(){ + it('should check controller', function() { expect(element('.doc-example-live div>:input').val()).toBe('John Smith'); - expect(element('.doc-example-live li[ng\\:repeat-index="0"] input').val()) + expect(element('.doc-example-live li:nth-child(1) input').val()) .toBe('408 555 1212'); - expect(element('.doc-example-live li[ng\\:repeat-index="1"] input').val()) + expect(element('.doc-example-live li:nth-child(2) input').val()) .toBe('john.smith@example.org'); element('.doc-example-live li:first a:contains("clear")').click(); expect(element('.doc-example-live li:first input').val()).toBe(''); element('.doc-example-live li:last a:contains("add")').click(); - expect(element('.doc-example-live li[ng\\:repeat-index="2"] input').val()) + expect(element('.doc-example-live li:nth-child(3) input').val()) .toBe('yourname@example.org'); }); @@ -8720,6 +10030,7 @@ angularDirective("ng:controller", function(expression){ getter(scope, expression, true) || getter(window, expression, true); assertArgFn(Controller, expression); + inferInjectionArgs(Controller); return Controller; }); return noop; @@ -8745,11 +10056,18 @@ angularDirective("ng:controller", function(expression){ * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - Enter name:
- Hello ! + +
+ Enter name:
+ Hello ! +
- it('should check ng:bind', function(){ + it('should check ng:bind', function() { expect(using('.doc-example-live').binding('name')).toBe('Whirled'); using('.doc-example-live').input('name').enter('world'); expect(using('.doc-example-live').binding('name')).toBe('world'); @@ -8759,110 +10077,55 @@ angularDirective("ng:controller", function(expression){ */ angularDirective("ng:bind", function(expression, element){ element.addClass('ng-binding'); - var exprFn = parser(expression).statements(); - return function(element) { - var lastValue = noop, lastError = noop; + return ['$exceptionHandler', '$parse', '$element', function($exceptionHandler, $parse, element) { + var exprFn = $parse(expression), + lastValue = Number.NaN; + this.$watch(function(scope) { // TODO(misko): remove error handling https://github.com/angular/angular.js/issues/347 - var error, value, html, isHtml, isDomElement, + var value, html, isHtml, isDomElement, hadOwnElement = scope.hasOwnProperty('$element'), oldElement = scope.$element; // TODO(misko): get rid of $element https://github.com/angular/angular.js/issues/348 scope.$element = element; try { value = exprFn(scope); - } catch (e) { - scope.$service('$exceptionHandler')(e); - error = formatError(e); - } finally { - if (hadOwnElement) { - scope.$element = oldElement; - } else { - delete scope.$element; - } - } - // If we are HTML, then save the raw HTML data so that we don't - // recompute sanitization since that is expensive. - // TODO: turn this into a more generic way to compute this - if ((isHtml = (value instanceof HTML))) - value = (html = value).html; - if (lastValue === value && lastError == error) return; - isDomElement = isElement(value); - if (!isHtml && !isDomElement && isObject(value)) { - value = toJson(value, true); - } - if (value != lastValue || error != lastError) { - lastValue = value; - lastError = error; - elementError(element, NG_EXCEPTION, error); - if (error) value = error; - if (isHtml) { - element.html(html.get()); - } else if (isDomElement) { - element.html(''); - element.append(value); - } else { - element.text(value == undefined ? '' : value); + // If we are HTML than save the raw HTML data so that we don't recompute sanitization since + // it is expensive. + // TODO(misko): turn this into a more generic way to compute this + if ((isHtml = (value instanceof HTML))) + value = (html = value).html; + if (lastValue === value) return; + isDomElement = isElement(value); + if (!isHtml && !isDomElement && isObject(value)) { + value = toJson(value, true); } - } - }); - }; -}); - -var bindTemplateCache = {}; -function compileBindTemplate(template){ - var fn = bindTemplateCache[template]; - if (!fn) { - var bindings = []; - forEach(parseBindings(template), function(text){ - var exp = binding(text); - bindings.push(exp - ? function(scope, element) { - var error, value; - try { - value = scope.$eval(exp); - } catch(e) { - scope.$service('$exceptionHandler')(e); - error = toJson(e); - } - elementError(element, NG_EXCEPTION, error); - return error ? error : value; + if (value != lastValue) { + lastValue = value; + if (isHtml) { + element.html(html.get()); + } else if (isDomElement) { + element.html(''); + element.append(value); + } else { + element.text(value == undefined ? '' : value); } - : function() { - return text; - }); - }); - bindTemplateCache[template] = fn = function(scope, element, prettyPrintJson) { - var parts = [], - hadOwnElement = scope.hasOwnProperty('$element'), - oldElement = scope.$element; - - // TODO(misko): get rid of $element - scope.$element = element; - try { - for (var i = 0; i < bindings.length; i++) { - var value = bindings[i](scope, element); - if (isElement(value)) - value = ''; - else if (isObject(value)) - value = toJson(value, prettyPrintJson); - parts.push(value); } - return parts.join(''); + } catch (e) { + $exceptionHandler(e); } finally { if (hadOwnElement) { scope.$element = oldElement; } else { delete scope.$element; - } - } - }; - } - return fn; -} + } + } + }); + }]; +}); + /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:bind-template * @@ -8881,12 +10144,20 @@ function compileBindTemplate(template){ * Try it here: enter text in text box and watch the greeting change. - Salutation:
- Name:
-

+       
+       
+ Salutation:
+ Name:
+

+       
- it('should check ng:bind', function(){ + it('should check ng:bind', function() { expect(using('.doc-example-live').binding('{{salutation}} {{name}}')). toBe('Hello World!'); using('.doc-example-live').input('salutation').enter('Greetings'); @@ -8912,13 +10183,6 @@ angularDirective("ng:bind-template", function(expression, element){ }; }); -var REMOVE_ATTRIBUTES = { - 'disabled':'disabled', - 'readonly':'readOnly', - 'checked':'checked', - 'selected':'selected', - 'multiple':'multiple' -}; /** * @ngdoc directive * @name angular.directive.ng:bind-attr @@ -8932,18 +10196,28 @@ var REMOVE_ATTRIBUTES = { * * Instead of writing `ng:bind-attr` statements in your HTML, you can use double-curly markup to * specify an {{expression}} for the value of an attribute. - * At compile time, the attribute is translated into an `` + * At compile time, the attribute is translated into an + * ``. * * The following HTML snippet shows how to specify `ng:bind-attr`: *
- *   Google
+ *   Google
  * 
* - * During compilation, the snippet gets translated to the following: + * This is cumbersome, so as we mentioned using double-curly markup is a prefered way of creating + * this binding: *
- *   Google
+ *   Google
  * 
* + * During compilation, the template with attribute markup gets translated to the ng:bind-attr form + * mentioned above. + * + * _Note_: You might want to consider using {@link angular.directive.ng:href ng:href} instead of + * `href` if the binding is present in the main application template (`index.html`) and you want to + * make sure that a user is not capable of clicking on raw/uncompiled link. + * + * * @element ANY * @param {string} attribute_json one or more JSON key-value pairs representing * the attributes to replace with expressions. Each key matches an attribute @@ -8956,12 +10230,23 @@ var REMOVE_ATTRIBUTES = { * Enter a search string in the Live Preview text box and then click "Google". The search executes instantly. - Google for: - - Google + +
+ Google for: + + + Google + (ng:bind-attr) | + Google + (curly binding in attribute val) +
- it('should check ng:bind-attr', function(){ + it('should check ng:bind-attr', function() { expect(using('.doc-example-live').element('a').attr('href')). toBe('http://www.google.com/search?q=AngularJS'); using('.doc-example-live').input('query').enter('google'); @@ -8977,22 +10262,10 @@ angularDirective("ng:bind-attr", function(expression){ this.$watch(function(scope){ var values = scope.$eval(expression); for(var key in values) { - var value = compileBindTemplate(values[key])(scope, element), - specialName = REMOVE_ATTRIBUTES[lowercase(key)]; + var value = compileBindTemplate(values[key])(scope, element); if (lastValue[key] !== value) { lastValue[key] = value; - if (specialName) { - if (toBoolean(value)) { - element.attr(specialName, specialName); - element.attr('ng-' + specialName, value); - } else { - element.removeAttr(specialName); - element.removeAttr('ng-' + specialName); - } - (element.data($$validate)||noop)(); - } else { - element.attr(key, value); - } + element.attr(key, BOOLEAN_ATTR[lowercase(key)] ? toBoolean(value) : value); } } }); @@ -9001,7 +10274,6 @@ angularDirective("ng:bind-attr", function(expression){ /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:click * @@ -9022,7 +10294,7 @@ angularDirective("ng:bind-attr", function(expression){ count: {{count}} - it('should check ng:click', function(){ + it('should check ng:click', function() { expect(binding('count')).toBe('0'); element('.doc-example-live :button').click(); expect(binding('count')).toBe('1'); @@ -9050,7 +10322,6 @@ angularDirective("ng:click", function(expression, element){ /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:submit * @@ -9066,17 +10337,36 @@ angularDirective("ng:click", function(expression, element){ * @example -
+ + Enter text and hit enter: - + +
list={{list}}
-
list={{list}}
- it('should check ng:submit', function(){ + it('should check ng:submit', function() { + expect(binding('list')).toBe('list=[]'); + element('.doc-example-live #submit').click(); + expect(binding('list')).toBe('list=["hello"]'); + expect(input('text').val()).toBe(''); + }); + it('should ignore empty strings', function() { expect(binding('list')).toBe('list=[]'); element('.doc-example-live #submit').click(); + element('.doc-example-live #submit').click(); expect(binding('list')).toBe('list=["hello"]'); }); @@ -9085,9 +10375,8 @@ angularDirective("ng:click", function(expression, element){ angularDirective("ng:submit", function(expression, element) { return function(element) { var self = this; - element.bind('submit', function(event) { + element.bind('submit', function() { self.$apply(expression); - event.preventDefault(); }); }; }); @@ -9098,8 +10387,10 @@ function ngClass(selector) { return function(element) { this.$watch(expression, function(scope, newVal, oldVal) { if (selector(scope.$index)) { - element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal) - element.addClass(isArray(newVal) ? newVal.join(' ') : newVal); + if (oldVal && (newVal !== oldVal)) { + element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal); + } + if (newVal) element.addClass(isArray(newVal) ? newVal.join(' ') : newVal); } }); }; @@ -9107,7 +10398,6 @@ function ngClass(selector) { } /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:class * @@ -9133,27 +10423,26 @@ function ngClass(selector) { Sample Text      - it('should check ng:class', function(){ - expect(element('.doc-example-live span').attr('className')).not(). + it('should check ng:class', function() { + expect(element('.doc-example-live span').prop('className')).not(). toMatch(/ng-input-indicator-wait/); using('.doc-example-live').element(':button:first').click(); - expect(element('.doc-example-live span').attr('className')). + expect(element('.doc-example-live span').prop('className')). toMatch(/ng-input-indicator-wait/); using('.doc-example-live').element(':button:last').click(); - expect(element('.doc-example-live span').attr('className')).not(). + expect(element('.doc-example-live span').prop('className')).not(). toMatch(/ng-input-indicator-wait/); });
*/ -angularDirective("ng:class", ngClass(function(){return true;})); +angularDirective("ng:class", ngClass(function() {return true;})); /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:class-odd * @@ -9182,10 +10471,10 @@ angularDirective("ng:class", ngClass(function(){return true;})); - it('should check ng:class-odd and ng:class-even', function(){ - expect(element('.doc-example-live li:first span').attr('className')). + it('should check ng:class-odd and ng:class-even', function() { + expect(element('.doc-example-live li:first span').prop('className')). toMatch(/ng-format-negative/); - expect(element('.doc-example-live li:last span').attr('className')). + expect(element('.doc-example-live li:last span').prop('className')). toMatch(/ng-input-indicator-wait/); }); @@ -9194,7 +10483,6 @@ angularDirective("ng:class", ngClass(function(){return true;})); angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;})); /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:class-even * @@ -9215,19 +10503,18 @@ angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;}));
  1. - + {{name}}      
- it('should check ng:class-odd and ng:class-even', function(){ - expect(element('.doc-example-live li:first span').attr('className')). - toMatch(/ng-format-negative/); - expect(element('.doc-example-live li:last span').attr('className')). - toMatch(/ng-input-indicator-wait/); + it('should check ng:class-odd and ng:class-even', function() { + expect(element('.doc-example-live li:first span').prop('className')). + toMatch(/odd/); + expect(element('.doc-example-live li:last span').prop('className')). + toMatch(/even/); });
@@ -9235,7 +10522,6 @@ angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;})); angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;})); /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:show * @@ -9250,12 +10536,12 @@ angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;})); * @example - Click me:
+ Click me:
Show: I show up when your checkbox is checked.
Hide: I hide when your checkbox is checked.
- it('should check ng:show / ng:hide', function(){ + it('should check ng:show / ng:hide', function() { expect(element('.doc-example-live span:first:hidden').count()).toEqual(1); expect(element('.doc-example-live span:last:visible').count()).toEqual(1); @@ -9276,7 +10562,6 @@ angularDirective("ng:show", function(expression, element){ }); /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:hide * @@ -9291,12 +10576,12 @@ angularDirective("ng:show", function(expression, element){ * @example - Click me:
+ Click me:
Show: I show up when you checkbox is checked?
Hide: I hide when you checkbox is checked?
- it('should check ng:show / ng:hide', function(){ + it('should check ng:show / ng:hide', function() { expect(element('.doc-example-live span:first:hidden').count()).toEqual(1); expect(element('.doc-example-live span:last:visible').count()).toEqual(1); @@ -9317,7 +10602,6 @@ angularDirective("ng:hide", function(expression, element){ }); /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:style * @@ -9339,29 +10623,23 @@ angularDirective("ng:hide", function(expression, element){
myStyle={{myStyle}}
- it('should check ng:style', function(){ + it('should check ng:style', function() { expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)'); element('.doc-example-live :button[value=set]').click(); - expect(element('.doc-example-live span').css('color')).toBe('red'); + expect(element('.doc-example-live span').css('color')).toBe('rgb(255, 0, 0)'); element('.doc-example-live :button[value=clear]').click(); expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)'); });
*/ -angularDirective("ng:style", function(expression, element){ - return function(element){ - var resetStyle = getStyle(element); - this.$watch(function(scope){ - var style = scope.$eval(expression) || {}, key, mergedStyle = {}; - for(key in style) { - if (resetStyle[key] === undefined) resetStyle[key] = ''; - mergedStyle[key] = style[key]; - } - for(key in resetStyle) { - mergedStyle[key] = mergedStyle[key] || resetStyle[key]; +angularDirective("ng:style", function(expression, element) { + return function(element) { + this.$watch(expression, function(scope, newStyles, oldStyles) { + if (oldStyles && (newStyles !== oldStyles)) { + forEach(oldStyles, function(val, style) { element.css(style, '');}); } - element.css(mergedStyle); + if (newStyles) element.css(newStyles); }); }; }); @@ -9476,34 +10754,6 @@ angularDirective("ng:cloak", function(expression, element) { * Understanding Angular Markup} in the Angular Developer Guide. */ -function parseBindings(string) { - var results = []; - var lastIndex = 0; - var index; - while((index = string.indexOf('{{', lastIndex)) > -1) { - if (lastIndex < index) - results.push(string.substr(lastIndex, index - lastIndex)); - lastIndex = index; - - index = string.indexOf('}}', index); - index = index < 0 ? string.length : index + 2; - - results.push(string.substr(lastIndex, index - lastIndex)); - lastIndex = index; - } - if (lastIndex != string.length) - results.push(string.substr(lastIndex, string.length - lastIndex)); - return results.length === 0 ? [ string ] : results; -} - -function binding(string) { - var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); - return binding ? binding[1] : null; -} - -function hasBindings(bindings) { - return bindings.length > 1 || binding(bindings[0]) !== null; -} angularTextMarkup('{{}}', function(text, textNode, parentElement) { var bindings = parseBindings(text), @@ -9559,7 +10809,6 @@ angularTextMarkup('option', function(text, textNode, parentElement){ }); /** - * @workInProgress * @ngdoc directive * @name angular.directive.ng:href * @@ -9588,13 +10837,13 @@ angularTextMarkup('option', function(text, textNode, parentElement){ * This example uses `link` variable inside `href` attribute: -
+
link 1 (link, don't reload)
link 2 (link, don't reload)
- link 3 (link, reload!)
+ link 3 (link, reload!)
anchor (link, don't reload)
anchor (no link)
- link (link, change hash) + link (link, change hash)
it('should execute ng:click but not reload when href without value', function() { @@ -9610,10 +10859,10 @@ angularTextMarkup('option', function(text, textNode, parentElement){ }); it('should execute ng:click and change url when ng:href specified', function() { + expect(element('#link-3').attr('href')).toBe("/123"); + element('#link-3').click(); - expect(input('value').val()).toEqual('3'); - expect(element('#link-3').attr('href')).toBe("#123"); - expect(browser().location().hash()).toEqual('123'); + expect(browser().window().path()).toEqual('/123'); }); it('should execute ng:click but not reload when href empty string and name specified', function() { @@ -9622,1923 +10871,2475 @@ angularTextMarkup('option', function(text, textNode, parentElement){ expect(element('#link-4').attr('href')).toBe(""); }); - it('should execute ng:click but not reload when no href but name specified', function() { - element('#link-5').click(); - expect(input('value').val()).toEqual('5'); - expect(element('#link-5').attr('href')).toBe(undefined); - }); + it('should execute ng:click but not reload when no href but name specified', function() { + element('#link-5').click(); + expect(input('value').val()).toEqual('5'); + expect(element('#link-5').attr('href')).toBe(undefined); + }); + + it('should only change url when only ng:href', function() { + input('value').enter('6'); + expect(element('#link-6').attr('href')).toBe("/6"); + + element('#link-6').click(); + expect(browser().window().path()).toEqual('/6'); + }); + +
+ */ + +/** + * @ngdoc directive + * @name angular.directive.ng:src + * + * @description + * Using markup like `{{hash}}` in a `src` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until replaces the expression inside + * `{{hash}}`. The `ng:src` attribute solves this problem by placing + * the `src` attribute in the `ng:` namespace. + * + * The buggy way to write it: + *
+ * 
+ * 
+ * + * The correct way to write it: + *
+ * 
+ * 
+ * + * @element ANY + * @param {template} template any string which can contain `{{}}` markup. + */ + +/** + * @ngdoc directive + * @name angular.directive.ng:disabled + * + * @description + * + * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: + *
+ * 
+ * + *
+ *
+ * + * 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 ng:disabled. + * + * @example + + + Click me to toggle:
+ +
+ + it('should toggle button', function() { + expect(element('.doc-example-live :button').prop('disabled')).toBeFalsy(); + input('checked').check(); + expect(element('.doc-example-live :button').prop('disabled')).toBeTruthy(); + }); + +
+ * + * @element ANY + * @param {template} template any string which can contain '{{}}' markup. + */ + + +/** + * @ngdoc directive + * @name angular.directive.ng:checked + * + * @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 ng:checked. + * @example + + + Check me to check both:
+ +
+ + it('should check both checkBoxes', function() { + expect(element('.doc-example-live #checkSlave').prop('checked')).toBeFalsy(); + input('master').check(); + expect(element('.doc-example-live #checkSlave').prop('checked')).toBeTruthy(); + }); + +
+ * + * @element ANY + * @param {template} template any string which can contain '{{}}' markup. + */ + + +/** + * @ngdoc directive + * @name angular.directive.ng:multiple + * + * @description + * The HTML specs do not require browsers to preserve the special attributes such as multiple. + * (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 ng:multiple. + * + * @example + + + Check me check multiple:
+ +
+ + it('should toggle multiple', function() { + expect(element('.doc-example-live #select').prop('multiple')).toBeFalsy(); + input('checked').check(); + expect(element('.doc-example-live #select').prop('multiple')).toBeTruthy(); + }); + +
+ * + * @element ANY + * @param {template} template any string which can contain '{{}}' markup. + */ + + +/** + * @ngdoc directive + * @name angular.directive.ng:readonly + * + * @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 ng:readonly. + * @example + + + Check me to make text readonly:
+ +
+ + it('should toggle readonly attr', function() { + expect(element('.doc-example-live :text').prop('readonly')).toBeFalsy(); + input('checked').check(); + expect(element('.doc-example-live :text').prop('readonly')).toBeTruthy(); + }); + +
+ * + * @element ANY + * @param {template} template any string which can contain '{{}}' markup. + */ + + +/** +* @ngdoc directive +* @name angular.directive.ng:selected +* +* @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 introduce ng:selected. +* @example + + + Check me to select:
+ +
+ + it('should select Greetings!', function() { + expect(element('.doc-example-live #greet').prop('selected')).toBeFalsy(); + input('checked').check(); + expect(element('.doc-example-live #greet').prop('selected')).toBeTruthy(); + }); + +
+* @element ANY +* @param {template} template any string which can contain '{{}}' markup. +*/ + - it('should only change url when only ng:href', function() { - input('value').enter('6'); - element('#link-6').click(); - expect(browser().location().hash()).toEqual('/6'); - expect(element('#link-6').attr('href')).toBe("#/6"); - }); -
-
- */ +var NG_BIND_ATTR = 'ng:bind-attr'; +var SIDE_EFFECT_ATTRS = {}; + +forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) { + SIDE_EFFECT_ATTRS['ng:' + name] = name; +}); + +angularAttrMarkup('{{}}', function(value, name, element){ + // don't process existing attribute markup + if (angularDirective(name) || angularDirective("@" + name)) return; + if (msie && name == 'src') + value = decodeURI(value); + var bindings = parseBindings(value), + bindAttr; + if (hasBindings(bindings) || SIDE_EFFECT_ATTRS[name]) { + element.removeAttr(name); + bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}"); + bindAttr[SIDE_EFFECT_ATTRS[name] || name] = value; + element.attr(NG_BIND_ATTR, toJson(bindAttr)); + } +}); /** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:src - * + * @ngdoc overview + * @name angular.widget * @description - * Using markup like `{{hash}}` in a `src` attribute doesn't - * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until replaces the expression inside - * `{{hash}}`. The `ng:src` attribute solves this problem by placing - * the `src` attribute in the `ng:` namespace. * - * The buggy way to write it: - *
- * 
- * 
+ * An angular widget can be either a custom attribute that modifies an existing DOM element or an + * entirely new DOM element. * - * The correct way to write it: - *
- * 
- * 
+ * During html compilation, widgets are processed after {@link angular.markup markup}, but before + * {@link angular.directive directives}. * - * @element ANY - * @param {template} template any string which can contain `{{}}` markup. + * Following is the list of built-in angular widgets: + * + * * {@link angular.widget.@ng:non-bindable ng:non-bindable} - Blocks angular from processing an + * HTML element. + * * {@link angular.widget.@ng:repeat ng:repeat} - Creates and manages a collection of cloned HTML + * elements. + * * {@link angular.inputType HTML input elements} - Standard HTML input elements data-bound by + * angular. + * * {@link angular.widget.ng:view ng:view} - Works with $route to "include" partial templates + * * {@link angular.widget.ng:switch ng:switch} - Conditionally changes DOM structure + * * {@link angular.widget.ng:include ng:include} - Includes an external HTML fragment + * + * For more information about angular widgets, see {@link guide/dev_guide.compiler.widgets + * Understanding Angular Widgets} in the angular Developer Guide. */ /** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:disabled + * @ngdoc widget + * @name angular.widget.ng:include * * @description + * Fetches, compiles and includes an external HTML fragment. * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: - *
- * 
- * - *
- *
+ * Keep in mind that Same Origin Policy applies to included resources + * (e.g. ng:include won't work for file:// access). * - * 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 ng:disabled. + * @param {string} 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'"`. + * @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an + * instance of angular.module.ng.$rootScope.Scope to set the HTML fragment to. + * @param {string=} onload Expression to evaluate when a new partial is loaded. + * + * @param {string=} autoscroll Whether `ng:include` should call {@link angular.module.ng.$anchorScroll + * $anchorScroll} to scroll the viewport after the content is loaded. + * + * - If the attribute is not set, disable scrolling. + * - If the attribute is set without value, enable scrolling. + * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - - Click me to toggle:
- + + +
+ + url of the template: {{template.url}} +
+ +
- it('should toggle button', function() { - expect(element('.doc-example-live :button').attr('disabled')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live :button').attr('disabled')).toBeTruthy(); + it('should load template1.html', function() { + expect(element('.doc-example-live .ng-include').text()). + toBe('Content of template1.html\n'); + }); + it('should load template2.html', function() { + select('template').option('1'); + expect(element('.doc-example-live .ng-include').text()). + toBe('Content of template2.html\n'); + }); + it('should change to blank', function() { + select('template').option(''); + expect(element('.doc-example-live .ng-include').text()).toEqual(''); });
- * - * @element ANY - * @param {template} template any string which can contain '{{}}' markup. */ +angularWidget('ng:include', function(element){ + var compiler = this, + srcExp = element.attr("src"), + scopeExp = element.attr("scope") || '', + onloadExp = element[0].getAttribute('onload') || '', //workaround for jquery bug #7537 + autoScrollExp = element.attr('autoscroll'); + + if (element[0]['ng:compiled']) { + this.descend(true); + this.directives(true); + } else { + element[0]['ng:compiled'] = true; + return ['$http', '$templateCache', '$anchorScroll', '$element', + function($http, $templateCache, $anchorScroll, element) { + var scope = this, + changeCounter = 0, + childScope; + + function incrementChange() { changeCounter++;} + this.$watch(srcExp, incrementChange); + this.$watch(function() { + var includeScope = scope.$eval(scopeExp); + if (includeScope) return includeScope.$id; + }, incrementChange); + this.$watch(function() {return changeCounter;}, function(scope, newChangeCounter) { + var src = scope.$eval(srcExp), + useScope = scope.$eval(scopeExp); + + function clearContent() { + // if this callback is still desired + if (newChangeCounter === changeCounter) { + if (childScope) childScope.$destroy(); + childScope = null; + element.html(''); + } + } + if (src) { + $http.get(src, {cache: $templateCache}).success(function(response) { + // if this callback is still desired + if (newChangeCounter === changeCounter) { + element.html(response); + if (childScope) childScope.$destroy(); + childScope = useScope ? useScope : scope.$new(); + compiler.compile(element)(childScope); + if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + scope.$eval(onloadExp); + } + }).error(clearContent); + } else { + clearContent(); + } + }); + }]; + } +}); /** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:checked + * @ngdoc widget + * @name angular.widget.ng:switch * * @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 ng:checked. + * Conditionally change the DOM structure. + * + * @usageContent + * ... + * ... + * ... + * ... + * + * @param {*} on expression to match against ng:switch-when. + * @paramDescription + * On child elments add: + * + * * `ng:switch-when`: the case statement to match against. If match then this + * case will be displayed. + * * `ng:switch-default`: the default case when no other casses match. + * * @example - Check me to check both:
- + +
+ + selection={{selection}} +
+ +
Settings Div
+ Home Span + default +
+
- it('should check both checkBoxes', function() { - expect(element('.doc-example-live #checkSlave').attr('checked')).toBeFalsy(); - input('master').check(); - expect(element('.doc-example-live #checkSlave').attr('checked')).toBeTruthy(); + it('should start in settings', function() { + expect(element('.doc-example-live ng\\:switch').text()).toEqual('Settings Div'); + }); + it('should change to home', function() { + select('selection').option('home'); + expect(element('.doc-example-live ng\\:switch').text()).toEqual('Home Span'); + }); + it('should select deafault', function() { + select('selection').option('other'); + expect(element('.doc-example-live ng\\:switch').text()).toEqual('default'); });
- * - * @element ANY - * @param {template} template any string which can contain '{{}}' markup. */ +angularWidget('ng:switch', function(element) { + var compiler = this, + watchExpr = element.attr("on"), + changeExpr = element.attr('change'), + casesTemplate = {}, + defaultCaseTemplate, + children = element.children(), + length = children.length, + child, + when; + + if (!watchExpr) throw new Error("Missing 'on' attribute."); + while(length--) { + child = jqLite(children[length]); + // this needs to be here for IE + child.remove(); + when = child.attr('ng:switch-when'); + if (isString(when)) { + casesTemplate[when] = compiler.compile(child); + } else if (isString(child.attr('ng:switch-default'))) { + defaultCaseTemplate = compiler.compile(child); + } + } + children = null; // release memory; + element.html(''); + return function(element){ + var changeCounter = 0; + var childScope; + var selectedTemplate; -/** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:multiple - * - * @description - * The HTML specs do not require browsers to preserve the special attributes such as multiple. - * (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 ng:multiple. - * - * @example - - - Check me check multiple:
- -
- - it('should toggle multiple', function() { - expect(element('.doc-example-live #select').attr('multiple')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live #select').attr('multiple')).toBeTruthy(); - }); - -
+ this.$watch(watchExpr, function(scope, value) { + element.html(''); + if ((selectedTemplate = casesTemplate[value] || defaultCaseTemplate)) { + changeCounter++; + if (childScope) childScope.$destroy(); + childScope = scope.$new(); + childScope.$eval(changeExpr); + } + }); + + this.$watch(function() {return changeCounter;}, function() { + element.html(''); + if (selectedTemplate) { + selectedTemplate(childScope, function(caseElement) { + element.append(caseElement); + }); + } + }); + }; +}); + + +/* + * Modifies the default behavior of html A tag, so that the default action is prevented when href + * attribute is empty. * - * @element ANY - * @param {template} template any string which can contain '{{}}' markup. + * The reasoning for this change is to allow easy creation of action links with ng:click without + * changing the location or causing page reloads, e.g.: + * Save */ +angularWidget('a', function() { + this.descend(true); + this.directives(true); + + return function(element) { + var hasNgHref = ((element.attr('ng:bind-attr') || '').indexOf('"href":') !== -1); + + // turn link into a link in IE + // but only if it doesn't have name attribute, in which case it's an anchor + if (!hasNgHref && !element.attr('name') && !element.attr('href')) { + element.attr('href', ''); + } + + if (element.attr('href') === '' && !hasNgHref) { + element.bind('click', function(event){ + event.preventDefault(); + }); + } + }; +}); /** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:readonly + * @ngdoc widget + * @name angular.widget.@ng:repeat * * @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 ng:readonly. + * The `ng:repeat` widget instantiates a template once per item from a collection. Each template + * instance gets its own scope, where the given loop variable is set to the current collection item, + * and `$index` is set to the item index or key. + * + * Special properties are exposed on the local scope of each template instance, including: + * + * * `$index` – `{number}` – iterator offset of the repeated element (0..length-1) + * * `$position` – `{string}` – position of the repeated element in the iterator. One of: + * * `'first'`, + * * `'middle'` + * * `'last'` + * + * Note: Although `ng:repeat` looks like a directive, it is actually an attribute widget. + * + * @element ANY + * @param {string} repeat_expression The expression indicating how to enumerate a collection. Two + * formats are currently supported: + * + * * `variable in expression` – where variable is the user defined loop variable and `expression` + * is a scope expression giving the collection to enumerate. + * + * For example: `track in cd.tracks`. + * + * * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, + * and `expression` is the scope expression giving the collection to enumerate. + * + * For example: `(name, age) in {'adam':10, 'amalie':12}`. + * * @example + * This example initializes the scope to a list of names and + * then uses `ng:repeat` to display every person: - Check me to make text readonly:
- +
+ I have {{friends.length}} friends. They are: +
    +
  • + [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +
  • +
+
- it('should toggle readonly attr', function() { - expect(element('.doc-example-live :text').attr('readonly')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live :text').attr('readonly')).toBeTruthy(); - }); + it('should check ng:repeat', function() { + var r = using('.doc-example-live').repeater('ul li'); + expect(r.count()).toBe(2); + expect(r.row(0)).toEqual(["1","John","25"]); + expect(r.row(1)).toEqual(["2","Mary","28"]); + });
- * - * @element ANY - * @param {template} template any string which can contain '{{}}' markup. */ +angularWidget('@ng:repeat', function(expression, element){ + element.removeAttr('ng:repeat'); + element.replaceWith(jqLite('')); + var linker = this.compile(element); + return function(iterStartElement){ + var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), + lhs, rhs, valueIdent, keyIdent; + if (! match) { + throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" + + expression + "'."); + } + lhs = match[1]; + rhs = match[2]; + match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); + if (!match) { + throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + + keyValue + "'."); + } + valueIdent = match[3] || match[1]; + keyIdent = match[2]; + var parentScope = this; + // Store a list of elements from previous run. This is a hash where key is the item from the + // iterator, and the value is an array of objects with following properties. + // - scope: bound scope + // - element: previous element. + // - index: position + // We need an array of these objects since the same object can be returned from the iterator. + // We expect this to be a rare case. + var lastOrder = new HashQueueMap(); + this.$watch(function(scope){ + var index, length, + collection = scope.$eval(rhs), + collectionLength = size(collection, true), + childScope, + // Same as lastOrder but it has the current state. It will become the + // lastOrder on the next iteration. + nextOrder = new HashQueueMap(), + key, value, // key/value of iteration + array, last, // last object information {scope, element, index} + cursor = iterStartElement; // current position of the node + + if (!isArray(collection)) { + // if object, extract keys, sort them and use to determine order of iteration over obj props + array = []; + for(key in collection) { + if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { + array.push(key); + } + } + array.sort(); + } else { + array = collection || []; + } -/** -* @workInProgress -* @ngdoc directive -* @name angular.directive.ng:selected -* -* @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 introduce ng:selected. -* @example - - - Check me to select:
- -
- - it('should select Greetings!', function() { - expect(element('.doc-example-live #greet').attr('selected')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live #greet').attr('selected')).toBeTruthy(); - }); - -
-* @element ANY -* @param {template} template any string which can contain '{{}}' markup. -*/ + // we are not using forEach for perf reasons (trying to avoid #call) + for (index = 0, length = array.length; index < length; index++) { + key = (collection === array) ? index : array[index]; + value = collection[key]; + last = lastOrder.shift(value); + if (last) { + // if we have already seen this object, then we need to reuse the + // associated scope/element + childScope = last.scope; + nextOrder.push(value, last); + + if (index === last.index) { + // do nothing + cursor = last.element; + } else { + // existing item which got moved + last.index = index; + // This may be a noop, if the element is next, but I don't know of a good way to + // figure this out, since it would require extra DOM access, so let's just hope that + // the browsers realizes that it is noop, and treats it as such. + cursor.after(last.element); + cursor = last.element; + } + } else { + // new item which we don't know about + childScope = parentScope.$new(); + } + childScope[valueIdent] = value; + if (keyIdent) childScope[keyIdent] = key; + childScope.$index = index; + childScope.$position = index === 0 ? + 'first' : + (index == collectionLength - 1 ? 'last' : 'middle'); + + if (!last) { + linker(childScope, function(clone){ + cursor.after(clone); + last = { + scope: childScope, + element: (cursor = clone), + index: index + }; + nextOrder.push(value, last); + }); + } + } -var NG_BIND_ATTR = 'ng:bind-attr'; -var SPECIAL_ATTRS = {}; + //shrink children + for (key in lastOrder) { + if (lastOrder.hasOwnProperty(key)) { + array = lastOrder[key]; + while(array.length) { + value = array.pop(); + value.element.remove(); + value.scope.$destroy(); + } + } + } -forEach('src,href,checked,disabled,multiple,readonly,selected'.split(','), function(name) { - SPECIAL_ATTRS['ng:' + name] = name; + lastOrder = nextOrder; + }); + }; }); -angularAttrMarkup('{{}}', function(value, name, element){ - // don't process existing attribute markup - if (angularDirective(name) || angularDirective("@" + name)) return; - if (msie && name == 'src') - value = decodeURI(value); - var bindings = parseBindings(value), - bindAttr; - if (hasBindings(bindings) || SPECIAL_ATTRS[name]) { - element.removeAttr(name); - bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}"); - bindAttr[SPECIAL_ATTRS[name] || name] = value; - element.attr(NG_BIND_ATTR, toJson(bindAttr)); - } -}); /** - * @workInProgress - * @ngdoc overview - * @name angular.widget - * @description + * @ngdoc widget + * @name angular.widget.@ng:non-bindable * - * An angular widget can be either a custom attribute that modifies an existing DOM element or an - * entirely new DOM element. + * @description + * Sometimes it is necessary to write code which looks like bindings but which should be left alone + * by angular. Use `ng:non-bindable` to make angular ignore a chunk of HTML. * - * During html compilation, widgets are processed after {@link angular.markup markup}, but before - * {@link angular.directive directives}. + * Note: `ng:non-bindable` looks like a directive, but is actually an attribute widget. * - * Following is the list of built-in angular widgets: + * @element ANY * - * * {@link angular.widget.@ng:format ng:format} - Formats data for display to user and for storage. - * * {@link angular.widget.@ng:non-bindable ng:non-bindable} - Blocks angular from processing an - * HTML element. - * * {@link angular.widget.@ng:repeat ng:repeat} - Creates and manages a collection of cloned HTML - * elements. - * * {@link angular.widget.@ng:required ng:required} - Verifies presence of user input. - * * {@link angular.widget.@ng:validate ng:validate} - Validates content of user input. - * * {@link angular.widget.HTML HTML input elements} - Standard HTML input elements data-bound by - * angular. - * * {@link angular.widget.ng:view ng:view} - Works with $route to "include" partial templates - * * {@link angular.widget.ng:switch ng:switch} - Conditionally changes DOM structure - * * {@link angular.widget.ng:include ng:include} - Includes an external HTML fragment + * @example + * In this example there are two location where a simple binding (`{{}}`) is present, but the one + * wrapped in `ng:non-bindable` is left alone. * - * For more information about angular widgets, see {@link guide/dev_guide.compiler.widgets - * Understanding Angular Widgets} in the angular Developer Guide. + * @example + + +
Normal: {{1 + 2}}
+
Ignored: {{1 + 2}}
+
+ + it('should check ng:non-bindable', function() { + expect(using('.doc-example-live').binding('1 + 2')).toBe('3'); + expect(using('.doc-example-live').element('div:last').text()). + toMatch(/1 \+ 2/); + }); + +
*/ +angularWidget("@ng:non-bindable", noop); + /** - * @workInProgress * @ngdoc widget - * @name angular.widget.HTML + * @name angular.widget.ng:view * * @description - * The most common widgets you will use will be in the form of the - * standard HTML set. These widgets are bound using the `name` attribute - * to an expression. In addition, they can have `ng:validate`, `ng:required`, - * `ng:format`, `ng:change` attribute to further control their behavior. + * # Overview + * `ng:view` is a widget that complements the {@link angular.module.ng.$route $route} service by + * including the rendered template of the current route into the main layout (`index.html`) file. + * Every time the current route changes, the included view changes with it according to the + * configuration of the `$route` service. * - * @usageContent - * see example below for usage + * This widget provides functionality similar to {@link angular.widget.ng:include ng:include} when + * used like this: + * + * + * + * + * # Advantages + * Compared to `ng:include`, `ng:view` offers these advantages: + * + * - shorter syntax + * - more efficient execution + * - doesn't require `$route` service to be available on the root scope * - * - * - {{input2|json}} - - - radio - String - - <input type="radio" name="input3" value="A">
- <input type="radio" name="input3" value="B"> -
- - - - - {{input3|json}} - - - checkbox - Boolean - <input type="checkbox" name="input4" value="checked"> - - {{input4|json}} - - - pulldown - String - - <select name="input5">
-   <option value="c">C</option>
-   <option value="d">D</option>
- </select>
-
- - - - {{input5|json}} - - - multiselect - Array - - <select name="input6" multiple size="4">
-   <option value="e">E</option>
-   <option value="f">F</option>
- </select>
-
- - - - {{input6|json}} - - + + +
+ overview | + bootstrap | + undefined + +
+ + The view is included below: +
+ +
+ it('should load templates', function() { + element('.doc-example-live a:contains(overview)').click(); + expect(element('.doc-example-live ng\\:view').text()).toMatch(/Developer Guide: Overview/); - it('should exercise text', function(){ - input('input1').enter('Carlos'); - expect(binding('input1')).toEqual('"Carlos"'); - }); - it('should exercise textarea', function(){ - input('input2').enter('Carlos'); - expect(binding('input2')).toEqual('"Carlos"'); - }); - it('should exercise radio', function(){ - expect(binding('input3')).toEqual('null'); - input('input3').select('A'); - expect(binding('input3')).toEqual('"A"'); - input('input3').select('B'); - expect(binding('input3')).toEqual('"B"'); - }); - it('should exercise checkbox', function(){ - expect(binding('input4')).toEqual('false'); - input('input4').check(); - expect(binding('input4')).toEqual('true'); - }); - it('should exercise pulldown', function(){ - expect(binding('input5')).toEqual('"c"'); - select('input5').option('d'); - expect(binding('input5')).toEqual('"d"'); - }); - it('should exercise multiselect', function(){ - expect(binding('input6')).toEqual('[]'); - select('input6').options('e'); - expect(binding('input6')).toEqual('["e"]'); - select('input6').options('e', 'f'); - expect(binding('input6')).toEqual('["e","f"]'); + element('.doc-example-live a:contains(bootstrap)').click(); + expect(element('.doc-example-live ng\\:view').text()).toMatch(/Developer Guide: Initializing Angular: Automatic Initialization/); });
*/ +angularWidget('ng:view', function(element) { + var compiler = this; + + if (!element[0]['ng:compiled']) { + element[0]['ng:compiled'] = true; + return ['$http', '$templateCache', '$route', '$anchorScroll', '$element', + function($http, $templateCache, $route, $anchorScroll, element) { + var template; + var changeCounter = 0; + + this.$on('$afterRouteChange', function() { + changeCounter++; + }); -function modelAccessor(scope, element) { - var expr = element.attr('name'); - var exprFn, assignFn; - if (expr) { - exprFn = parser(expr).assignable(); - assignFn = exprFn.assign; - if (!assignFn) throw new Error("Expression '" + expr + "' is not assignable."); - return { - get: function() { - return exprFn(scope); - }, - set: function(value) { - if (value !== undefined) { - assignFn(scope, value); + this.$watch(function() {return changeCounter;}, function(scope, newChangeCounter) { + var template = $route.current && $route.current.template; + + function clearContent() { + // ignore callback if another route change occured since + if (newChangeCounter == changeCounter) { + element.html(''); + } } - } - }; - } -} -function modelFormattedAccessor(scope, element) { - var accessor = modelAccessor(scope, element), - formatterName = element.attr('ng:format') || NOOP, - formatter = compileFormatter(formatterName); - if (accessor) { - return { - get: function() { - return formatter.format(scope, accessor.get()); - }, - set: function(value) { - return accessor.set(formatter.parse(scope, value)); - } - }; + if (template) { + $http.get(template, {cache: $templateCache}).success(function(response) { + // ignore callback if another route change occured since + if (newChangeCounter == changeCounter) { + element.html(response); + compiler.compile(element)($route.current.scope); + $anchorScroll(); + } + }).error(clearContent); + } else { + clearContent(); + } + }); + }]; + } else { + compiler.descend(true); + compiler.directives(true); } -} - -function compileValidator(expr) { - return parser(expr).validator()(); -} +}); -function compileFormatter(expr) { - return parser(expr).formatter()(); -} /** - * @workInProgress * @ngdoc widget - * @name angular.widget.@ng:validate + * @name angular.widget.ng:pluralize * * @description - * The `ng:validate` attribute widget validates the user input. If the input does not pass - * validation, the `ng-validation-error` CSS class and the `ng:error` attribute are set on the input - * element. Check out {@link angular.validator validators} to find out more. - * - * @param {string} validator The name of a built-in or custom {@link angular.validator validator} to - * to be used. + * # Overview + * ng:pluralize is a widget that displays messages according to en-US localization rules. + * These rules are bundled with angular.js and the rules can be overridden + * (see {@link guide/dev_guide.i18n Angular i18n} dev guide). You configure ng:pluralize by + * specifying the mappings between + * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html + * plural categories} and the strings to be displayed. * - * @element INPUT - * @css ng-validation-error + * # Plural categories and explicit number rules + * There are two + * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html + * plural categories} in Angular's default en-US locale: "one" and "other". * - * @example - * This example shows how the input element becomes red when it contains invalid input. Correct - * the input to make the error disappear. + * While a pural category may match many numbers (for example, in en-US locale, "other" can match + * any number that is not 1), an explicit number rule can only match one number. For example, the + * explicit number rule for "3" matches the number 3. You will see the use of plural categories + * and explicit number rules throughout later parts of this documentation. * - - - I don't validate: -
- - I need an integer or nothing: -
-
- - it('should check ng:validate', function(){ - expect(element('.doc-example-live :input:last').attr('className')). - toMatch(/ng-validation-error/); - - input('value').enter('123'); - expect(element('.doc-example-live :input:last').attr('className')). - not().toMatch(/ng-validation-error/); - }); - -
- */ -/** - * @workInProgress - * @ngdoc widget - * @name angular.widget.@ng:required + * # Configuring ng:pluralize + * You configure ng:pluralize by providing 2 attributes: `count` and `when`. + * You can also provide an optional attribute, `offset`. * - * @description - * The `ng:required` attribute widget validates that the user input is present. It is a special case - * of the {@link angular.widget.@ng:validate ng:validate} attribute widget. + * The value of the `count` attribute can be either a string or an {@link guide/dev_guide.expressions + * Angular expression}; these are evaluated on the current scope for its binded value. * - * @element INPUT - * @css ng-validation-error + * The `when` attribute specifies the mappings between plural categories and the actual + * string to be displayed. The value of the attribute should be a JSON object so that Angular + * can interpret it correctly. * - * @example - * This example shows how the input element becomes red when it contains invalid input. Correct - * the input to make the error disappear. + * The following example shows how to configure ng:pluralize: * - - - I cannot be blank:
-
- - it('should check ng:required', function(){ - expect(element('.doc-example-live :input').attr('className')).toMatch(/ng-validation-error/); - input('value').enter('123'); - expect(element('.doc-example-live :input').attr('className')).not().toMatch(/ng-validation-error/); - }); - -
- */ -/** - * @workInProgress - * @ngdoc widget - * @name angular.widget.@ng:format + *
+ * 
+ * 
+ *
* - * @description - * The `ng:format` attribute widget formats stored data to user-readable text and parses the text - * back to the stored form. You might find this useful, for example, if you collect user input in a - * text field but need to store the data in the model as a list. Check out - * {@link angular.formatter formatters} to learn more. + * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not + * specify this rule, 0 would be matched to the "other" category and "0 people are viewing" + * would be shown instead of "Nobody is viewing". You can specify an explicit number rule for + * other numbers, for example 12, so that instead of showing "12 people are viewing", you can + * show "a dozen people are viewing". * - * @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter} - * to be used. + * You can use a set of closed braces(`{}`) as a placeholder for the number that you want substituted + * into pluralized strings. In the previous example, Angular will replace `{}` with + * `{{personCount}}`. The closed braces `{}` is a placeholder + * for {{numberExpression}}. * - * @element INPUT + * # Configuring ng:pluralize with offset + * The `offset` attribute allows further customization of pluralized text, which can result in + * a better user experience. For example, instead of the message "4 people are viewing this document", + * you might display "John, Kate and 2 others are viewing this document". + * The offset attribute allows you to offset a number by any desired value. + * Let's take a look at an example: * - * @example - * This example shows how the user input is converted from a string and internally represented as an - * array. + *
+ * 
+ * 
+ * 
* - - - Enter a comma separated list of items: - -
list={{list}}
-
- - it('should check ng:format', function(){ - expect(binding('list')).toBe('list=["table","chairs","plate"]'); - input('list').enter(',,, a ,,,'); - expect(binding('list')).toBe('list=["a"]'); - }); - -
- */ -function valueAccessor(scope, element) { - var validatorName = element.attr('ng:validate') || NOOP, - validator = compileValidator(validatorName), - requiredExpr = element.attr('ng:required'), - formatterName = element.attr('ng:format') || NOOP, - formatter = compileFormatter(formatterName), - format, parse, lastError, required, - invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop}; - if (!validator) throw "Validator named '" + validatorName + "' not found."; - format = formatter.format; - parse = formatter.parse; - if (requiredExpr) { - scope.$watch(requiredExpr, function(scope, newValue) { - required = newValue; - validate(); - }); - } else { - required = requiredExpr === ''; - } - - element.data($$validate, validate); - return { - get: function(){ - if (lastError) - elementError(element, NG_VALIDATION_ERROR, null); - try { - var value = parse(scope, element.val()); - validate(); - return value; - } catch (e) { - lastError = e; - elementError(element, NG_VALIDATION_ERROR, e); - } - }, - set: function(value) { - var oldValue = element.val(), - newValue = format(scope, value); - if (oldValue != newValue) { - element.val(newValue || ''); // needed for ie - } - validate(); - } - }; - - function validate() { - var value = trim(element.val()); - if (element[0].disabled || element[0].readOnly) { - elementError(element, NG_VALIDATION_ERROR, null); - invalidWidgets.markValid(element); - } else { - var error, validateScope = inherit(scope, {$element:element}); - error = required && !value - ? 'Required' - : (value ? validator(validateScope, value) : null); - elementError(element, NG_VALIDATION_ERROR, error); - lastError = error; - if (error) { - invalidWidgets.markInvalid(element); - } else { - invalidWidgets.markValid(element); - } - } - } -} - -function checkedAccessor(scope, element) { - var domElement = element[0], elementValue = domElement.value; - return { - get: function(){ - return !!domElement.checked; - }, - set: function(value){ - domElement.checked = toBoolean(value); - } - }; -} - -function radioAccessor(scope, element) { - var domElement = element[0]; - return { - get: function(){ - return domElement.checked ? domElement.value : null; - }, - set: function(value){ - domElement.checked = value == domElement.value; - } - }; -} - -function optionsAccessor(scope, element) { - var formatterName = element.attr('ng:format') || NOOP, - formatter = compileFormatter(formatterName); - return { - get: function(){ - var values = []; - forEach(element[0].options, function(option){ - if (option.selected) values.push(formatter.parse(scope, option.value)); - }); - return values; - }, - set: function(values){ - var keys = {}; - forEach(values, function(value){ - keys[formatter.format(scope, value)] = true; - }); - forEach(element[0].options, function(option){ - option.selected = keys[option.value]; - }); - } - }; -} - -function noopAccessor() { return { get: noop, set: noop }; } - -/* - * TODO: refactor - * - * The table below is not quite right. In some cases the formatter is on the model side - * and in some cases it is on the view side. This is a historical artifact - * - * The concept of model/view accessor is useful for anyone who is trying to develop UI, and - * so it should be exposed to others. There should be a form object which keeps track of the - * accessors and also acts as their factory. It should expose it as an object and allow - * the validator to publish errors to it, so that the the error messages can be bound to it. - * - */ -var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true), - INPUT_TYPE = { - 'text': textWidget, - 'textarea': textWidget, - 'hidden': textWidget, - 'password': textWidget, - 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)), - 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), - 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)), - 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([])) -// 'file': fileWidget??? - }; - - -function initWidgetValue(initValue) { - return function (model, view) { - var value = view.get(); - if (!value && isDefined(initValue)) { - value = copy(initValue); - } - if (isUndefined(model.get()) && isDefined(value)) { - model.set(value); - } - }; -} - -function radioInit(model, view, element) { - var modelValue = model.get(), viewValue = view.get(), input = element[0]; - input.checked = false; - input.name = this.$id + '@' + input.name; - if (isUndefined(modelValue)) { - model.set(modelValue = null); - } - if (modelValue == null && viewValue !== null) { - model.set(viewValue); - } - view.set(modelValue); -} - -/** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:change + * Notice that we are still using two plural categories(one, other), but we added + * three explicit number rules 0, 1 and 2. + * When one person, perhaps John, views the document, "John is viewing" will be shown. + * When three people view the document, no explicit number rule is found, so + * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. + * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * is shown. * - * @description - * The directive executes an expression whenever the input widget changes. + * Note that when you specify offsets, you must provide explicit number rules for + * numbers from 0 up to and including the offset. If you use an offset of 3, for example, + * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for + * plural categories "one" and "other". * - * @element INPUT - * @param {expression} expression to execute. + * @param {string|expression} count The variable to be bounded to. + * @param {string} when The mapping between plural category to its correspoding strings. + * @param {number=} offset Offset to deduct from the total number. * - * @example * @example -
- - changeCount {{textCount}}
- - changeCount {{checkboxCount}}
+ +
+ Person 1:
+ Person 2:
+ Number of People:
+ + + Without Offset: + +
+ + + With Offset(2): + + +
- it('should check ng:change', function(){ - expect(binding('textCount')).toBe('0'); - expect(binding('checkboxCount')).toBe('0'); + it('should show correct pluralized string', function() { + expect(element('.doc-example-live .ng-pluralize:first').text()). + toBe('1 person is viewing.'); + expect(element('.doc-example-live .ng-pluralize:last').text()). + toBe('Igor is viewing.'); + + using('.doc-example-live').input('personCount').enter('0'); + expect(element('.doc-example-live .ng-pluralize:first').text()). + toBe('Nobody is viewing.'); + expect(element('.doc-example-live .ng-pluralize:last').text()). + toBe('Nobody is viewing.'); - using('.doc-example-live').input('text').enter('abc'); - expect(binding('textCount')).toBe('1'); - expect(binding('checkboxCount')).toBe('0'); + using('.doc-example-live').input('personCount').enter('2'); + expect(element('.doc-example-live .ng-pluralize:first').text()). + toBe('2 people are viewing.'); + expect(element('.doc-example-live .ng-pluralize:last').text()). + toBe('Igor and Misko are viewing.'); + using('.doc-example-live').input('personCount').enter('3'); + expect(element('.doc-example-live .ng-pluralize:first').text()). + toBe('3 people are viewing.'); + expect(element('.doc-example-live .ng-pluralize:last').text()). + toBe('Igor, Misko and one other person are viewing.'); - using('.doc-example-live').input('checkbox').check(); - expect(binding('textCount')).toBe('1'); - expect(binding('checkboxCount')).toBe('1'); - }); + using('.doc-example-live').input('personCount').enter('4'); + expect(element('.doc-example-live .ng-pluralize:first').text()). + toBe('4 people are viewing.'); + expect(element('.doc-example-live .ng-pluralize:last').text()). + toBe('Igor, Misko and 2 other people are viewing.'); + }); + + it('should show data-binded names', function() { + using('.doc-example-live').input('personCount').enter('4'); + expect(element('.doc-example-live .ng-pluralize:last').text()). + toBe('Igor, Misko and 2 other people are viewing.'); + + using('.doc-example-live').input('person1').enter('Di'); + using('.doc-example-live').input('person2').enter('Vojta'); + expect(element('.doc-example-live .ng-pluralize:last').text()). + toBe('Di, Vojta and 2 other people are viewing.'); + });
*/ -function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) { - return annotate('$defer', function($defer, element) { +angularWidget('ng:pluralize', function(element) { + var numberExp = element.attr('count'), + whenExp = element.attr('when'), + offset = element.attr('offset') || 0; + + return ['$locale', '$element', function($locale, element) { var scope = this, - model = modelAccessor(scope, element), - view = viewAccessor(scope, element), - ngChange = element.attr('ng:change') || noop, - lastValue; - if (model) { - initFn.call(scope, model, view, element); - scope.$eval(element.attr('ng:init') || noop); - element.bind(events, function(event){ - function handler(){ - var value = view.get(); - if (!textBox || value != lastValue) { - model.set(value); - lastValue = model.get(); - scope.$eval(ngChange); - } - } - event.type == 'keydown' ? $defer(handler) : scope.$apply(handler); - }); - scope.$watch(model.get, function(scope, value) { - if (!equals(lastValue, value)) { - view.set(lastValue = value); - } - }); - } - }); -} + whens = scope.$eval(whenExp), + whensExpFns = {}; -function inputWidgetSelector(element){ - this.directives(true); - this.descend(true); - return INPUT_TYPE[lowercase(element[0].type)] || noop; -} + forEach(whens, function(expression, key) { + whensExpFns[key] = compileBindTemplate(expression.replace(/{}/g, + '{{' + numberExp + '-' + offset + '}}')); + }); -angularWidget('input', inputWidgetSelector); -angularWidget('textarea', inputWidgetSelector); + scope.$watch(function() { + var value = parseFloat(scope.$eval(numberExp)); + if (!isNaN(value)) { + //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, + //check it against pluralization rules in $locale service + if (!whens[value]) value = $locale.pluralCat(value - offset); + return whensExpFns[value](scope, element, true); + } else { + return ''; + } + }, function(scope, newVal) { + element.text(newVal); + }); + }]; +}); /** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:options + * @ngdoc widget + * @name angular.widget.form * * @description - * Dynamically generate a list of `
*/ -// 00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777 -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+(.*)$/; -angularWidget('select', function(element){ +angularWidget('form', function(form){ this.descend(true); this.directives(true); - - var isMultiselect = element.attr('multiple'), - expression = element.attr('ng:options'), - onChange = expressionCompile(element.attr('ng:change') || ""), - match; - - if (!expression) { - return inputWidgetSelector.call(this, element); - } - if (! (match = expression.match(NG_OPTIONS_REGEXP))) { - throw Error( - "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + - " but got '" + expression + "'."); - } - - var displayFn = expressionCompile(match[2] || match[1]), - valueName = match[4] || match[6], - keyName = match[5], - groupByFn = expressionCompile(match[3] || ''), - valueFn = expressionCompile(match[2] ? match[1] : valueName), - valuesFn = expressionCompile(match[7]), - // we can't just jqLite('
- - */ -angularWidget('ng:pluralize', function(element) { - var numberExp = element.attr('count'), - whenExp = element.attr('when'), - offset = element.attr('offset') || 0; + }; - return annotate('$locale', function($locale, element) { - var scope = this, - whens = scope.$eval(whenExp), - whensExpFns = {}; + selectElement.bind('change', function() { + widget.$apply(function() { + var array = []; + forEach(selectElement.children(), function(option){ + if (option.selected) { + array.push(option.value); + } + }); + widget.$emit('$viewChange', array); + }); + }); - forEach(whens, function(expression, key) { - whensExpFns[key] = compileBindTemplate(expression.replace(/{}/g, - '{{' + numberExp + '-' + offset + '}}')); - }); + } - scope.$watch(function() { - var value = parseFloat(scope.$eval(numberExp)); + function Single() { + var widget = this; - if (!isNaN(value)) { - //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, - //check it against pluralization rules in $locale service - if (!whens[value]) value = $locale.pluralCat(value - offset); - return whensExpFns[value](scope, element, true); - } else { - return ''; + widget.$render = function() { + selectElement.val(widget.$viewValue); + }; + + selectElement.bind('change', function() { + widget.$apply(function() { + widget.$emit('$viewChange', selectElement.val()); + }); + }); + + widget.$viewValue = selectElement.val(); + } + + function Options() { + var widget = this, + match; + + if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { + throw Error( + "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + + " but got '" + optionsExp + "'."); } - }, function(scope, newVal) { - element.text(newVal); - }); - }); -}); -var browserSingleton; - -angularService('$browser', function($log){ - if (!browserSingleton) { - browserSingleton = new Browser(window, jqLite(window.document), jqLite(window.document.body), - XHR, $log); - browserSingleton.bind(); - } - return browserSingleton; -}, {$inject:['$log']}); - - -extend(angular, { - // disabled for now until we agree on public name - //'annotate': annotate, - 'compile': compile, - 'scope': createScope, - 'copy': copy, - 'extend': extend, - 'equals': equals, - 'forEach': forEach, - 'injector': createInjector, - 'noop':noop, - 'bind':bind, - 'toJson': toJson, - 'fromJson': fromJson, - 'identity':identity, - 'isUndefined': isUndefined, - 'isDefined': isDefined, - 'isString': isString, - 'isFunction': isFunction, - 'isObject': isObject, - 'isNumber': isNumber, - 'isArray': isArray, - 'version': version, - 'isDate': isDate, - 'lowercase': lowercase, - 'uppercase': uppercase -}); + var widgetScope = this, + displayFn = $parse(match[2] || match[1]), + valueName = match[4] || match[6], + keyName = match[5], + groupByFn = $parse(match[3] || ''), + valueFn = $parse(match[2] ? match[1] : valueName), + valuesFn = $parse(match[7]), + // we can't just jqLite('