From 3f2cce012077bced39185888820034780278d2f7 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 23 Dec 2010 22:21:27 -0800 Subject: [PATCH] the initial version --- .gitignore | 5 + README.md | 146 + app/css/.gitignore | 0 app/css/app.css | 30 + app/img/.gitignore | 0 app/index.html | 21 + app/js/controllers.js | 10 + app/js/filters.js | 1 + app/js/services.js | 21 + app/js/widgets.js | 1 + app/lib/angular/angular-ie-compat.js | 32 + app/lib/angular/angular.js | 8690 ++++++++ app/lib/angular/angular.min.js | 106 + app/lib/angular/version.txt | 1 + app/partials/.gitignore | 0 app/partials/partial1.html | 1 + app/partials/partial2.html | 1 + jsTestDriver.conf | 12 + logs/.gitignore | 2 + scripts/test-server.sh | 12 + scripts/test.sh | 10 + scripts/watchr.rb | 19 + scripts/web-server.js | 243 + test/e2e/runner.html | 10 + test/e2e/scenarios.js | 41 + test/lib/angular/angular-mocks.js | 303 + test/lib/angular/angular-scenario.js | 16805 ++++++++++++++++ test/lib/angular/version.txt | 2 + .../jasmine-jstd-adapter/JasmineAdapter.js | 111 + test/lib/jasmine-jstd-adapter/version.txt | 1 + test/lib/jasmine/MIT.LICENSE | 20 + test/lib/jasmine/index.js | 180 + test/lib/jasmine/jasmine-html.js | 188 + test/lib/jasmine/jasmine.css | 166 + test/lib/jasmine/jasmine.js | 2421 +++ test/lib/jasmine/version.txt | 2 + test/lib/jstestdriver/JsTestDriver.jar | Bin 0 -> 3738920 bytes test/lib/jstestdriver/version.txt | 2 + test/unit/controllersSpec.js | 29 + test/unit/filtersSpec.js | 1 + test/unit/servicesSpec.js | 1 + test/unit/widgetsSpec.js | 1 + 42 files changed, 29648 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/css/.gitignore create mode 100644 app/css/app.css create mode 100644 app/img/.gitignore create mode 100644 app/index.html create mode 100644 app/js/controllers.js create mode 100644 app/js/filters.js create mode 100644 app/js/services.js create mode 100644 app/js/widgets.js create mode 100644 app/lib/angular/angular-ie-compat.js create mode 100644 app/lib/angular/angular.js create mode 100644 app/lib/angular/angular.min.js create mode 100644 app/lib/angular/version.txt create mode 100644 app/partials/.gitignore create mode 100644 app/partials/partial1.html create mode 100644 app/partials/partial2.html create mode 100644 jsTestDriver.conf create mode 100644 logs/.gitignore create mode 100755 scripts/test-server.sh create mode 100755 scripts/test.sh create mode 100755 scripts/watchr.rb create mode 100755 scripts/web-server.js create mode 100644 test/e2e/runner.html create mode 100644 test/e2e/scenarios.js create mode 100644 test/lib/angular/angular-mocks.js create mode 100644 test/lib/angular/angular-scenario.js create mode 100644 test/lib/angular/version.txt create mode 100644 test/lib/jasmine-jstd-adapter/JasmineAdapter.js create mode 100644 test/lib/jasmine-jstd-adapter/version.txt create mode 100644 test/lib/jasmine/MIT.LICENSE create mode 100644 test/lib/jasmine/index.js create mode 100644 test/lib/jasmine/jasmine-html.js create mode 100644 test/lib/jasmine/jasmine.css create mode 100644 test/lib/jasmine/jasmine.js create mode 100644 test/lib/jasmine/version.txt create mode 100644 test/lib/jstestdriver/JsTestDriver.jar create mode 100644 test/lib/jstestdriver/version.txt create mode 100644 test/unit/controllersSpec.js create mode 100644 test/unit/filtersSpec.js create mode 100644 test/unit/servicesSpec.js create mode 100644 test/unit/widgetsSpec.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bbad47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +**/.DS_Store +nbproject +manifest.mf +build.xml + diff --git a/README.md b/README.md new file mode 100644 index 0000000..46445ed --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# angular-seed — the seed for <angular/> apps + +This project is an application skeleton for a typical [angular](http://angularjs.org/) web app. You +can use it to quickly bootstrap your angular webapp projects and dev environment for these projects. + +The seed contains angular libraries, test libraries and a bunch of scripts all preconfigured for +instant web development gratification. Just clone the repo (or download the zip/tarball), start up +our (or yours) webserver and you are ready to develop and test your application. + +The seed app doesn't do much, just shows how to wire two controllers and views together. You can +check it out by opening app/index.html in your browser (might not work file `file://` scheme in +certain browsers, see note below). + +_Note: While angular is client-side-only technology and it's possible to create angular webapps that +don't require a backend server at all, we recommend hosting the project files using a local +webserver during development to avoid issues with security restrictions (sandbox) in browsers. The +sandbox implementation varies between browsers, but quite often prevents things like cookies, xhr, +etc to function properly when an html page is opened via `file://` scheme instead of `http://`._ + + +## How to use angular-seed + +Clone the angular-seed repository and start hacking... + + +### Running the app during development + +You can pick one of these options: + +* serve this repository with your webserver +* install node.js and run `scripts/web-server.js` + +Then navigate your browser to `http://localhost:/app/index.html` to see the app running in +your browser. + + +### Running the app in production + +This really depends on how complex is your app and the overall infrastructure of your system, but +the general rule is that all you need in production are all the files under the `app/` directory. +Everything else should be omitted. + +angular apps are really just a bunch of static html, css and js files that just need to be hosted +somewhere, where they can be accessed by browsers. + +If your angular app is talking to the backend server via xhr or other means, you need to figure +out what is the best way to host the static files to comply with the same origin policy if +applicable. Usually this is done by hosting the files by the backend server or through +reverse-proxying the backend server(s) and a webserver(s). + + +### Running unit tests + +We recommend using [jasmine](http://pivotal.github.com/jasmine/) and +[JsTestDriver](http://code.google.com/p/js-test-driver/) for your unit tests/specs, but you are free +to use whatever works for you. + +Requires java and a local or remote browser. + +* start `scripts/test-server.sh` +* navigate your browser to `http://localhost:9876/` +* click on one of the capture links (preferably the "strict" one) +* run `scripts/test.sh` + + +### Continuous unit testing + +Requires ruby and [watchr](https://github.com/mynyml/watchr) gem. + +* start JSTD server and capture a browser as described above +* start watchr as `watchr scripts/watchr.rb` +* in a different window/tab/editor `tail -f logs/jstd.log` +* edit files in `app/` or `src/` and save them +* watch the log to see updates + +There are many other ways to achieve the same effect. Feel free to use them if you prefer them over +watchr. + + +### End to end testing + +angular ships with a baked-in end-to-end test runner that understands angular, your app and allows +you to write your tests with jasmine-like BDD syntax. + +Requires a webserver, node.js or your backend server that hosts the angular static files. + +* create your end-to-end tests in `test/e2e/scenarios.js` +* serve your project directory with your http/backend server or node.js + `scripts/web-server.js` +* open `http://localhost:port/test/e2e/runner.html` in your browser + + +### Receiving updates from upstream + +When we upgrade angular-seed's repo with newer angular or testing library code, you can just +fetch the changes and merge them into your project with git. + + +## Directory Layout + + app/ --> all of the files to be used in production + css/ --> css files + app.css --> default stylesheet + img/ --> image files + js/ --> javascript files + controllers.js --> application controllers + filters.js --> custom angular filters + services.js --> custom angular services + widgets.js --> custom angular widgets + lib/ --> angular and 3rd party javascript libraries + angular/ + angular.js --> the latest angular js + angular.min.js --> the latest minified angular js + angular-ie-compat.js --> angular patch for IE 6&7 compatibility + version.txt --> version number + partials/ --> angular view partials (partial html templates) + partial1.html + partial2.html + + jsTestDriver.conf --> config file for JsTestDriver + + logs/ --> JSTD and other logs go here (git-ignored) + + scripts/ --> handy shell/js/ruby scripts + test-server.sh --> starts JSTD server + test.sh --> runs all unit tests + watchr.rb --> config script for continuous testing with watchr + web-server.js --> simple development webserver based on node.js + + test/ --> test source files and libraries + e2e/ --> + runner.html --> end-to-end test runner (open in your browser to run) + scenarios.js --> end-to-end specs + lib/ + angular/ --> angular testing libraries + angular-mocks.js --> mocks that replace certain angular services in tests + angular-scenario.js --> angular's scenario (end-to-end) test runner library + version.txt --> version file + jasmine/ --> Pivotal's Jasmine - an elegant BDD-style testing framework + jasmine-jstd-adapter/ --> bridge between JSTD and Jasmine + jstestdriver/ --> JSTD - JavaScript test runner + unit/ --> unit level specs/tests + controllersSpec.js --> specs for controllers + +## Contact + +For more information on angular please check out http://angularjs.org/ diff --git a/app/css/.gitignore b/app/css/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/app/css/app.css b/app/css/app.css new file mode 100644 index 0000000..c925240 --- /dev/null +++ b/app/css/app.css @@ -0,0 +1,30 @@ +/* app css stylesheet */ + +.menu { + list-style: none; + border-bottom: 0.1em solid black; + margin-bottom: 2em; + padding: 0 0 0.5em; +} + +.menu:before { + content: "["; +} + +.menu:after { + content: "]"; +} + +.menu > li { + display: inline; +} + +.menu > li:before { + content: "|"; + padding-right: 0.3em; +} + +.menu > li:nth-child(1):before { + content: ""; + padding: 0; +} diff --git a/app/img/.gitignore b/app/img/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..a599349 --- /dev/null +++ b/app/index.html @@ -0,0 +1,21 @@ + + + + my angular app + + + + + + + + + + + + + + diff --git a/app/js/controllers.js b/app/js/controllers.js new file mode 100644 index 0000000..f0988ad --- /dev/null +++ b/app/js/controllers.js @@ -0,0 +1,10 @@ +/* App Controllers */ + + +function MyCtrl1() {} +MyCtrl1.$inject = []; + + +function MyCtrl2() { +} +MyCtrl1.$inject = []; diff --git a/app/js/filters.js b/app/js/filters.js new file mode 100644 index 0000000..25be4a4 --- /dev/null +++ b/app/js/filters.js @@ -0,0 +1 @@ +/* http://docs.angularjs.org/#!angular.filter */ diff --git a/app/js/services.js b/app/js/services.js new file mode 100644 index 0000000..ca08ff4 --- /dev/null +++ b/app/js/services.js @@ -0,0 +1,21 @@ +/* http://docs.angularjs.org/#!angular.service */ + +/** + * App service which is responsible for the main configuration of the app. + */ +angular.service('myAngularApp', function($route, $location, $window) { + + $route.when('/view1', {template: 'partials/partial1.html', controller: MyCtrl1}); + $route.when('/view2', {template: 'partials/partial2.html', controller: MyCtrl2}); + + $route.onChange(function() { + if ($location.hash === '') { + $location.updateHash('/view1'); + this.$eval(); + } else { + $route.current.scope.params = $route.current.params; + $window.scrollTo(0,0); + } + }); + +}, {$inject:['$route', '$location', '$window'], $creation: 'eager'}); diff --git a/app/js/widgets.js b/app/js/widgets.js new file mode 100644 index 0000000..49f12c1 --- /dev/null +++ b/app/js/widgets.js @@ -0,0 +1 @@ +/* http://docs.angularjs.org/#!angular.widget */ diff --git a/app/lib/angular/angular-ie-compat.js b/app/lib/angular/angular-ie-compat.js new file mode 100644 index 0000000..85964d0 --- /dev/null +++ b/app/lib/angular/angular-ie-compat.js @@ -0,0 +1,32 @@ +/* +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; + var 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")}' + var 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.js b/app/lib/angular/angular.js new file mode 100644 index 0000000..e330543 --- /dev/null +++ b/app/lib/angular/angular.js @@ -0,0 +1,8690 @@ +/** + * The MIT License + * + * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(window, document, previousOnLoad){ +//////////////////////////////////// + +if (typeof document.getAttribute == $undefined) + document.getAttribute = function() {}; + +/** + * @workInProgress + * @ngdoc function + * @name angular.lowercase + * @function + * + * @description Converts string to lowercase + * @param {string} string String to be lowercased. + * @returns {string} Lowercased string. + */ +var lowercase = function (string){ return isString(string) ? string.toLowerCase() : string; }; + + +/** + * @workInProgress + * @ngdoc function + * @name angular.uppercase + * @function + * + * @description Converts string to uppercase. + * @param {string} string String to be uppercased. + * @returns {string} Uppercased string. + */ +var uppercase = function (string){ return isString(string) ? string.toUpperCase() : string; }; + + +var manualLowercase = function (s) { + return isString(s) ? s.replace(/[A-Z]/g, + function (ch) {return fromCharCode(ch.charCodeAt(0) | 32); }) : s; +}; +var manualUppercase = function (s) { + return isString(s) ? s.replace(/[a-z]/g, + function (ch) {return fromCharCode(ch.charCodeAt(0) & ~32); }) : s; +}; + + +// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish +// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods with +// correct but slower alternatives. +if ('i' !== 'I'.toLowerCase()) { + lowercase = manualLowercase; + uppercase = manualUppercase; +} + +function fromCharCode(code) { return String.fromCharCode(code); } + + +var _undefined = undefined, + _null = null, + $$element = '$element', + $$update = '$update', + $$scope = '$scope', + $$validate = '$validate', + $angular = 'angular', + $array = 'array', + $boolean = 'boolean', + $console = 'console', + $date = 'date', + $display = 'display', + $element = 'element', + $function = 'function', + $length = 'length', + $name = 'name', + $none = 'none', + $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', + PRIORITY_FIRST = -99999, + PRIORITY_WATCH = -1000, + PRIORITY_LAST = 99999, + PRIORITY = {'FIRST': PRIORITY_FIRST, 'LAST': PRIORITY_LAST, 'WATCH':PRIORITY_WATCH}, + Error = window.Error, + jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy + _ = window['_'], + /** holds major version number for IE or NaN for real browsers */ + msie = parseInt((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1], 10), + jqLite = jQuery || jqLiteWrap, + slice = Array.prototype.slice, + push = Array.prototype.push, + error = window[$console] ? bind(window[$console], window[$console]['error'] || noop) : noop, + + angular = window[$angular] || (window[$angular] = {}), + angularTextMarkup = extensionMap(angular, 'markup'), + angularAttrMarkup = extensionMap(angular, 'attrMarkup'), + /** @name angular.directive */ + angularDirective = extensionMap(angular, 'directive'), + /** @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'), + nodeName, + rngScript = /^(|.*\/)angular(-.*?)?(\.min)?.js(\?[^#]*)?(#(.*))?$/, + DATE_ISOSTRING_LN = 24; + +/** + * @workInProgress + * @ngdoc function + * @name angular.foreach + * @function + * + * @description + * Invokes the `iterator` function once for each item in `obj` collection. The collection can either + * be an object or an array. The `iterator` function is invoked with `iterator(value, key)`, where + * `value` is the value of an object property or an array element and `key` is the object property + * key or array element index. Optionally, `context` can be specified for the iterator function. + * +
+     var values = {name: 'misko', gender: 'male'};
+     var log = [];
+     angular.foreach(values, function(value, key){
+       this.push(key + ': ' + value);
+     }, log);
+     expect(log).toEqual(['name: misko', 'gender:male']);
+   
+ * + * @param {Object|Array} obj Object to iterate over. + * @param {function()} iterator Iterator function. + * @param {Object} context Object to become context (`this`) for the iterator function. + * @returns {Objet|Array} Reference to `obj`. + */ +function foreach(obj, iterator, context) { + var key; + if (obj) { + if (isFunction(obj)){ + for (key in obj) { + if (key != 'prototype' && key != $length && key != $name && obj.hasOwnProperty(key)) { + iterator.call(context, obj[key], key); + } + } + } else if (obj.forEach) { + obj.forEach(iterator, context); + } else if (isObject(obj) && isNumber(obj.length)) { + for (key = 0; key < obj.length; key++) + iterator.call(context, obj[key], key); + } else { + for (key in obj) + iterator.call(context, obj[key], key); + } + } + return obj; +} + +function foreachSorted(obj, iterator, context) { + var keys = []; + for (var key in obj) keys.push(key); + keys.sort(); + for ( var i = 0; i < keys.length; i++) { + iterator.call(context, obj[keys[i]], keys[i]); + } + return keys; +} + + +function formatError(arg) { + if (arg instanceof Error) { + if (arg.stack) { + arg = arg.stack; + } else if (arg.sourceURL) { + arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; + } + } + return arg; +} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.extend + * @function + * + * @description + * Extends the destination object `dst` by copying all of the properties from the `src` objects to + * `dst`. You can specify multiple `src` objects. + * + * @param {Object} dst The destination object. + * @param {...Object} src The source object(s). + */ +function extend(dst) { + foreach(arguments, function(obj){ + if (obj !== dst) { + foreach(obj, function(value, key){ + dst[key] = value; + }); + } + }); + return dst; +} + + +function inherit(parent, extra) { + return extend(new (extend(function(){}, {prototype:parent}))(), extra); +} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.noop + * @function + * + * @description + * Empty function that performs no operation whatsoever. This function is useful when writing code + * in the functional style. +
+     function foo(callback) {
+       var result = calculateResult();
+       (callback || angular.noop)(result);
+     }
+   
+ */ +function noop() {} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.identity + * @function + * + * @description + * A function that does nothing except for returning its first argument. This function is useful + * when writing code in the functional style. + * +
+     function transformer(transformationFn, value) {
+       return (transformationFn || identity)(value);
+     };
+   
+ */ +function identity($) {return $;} + + +function valueFn(value) {return function(){ return value; };} + + +function extensionMap(angular, name, transform) { + var extPoint; + return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ + name = (transform || identity)(name); + if (isDefined(fn)) { + if (isDefined(extPoint[name])) { + foreach(extPoint[name], function(property, key) { + if (key.charAt(0) == '$' && isUndefined(fn[key])) + fn[key] = property; + }); + } + extPoint[name] = extend(fn, prop || {}); + } + return extPoint[name]; + }); +} + +function jqLiteWrap(element) { + // for some reasons the parentNode of an orphan looks like _null but its typeof is object. + if (element) { + if (isString(element)) { + var div = document.createElement('div'); + div.innerHTML = element; + element = new JQLite(div.childNodes); + } else if (!(element instanceof JQLite) && isElement(element)) { + element = new JQLite(element); + } + } + return element; +} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isUndefined + * @function + * + * @description + * Checks if a reference is undefined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is undefined. + */ +function isUndefined(value){ return typeof value == $undefined; } + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isDefined + * @function + * + * @description + * Checks if a reference is defined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is defined. + */ +function isDefined(value){ return typeof value != $undefined; } + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isObject + * @function + * + * @description + * Checks if a reference is an `Object`. Unlike in JavaScript `null`s are not considered to be + * objects. + * + * @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;} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isString + * @function + * + * @description + * Checks if a reference is a `String`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `String`. + */ +function isString(value){ return typeof value == $string;} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isNumber + * @function + * + * @description + * Checks if a reference is a `Number`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Number`. + */ +function isNumber(value){ return typeof value == $number;} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isDate + * @function + * + * @description + * Checks if value is a date. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Date`. + */ +function isDate(value){ return value instanceof Date; } + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isArray + * @function + * + * @description + * Checks if a reference is an `Array`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Array`. + */ +function isArray(value) { return value instanceof Array; } + + +/** + * @workInProgress + * @ngdoc function + * @name angular.isFunction + * @function + * + * @description + * Checks if a reference is a `Function`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Function`. + */ +function isFunction(value){ return typeof value == $function;} + + +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; } +function isElement(node) { + return node && (node.nodeName || node instanceof JQLite || (jQuery && node instanceof jQuery)); +} + +/** + * HTML class which is the only class which can be used in ng:bind to inline HTML for security reasons. + * @constructor + * @param html raw (unsafe) html + * @param {string=} option if set to 'usafe' then get method will return raw (unsafe/unsanitized) html + */ +function HTML(html, option) { + this.html = html; + this.get = lowercase(option) == 'unsafe' ? + valueFn(html) : + function htmlSanitize() { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf)); + return buf.join(''); + }; +} + +if (msie) { + nodeName = function(element) { + element = element.nodeName ? element : element[0]; + return (element.scopeName && element.scopeName != 'HTML' ) ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; + }; +} else { + nodeName = function(element) { + return element.nodeName ? element.nodeName : element[0].nodeName; + }; +} + +function quickClone(element) { + return jqLite(element[0].cloneNode(true)); +} + +function isVisible(element) { + var rect = element[0].getBoundingClientRect(), + width = (rect.width || (rect.right||0 - rect.left||0)), + height = (rect.height || (rect.bottom||0 - rect.top||0)); + return width>0 && height>0; +} + +function map(obj, iterator, context) { + var results = []; + foreach(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; +} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.Object.size + * @function + * + * @description + * Determines the number of elements in an array or number of properties of an object. + * + * Note: this function is used to augment the Object type in angular expressions. See + * {@link angular.Object} for more info. + * + * @param {Object|Array} obj Object or array to inspect. + * @returns {number} The size of `obj` or `0` if `obj` is not an object or array. + * + * @example + * Number of items in array: {{ [1,2].$size() }}
+ * Number of items in object: {{ {a:1, b:2, c:3}.$size() }}
+ */ +function size(obj) { + var size = 0; + if (obj) { + if (isNumber(obj.length)) { + return obj.length; + } else if (isObject(obj)){ + for (key in obj) + size++; + } + } + return size; +} +function includes(array, obj) { + for ( var i = 0; i < array.length; i++) { + if (obj === array[i]) return true; + } + return false; +} + +function indexOf(array, obj) { + for ( var i = 0; i < array.length; i++) { + if (obj === array[i]) return i; + } + return -1; +} + +function isLeafNode (node) { + if (node) { + switch (node.nodeName) { + case "OPTION": + case "PRE": + case "TITLE": + return true; + } + } + return false; +} + +/** + * @workInProgress + * @ngdoc function + * @name angular.Object.copy + * @function + * + * @description + * Creates a deep copy of `source`. + * + * If `destination` is not provided and `source` is an object or an array, a copy is created & + * returned, otherwise the `source` is returned. + * + * If `destination` is provided, all of its properties will be deleted. + * + * If `source` is an object or an array, all of its members will be copied into the `destination` + * object. + * + * Note: this function is used to augment the Object type in angular expressions. See + * {@link angular.Object} for more info. + * + * @param {*} source The source to be used to make a copy. + * Can be any type including primitives, `null` and `undefined`. + * @param {(Object|Array)=} destination Optional destination into which the source is copied. + * @returns {*} The copy or updated `destination` if `destination` was specified. + * + * @example + Salutation:
+ Name:
+ +
+ + Master is NOT same as form. + +
master={{master}}
+
form={{form}}
+ */ +function copy(source, destination){ + if (!destination) { + destination = source; + if (source) { + if (isArray(source)) { + destination = copy(source, []); + } else if (isDate(source)) { + destination = new Date(source.getTime()); + } else if (isObject(source)) { + destination = copy(source, {}); + } + } + } else { + if (isArray(source)) { + while(destination.length) { + destination.pop(); + } + for ( var i = 0; i < source.length; i++) { + destination.push(copy(source[i])); + } + } else { + foreach(destination, function(value, key){ + delete destination[key]; + }); + for ( var key in source) { + destination[key] = copy(source[key]); + } + } + } + return destination; +} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.Object.equals + * @function + * + * @description + * Determines if two objects or value are equivalent. + * + * To be equivalent, they must pass `==` comparison or be of the same type and have all their + * properties pass `==` comparison. + * + * Supports values types, arrays and objects. + * + * For objects `function` properties and properties that start with `$` are not considered during + * comparisons. + * + * Note: this function is used to augment the Object type in angular expressions. See + * {@link angular.Object} for more info. + * + * @param {*} o1 Object or value to compare. + * @param {*} o2 Object or value to compare. + * @returns {boolean} True if arguments are equal. + * + * @example + Salutation:
+ Name:
+ +
+ + Master is NOT same as form. + +
master={{master}}
+
form={{form}}
+ */ +function equals(o1, o2) { + if (o1 == o2) return true; + var t1 = typeof o1, t2 = typeof o2, length, key, keySet; + if (t1 == t2 && t1 == 'object') { + if (o1 instanceof Array) { + if ((length = o1.length) == o2.length) { + for(key=0; key 2 ? slice.call(arguments, 2, arguments.length) : []; + if (typeof fn == $function) { + return curryArgs.length ? function() { + return arguments.length ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0, arguments.length))) : fn.apply(self, curryArgs); + }: function() { + return arguments.length ? fn.apply(self, arguments) : fn.call(self); + }; + } else { + // in IE, native methods are not functions and so they can not be bound (but they don't need to be) + return fn; + } +} + +function toBoolean(value) { + if (value && value.length !== 0) { + var v = lowercase("" + value); + value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); + } else { + value = false; + } + return value; +} + +function merge(src, dst) { + for ( var key in src) { + var value = dst[key]; + var type = typeof value; + if (type == $undefined) { + dst[key] = fromJson(toJson(src[key])); + } else if (type == 'object' && value.constructor != array && + key.substring(0, 1) != "$") { + merge(src[key], value); + } + } +} + + +/** + * @workInProgress + * @ngdoc function + * @name angular.compile + * @function + * + * @description + * Compiles a piece of HTML or DOM into a {@link angular.scope scope} object. +
+    var scope1 = angular.compile(window.document);
+    scope1.$init();
+
+    var scope2 = angular.compile('
click me
'); + scope2.$init(); +
+ * + * @param {string|DOMElement} element Element to compile. + * @param {Object=} parentScope Scope to become the parent scope of the newly compiled scope. + * @returns {Object} Compiled scope object. + */ +function compile(element, parentScope) { + var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget), + $element = jqLite(element); + return compiler.compile($element)($element, parentScope); +} +///////////////////////////////////////////////// + +/** + * Parses an escaped url query string into key-value pairs. + * @returns Object.<(string|boolean)> + */ +function parseKeyValue(/**string*/keyValue) { + var obj = {}, key_value, key; + foreach((keyValue || "").split('&'), function(keyValue){ + if (keyValue) { + key_value = keyValue.split('='); + key = unescape(key_value[0]); + obj[key] = isDefined(key_value[1]) ? unescape(key_value[1]) : true; + } + }); + return obj; +} + +function toKeyValue(obj) { + var parts = []; + foreach(obj, function(value, key) { + parts.push(escape(key) + (value === true ? '' : '=' + escape(value))); + }); + return parts.length ? parts.join('&') : ''; +} + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:autobind + * @element script + * + * @TODO ng:autobind is not a directive!! it should be documented as bootstrap parameter in a + * separate bootstrap section. + * @TODO rename to ng:autobind to ng:autoboot + * + * @description + * This section explains how to bootstrap your application with angular using either the angular + * javascript file. + * + * + * ## The angular distribution + * Note that there are two versions of the angular javascript file that you can use: + * + * * `angular.js` - the development version - this file is unobfuscated, uncompressed, and thus + * human-readable and useful when developing your angular applications. + * * `angular.min.js` - the production version - this is a minified and obfuscated version of + * `angular.js`. You want to use this version when you want to load a smaller but functionally + * equivalent version of the code in your application. We use the Closure compiler to create this + * file. + * + * + * ## Auto-bootstrap with `ng:autobind` + * The simplest way to get an application up and running is by inserting a script tag in + * your HTML file that bootstraps the `http://code.angularjs.org/angular-x.x.x.min.js` code and uses + * the special `ng:autobind` attribute, like in this snippet of HTML: + * + *
+    <!doctype html>
+    <html xmlns:ng="http://angularjs.org">
+     <head>
+      <script type="text/javascript" src="http://code.angularjs.org/angular-0.9.3.min.js"
+              ng:autobind></script>
+     </head>
+     <body>
+       Hello {{'world'}}!
+     </body>
+    </html>
+ * 
+ * + * The `ng:autobind` attribute tells to compile and manage the whole HTML document. The + * compilation occurs in the page's `onLoad` handler. Note that you don't need to explicitly add an + * `onLoad` event; auto bind mode takes care of all the magic for you. + * + * + * ## Auto-bootstrap with `#autobind` + * In rare cases when you can't define the `ng` namespace before the script tag (e.g. in some CMS + * systems, etc), it is possible to auto-bootstrap angular by appending `#autobind` to the script + * src URL, like in this snippet: + * + *
+    <!doctype html>
+    <html>
+     <head>
+      <script type="text/javascript"
+              src="http://code.angularjs.org/angular-0.9.3.min.js#autobind"></script>
+     </head>
+     <body>
+       <div xmlns:ng="http://angularjs.org">
+         Hello {{'world'}}!
+       </div>
+     </body>
+    </html>
+ * 
+ * + * In this case it's the `#autobind` URL fragment that tells angular to auto-bootstrap. + * + * + * ## Filename Restrictions for Auto-bootstrap + * In order for us to find the auto-bootstrap script attribute or URL fragment, the value of the + * `script` `src` attribute that loads angular script must match one of these naming + * conventions: + * + * - `angular.js` + * - `angular-min.js` + * - `angular-x.x.x.js` + * - `angular-x.x.x.min.js` + * - `angular-x.x.x-xxxxxxxx.js` (dev snapshot) + * - `angular-x.x.x-xxxxxxxx.min.js` (dev snapshot) + * - `angular-bootstrap.js` (used for development of angular) + * + * Optionally, any of the filename format above can be prepended with relative or absolute URL that + * ends with `/`. + * + * + * ## Manual Bootstrap + * Using auto-bootstrap is a handy way to start using , but advanced users who want more + * control over the initialization process might prefer to use manual bootstrap instead. + * + * The best way to get started with manual bootstraping is to look at the magic behind `ng:autobind` + * by writing out each step of the autobind process explicitly. Note that the following code is + * equivalent to the code in the previous section. + * + *
+    <!doctype html>
+    <html xmlns:ng="http://angularjs.org">
+     <head>
+      <script type="text/javascript" src="http://code.angularjs.org/angular-0.9.3.min.js"
+              ng:autobind></script>
+      <script type="text/javascript">
+       (function(window, previousOnLoad){
+         window.onload = function(){
+          try { (previousOnLoad||angular.noop)(); } catch(e) {}
+          angular.compile(window.document).$init();
+         };
+       })(window, window.onload);
+      </script>
+     </head>
+     <body>
+       Hello {{'World'}}!
+     </body>
+    </html>
+ * 
+ * + * This is the sequence that your code should follow if you're bootstrapping angular on your own: + * + * * After the page is loaded, find the root of the HTML template, which is typically the root of + * the document. + * * Run the HTML compiler, which converts the templates into an executable, bi-directionally bound + * application. + * + * + * ##XML Namespace + * *IMPORTANT:* When using you must declare the ng namespace using the xmlns tag. If you + * don't declare the namespace, Internet Explorer does not render widgets properly. + * + *
+ * <html xmlns:ng="http://angularjs.org">
+ * 
+ * + * + * ## Create your own namespace + * If you want to define your own widgets, you must create your own namespace and use that namespace + * to form the fully qualified widget name. For example, you could map the alias `my` to your domain + * and create a widget called my:widget. To create your own namespace, simply add another xmlsn tag + * to your page, create an alias, and set it to your unique domain: + * + *
+ * <html xmlns:ng="http://angularjs.org" xmlns:my="http://mydomain.com">
+ * 
+ * + * + * ## Global Object + * The script creates a single global variable `angular` in the global namespace. All + * APIs are bound to fields of this global object. + * + */ +function angularInit(config){ + if (config.autobind) { + // TODO default to the source of angular.js + var scope = compile(window.document, _null, {'$config':config}), + $browser = scope.$inject('$browser'); + + if (config.css) + $browser.addCss(config.base_url + config.css); + else if(msie<8) + $browser.addJs(config.base_url + config.ie_compat, config.ie_compat_id); + + scope.$init(); + } +} + +function angularJsConfig(document, config) { + 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, '_'); + if (name == 'autobind') value = true; + config[name] = value; + } + }); + } + } + return config; +} +var array = [].constructor; + +/** + * @workInProgress + * @ngdoc function + * @name angular.toJson + * @function + * + * @description + * Serializes the input into a JSON formated string. + * + * @param {Object|Array|Date|string|number} obj Input to jsonify. + * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. + * @returns {string} Jsonified string representing `obj`. + */ +function toJson(obj, pretty) { + var buf = []; + toJsonArray(buf, obj, pretty ? "\n " : _null, []); + return buf.join(''); +} + +/** + * @workInProgress + * @ngdoc function + * @name angular.fromJson + * @function + * + * @description + * Deserializes a string in the JSON format. + * + * @param {string} json JSON string to deserialize. + * @param {boolean} [useNative=false] Use native JSON parser if available + * @returns {Object|Array|Date|string|number} Deserialized thingy. + */ +function fromJson(json, useNative) { + if (!json) return json; + + var obj, p, expression; + + try { + if (useNative && JSON && JSON.parse) { + obj = JSON.parse(json); + return transformDates(obj); + } + + p = parser(json, true); + expression = p.primary(); + p.assertAllConsumed(); + return expression(); + + } catch (e) { + error("fromJson error: ", json, e); + throw e; + } + + // TODO make foreach optionally recursive and remove this function + function transformDates(obj) { + if (isString(obj) && obj.length === DATE_ISOSTRING_LN) { + return angularString.toDate(obj); + } else if (isArray(obj) || isObject(obj)) { + foreach(obj, function(val, name) { + obj[name] = transformDates(val); + }); + } + return obj; + } +} + +angular['toJson'] = toJson; +angular['fromJson'] = fromJson; + +function toJsonArray(buf, obj, pretty, stack) { + if (isObject(obj)) { + if (obj === window) { + buf.push('WINDOW'); + return; + } + + if (obj === document) { + buf.push('DOCUMENT'); + return; + } + + if (includes(stack, obj)) { + buf.push('RECURSION'); + return; + } + stack.push(obj); + } + if (obj === _null) { + buf.push($null); + } else if (obj instanceof RegExp) { + buf.push(angular['String']['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); + } else { + buf.push('' + obj); + } + } else if (isString(obj)) { + return buf.push(angular['String']['quoteUnicode'](obj)); + } else if (isObject(obj)) { + if (isArray(obj)) { + buf.push("["); + var len = obj.length; + var sep = false; + for(var i=0; iTOTAL: without ng:eval-order {{ items.$sum('total') | currency }} +
TOTAL: with ng:eval-order {{ items.$sum('total') | currency }}
+ + + + + + + + + + + + + + + + + + + +
QTYDescriptionCostTotal
{{item.total = item.qty * item.cost | currency}}X
add{{ items.$sum('total') | currency }}
+ * + * @scenario + it('should check ng:format', function(){ + expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99'); + expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$9.99'); + input('item.qty').enter('2'); + expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99'); + expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$19.98'); + }); + */ + + templatize: function(element, elementIndex, priority){ + var self = this, + widget, + fn, + directiveFns = self.directives, + descend = true, + directives = true, + elementName = nodeName(element), + template, + selfApi = { + compile: bind(self, self.compile), + comment:function(text) {return jqLite(document.createComment(text));}, + element:function(type) {return jqLite(document.createElement(type));}, + text:function(text) {return jqLite(document.createTextNode(text));}, + 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;} + }; + try { + priority = element.attr('ng:eval-order') || priority || 0; + } catch (e) { + // for some reason IE throws error under some weird circumstances. so just assume nothing + priority = priority || 0; + } + if (isString(priority)) { + priority = PRIORITY[uppercase(priority)] || parseInt(priority, 10); + } + template = new Template(priority); + 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 (elementName.indexOf(':') > 0) + element.addClass('ng-widget'); + widget = bind(selfApi, widget, element); + } + } + if (widget) { + descend = false; + directives = false; + var parent = element.parent(); + template.addInit(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 1; i++) { + var key = element.shift(); + var newInstance = instance[key]; + if (!newInstance) { + newInstance = {}; + instance[key] = newInstance; + } + instance = newInstance; + } + instance[element.shift()] = value; + return value; +} + +/////////////////////////////////// +var scopeId = 0, + 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, arguments.length))); };\n' + + '}\n'; + } + }); + code += 'return s;'; + fn = Function('s', code); + fn["toString"] = function(){ return code; }; + + return getterFnCache[path] = fn; +} + +/////////////////////////////////// + +function expressionCompile(exp){ + if (typeof exp === $function) return exp; + var fn = compileCache[exp]; + if (!fn) { + var p = parser(exp); + var fnSelf = p.statements(); + p.assertAllConsumed(); + fn = compileCache[exp] = extend( + function(){ return fnSelf(this);}, + {fnSelf: fnSelf}); + } + return fn; +} + +function errorHandlerFor(element, error) { + elementError(element, NG_EXCEPTION, isDefined(error) ? formatError(error) : error); +} + +/** + * @workInProgress + * @ngdoc overview + * @name angular.scope + * + * @description + * Scope is a JavaScript object and the execution context for expressions. You can think about + * scopes as JavaScript objects that have extra APIs for registering watchers. A scope is the model + * in the model-view-controller design pattern. + * + * A few other characteristics of scopes: + * + * - Scopes can be nested. A scope (prototypically) inherits properties from its parent scope. + * - Scopes can be attached (bound) to the HTML DOM tree (the view). + * - A scope {@link angular.scope.$become becomes} `this` for a controller. + * - Scope's {@link angular.scope.$eval $eval} is used to update its view. + * - Scopes can {@link angular.scope.$watch watch} properties and fire events. + * + * # Basic Operations + * Scopes can be created by calling {@link angular.scope() angular.scope()} or by compiling HTML. + * + * {@link angular.widget Widgets} and data bindings register listeners on the current scope to get + * notified of changes to the scope state. When notified, these listeners push the updated state + * through to the DOM. + * + * 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 + '!';
+       });
+
+       expect(scope.greeting).toEqual('Hello World!');
+       scope.name = 'Misko';
+       // scope.$eval() will propagate the change to listeners
+       expect(scope.greeting).toEqual('Hello World!');
+
+       scope.$eval();
+       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 = angular.scope(parent);
+
+     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 + * Scope also acts as a simple dependency injection framework. + * + * **TODO**: more info needed + * + * # When scopes are evaluated + * Anyone can update a scope by calling its {@link angular.scope.$eval $eval()} method. By default + * angular widgets listen to user change events (e.g. the user enters text into text field), copy + * the data from the widget to the scope (the MVC model), and then call the `$eval()` method on the + * root scope to update dependents. This creates a spreadsheet-like behavior: the bound views update + * immediately as the user types into the text field. + * + * Similarly, when a request to fetch data from a server is made and the response comes back, the + * data is written into the model and then $eval() is called to push updates through to the view and + * any other dependents. + * + * Because a change in the model that's triggered either by user input or by server response calls + * `$eval()`, it is unnecessary to call `$eval()` from within your controller. The only time when + * calling `$eval()` is needed, is when implementing a custom widget or service. + * + * Because scopes are inherited, the child scope `$eval()` overrides the parent `$eval()` method. + * So to update the whole page you need to call `$eval()` on the root scope as `$root.$eval()`. + * + * Note: A widget that creates scopes (i.e. {@link angular.widget.@ng:repeat ng:repeat}) is + * responsible for forwarding `$eval()` calls from the parent to those child scopes. That way, + * calling $eval() on the root scope will update the whole page. + * + * + * @TODO THESE PARAMS AND RETURNS ARE NOT RENDERED IN THE TEMPLATE!! FIX THAT! + * @param {Object} parent The scope that should become the parent for the newly created scope. + * @param {Object.=} providers Map of service factory which need to be provided + * for the current scope. Usually {@link angular.service}. + * @param {Object.=} instanceCache Provides pre-instantiated services which should + * append/override services provided by `providers`. + * @returns {Object} Newly created scope. + * + * + * @exampleDescription + * This example demonstrates scope inheritance and property overriding. + * + * In this example, the root scope encompasses the whole HTML DOM tree. This scope has `salutation`, + * `name`, and `names` properties. The {@link angular.widget@ng:repeat ng:repeat} creates a child + * scope, one for each element in the names array. The repeater also assigns $index and name into + * the child scope. + * + * Notice that: + * + * - While the name is set in the child scope it does not change the name defined in the root scope. + * - The child scope inherits the salutation property from the root scope. + * - The $index property does not leak from the child scope to the root scope. + * + * @example +
    +
  • + {{$index}}: {{salutation}} {{name}}! +
  • +
+
+   $index={{$index}}
+   salutation={{salutation}}
+   name={{name}}
+ + @scenario + it('should inherit the salutation property and override the name property', function() { + expect(using('.doc-example-live').repeater('li').row(0)). + toEqual(['0', 'Hello', 'World']); + expect(using('.doc-example-live').repeater('li').row(1)). + toEqual(['1', 'Hello', 'Earth']); + expect(using('.doc-example-live').element('pre').text()). + toBe('$index=\nsalutation=Hello\nname=Misko'); + }); + + */ +function createScope(parent, providers, instanceCache) { + function Parent(){} + parent = Parent.prototype = (parent || {}); + var instance = new Parent(); + var evalLists = {sorted:[]}; + + extend(instance, { + 'this': instance, + $id: (scopeId++), + $parent: parent, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$bind + * @function + * + * @description + * Binds a function `fn` to the current scope. See: {@link angular.bind}. + +
+         var scope = angular.scope();
+         var fn = scope.$bind(function(){
+           return this;
+         });
+         expect(fn()).toEqual(scope);
+       
+ * + * @param {function()} fn Function to be bound. + */ + $bind: bind(instance, bind, instance), + + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$get + * @function + * + * @description + * Returns the value for `property_chain` on the current scope. Unlike in JavaScript, if there + * are any `undefined` intermediary properties, `undefined` is returned instead of throwing an + * exception. + * +
+         var scope = angular.scope();
+         expect(scope.$get('person.name')).toEqual(undefined);
+         scope.person = {};
+         expect(scope.$get('person.name')).toEqual(undefined);
+         scope.person.name = 'misko';
+         expect(scope.$get('person.name')).toEqual('misko');
+       
+ * + * @param {string} property_chain String representing name of a scope property. Optionally + * properties can be chained with `.` (dot), e.g. `'person.name.first'` + * @returns {*} Value for the (nested) property. + */ + $get: bind(instance, getter, instance), + + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$set + * @function + * + * @description + * Assigns a value to a property of the current scope specified via `property_chain`. Unlike in + * JavaScript, if there are any `undefined` intermediary properties, empty objects are created + * and assigned in to them instead of throwing an exception. + * +
+         var scope = angular.scope();
+         expect(scope.person).toEqual(undefined);
+         scope.$set('person.name', 'misko');
+         expect(scope.person).toEqual({name:'misko'});
+         expect(scope.person.name).toEqual('misko');
+       
+ * + * @param {string} property_chain String representing name of a scope property. Optionally + * properties can be chained with `.` (dot), e.g. `'person.name.first'` + * @param {*} value Value to assign to the scope property. + */ + $set: bind(instance, setter, instance), + + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$eval + * @function + * + * @description + * Without the `exp` parameter triggers an eval cycle, for this scope and it's child scopes. + * + * With the `exp` parameter, compiles the expression to a function and calls it with `this` set + * to the current scope and returns the result. + * + * # Example +
+         var scope = angular.scope();
+         scope.a = 1;
+         scope.b = 2;
+
+         expect(scope.$eval('a+b')).toEqual(3);
+         expect(scope.$eval(function(){ return this.a + this.b; })).toEqual(3);
+
+         scope.$onEval('sum = a+b');
+         expect(scope.sum).toEqual(undefined);
+         scope.$eval();
+         expect(scope.sum).toEqual(3);
+       
+ * + * @param {(string|function())=} exp An angular expression to be compiled to a function or a js + * function. + * + * @returns {*} The result of calling compiled `exp` with `this` set to the current scope. + */ + $eval: function(exp) { + var type = typeof exp; + var i, iSize; + var j, jSize; + var queue; + var fn; + if (type == $undefined) { + for ( i = 0, iSize = evalLists.sorted.length; i < iSize; i++) { + for ( queue = evalLists.sorted[i], + jSize = queue.length, + j= 0; j < jSize; j++) { + instance.$tryEval(queue[j].fn, queue[j].handler); + } + } + } else if (type === $function) { + return exp.call(instance); + } else if (type === 'string') { + return expressionCompile(exp).call(instance); + } + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$tryEval + * @function + * + * @description + * Evaluates the expression in the context of the current scope just like + * {@link angular.scope.$eval()} with expression parameter, but also wraps it in a try/catch + * block. + * + * If exception is thrown then `exceptionHandler` is used to handle the exception. + * + * # Example +
+         var scope = angular.scope();
+         scope.error = function(){ throw 'myerror'; };
+         scope.$exceptionHandler = function(e) {this.lastException = e; };
+
+         expect(scope.$eval('error()'));
+         expect(scope.lastException).toEqual('myerror');
+         this.lastException = null;
+
+         expect(scope.$eval('error()'),  function(e) {this.lastException = e; });
+         expect(scope.lastException).toEqual('myerror');
+
+         var body = angular.element(window.document.body);
+         expect(scope.$eval('error()'), body);
+         expect(body.attr('ng-exception')).toEqual('"myerror"');
+         expect(body.hasClass('ng-exception')).toEqual(true);
+       
+ * + * @param {string|function()} expression Angular expression to evaluate. + * @param {function()|DOMElement} exceptionHandler Function to be called or DOMElement to be + * decorated. + * @returns {*} The result of `expression` evaluation. + */ + $tryEval: function (expression, exceptionHandler) { + var type = typeof expression; + try { + if (type == $function) { + return expression.call(instance); + } else if (type == 'string'){ + return expressionCompile(expression).call(instance); + } + } catch (e) { + (instance.$log || {error:error}).error(e); + if (isFunction(exceptionHandler)) { + exceptionHandler(e); + } else if (exceptionHandler) { + errorHandlerFor(exceptionHandler, e); + } else if (isFunction(instance.$exceptionHandler)) { + instance.$exceptionHandler(e); + } + } + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$watch + * @function + * + * @description + * Registers `listener` as a callback to be executed every time the `watchExp` changes. Be aware + * that callback gets, by default, called upon registration, this can be prevented via the + * `initRun` parameter. + * + * # Example +
+         var scope = angular.scope();
+         scope.name = 'misko';
+         scope.counter = 0;
+
+         expect(scope.counter).toEqual(0);
+         scope.$watch('name', 'counter = counter + 1');
+         expect(scope.counter).toEqual(1);
+
+         scope.$eval();
+         expect(scope.counter).toEqual(1);
+
+         scope.name = 'adam';
+         scope.$eval();
+         expect(scope.counter).toEqual(2);
+       
+ * + * @param {function()|string} watchExp Expression that should be evaluated and checked for + * change during each eval cycle. Can be an angular string expression or a function. + * @param {function()|string} listener Function (or angular string expression) that gets called + * every time the value of the `watchExp` changes. The function will be called with two + * parameters, `newValue` and `oldValue`. + * @param {(function()|DOMElement)=} [exceptionHanlder=angular.service.$exceptionHandler] Handler + * that gets called when `watchExp` or `listener` throws an exception. If a DOMElement is + * specified as handler, the element gets decorated by angular with the information about the + * exception. + * @param {boolean=} [initRun=true] Flag that prevents the first execution of the listener upon + * registration. + * + */ + $watch: function(watchExp, listener, exceptionHandler, initRun) { + var watch = expressionCompile(watchExp), + last = watch.call(instance); + listener = expressionCompile(listener); + function watcher(firstRun){ + var value = watch.call(instance), + // we have to save the value because listener can call ourselves => inf loop + lastValue = last; + if (firstRun || lastValue !== value) { + last = value; + instance.$tryEval(function(){ + return listener.call(instance, value, lastValue); + }, exceptionHandler); + } + } + instance.$onEval(PRIORITY_WATCH, watcher); + if (isUndefined(initRun)) initRun = true; + if (initRun) watcher(true); + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$onEval + * @function + * + * @description + * Evaluates the `expr` expression in the context of the current scope during each + * {@link angular.scope.$eval eval cycle}. + * + * # Example +
+         var scope = angular.scope();
+         scope.counter = 0;
+         scope.$onEval('counter = counter + 1');
+         expect(scope.counter).toEqual(0);
+         scope.$eval();
+         expect(scope.counter).toEqual(1);
+       
+ * + * @param {number} [priority=0] Execution priority. Lower priority numbers get executed first. + * @param {string|function()} expr Angular expression or function to be executed. + * @param {(function()|DOMElement)=} [exceptionHandler=angular.service.$exceptionHandler] Handler + * function to call or DOM element to decorate when an exception occurs. + * + */ + $onEval: function(priority, expr, exceptionHandler){ + if (!isNumber(priority)) { + exceptionHandler = expr; + expr = priority; + priority = 0; + } + var evalList = evalLists[priority]; + if (!evalList) { + evalList = evalLists[priority] = []; + evalList.priority = priority; + evalLists.sorted.push(evalList); + evalLists.sorted.sort(function(a,b){return a.priority-b.priority;}); + } + evalList.push({ + fn: expressionCompile(expr), + handler: exceptionHandler + }); + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$become + * @function + * @deprecated This method will be removed before 1.0 + * + * @description + * Modifies the scope to act like an instance of the given class by: + * + * - copying the class's prototype methods + * - applying the class's initialization function to the scope instance (without using the new + * operator) + * + * That makes the scope be a `this` for the given class's methods — effectively an instance of + * the given class with additional (scope) stuff. A scope can later `$become` another class. + * + * `$become` gets used to make the current scope act like an instance of a controller class. + * This allows for use of a controller class in two ways. + * + * - as an ordinary JavaScript class for standalone testing, instantiated using the new + * operator, with no attached view. + * - as a controller for an angular model stored in a scope, "instantiated" by + * `scope.$become(ControllerClass)`. + * + * Either way, the controller's methods refer to the model variables like `this.name`. When + * stored in a scope, the model supports data binding. When bound to a view, {{name}} in the + * HTML template refers to the same variable. + */ + $become: function(Class) { + if (isFunction(Class)) { + instance.constructor = Class; + foreach(Class.prototype, function(fn, name){ + instance[name] = bind(instance, fn); + }); + instance.$inject.apply(instance, concat([Class, instance], arguments, 1)); + + //TODO: backwards compatibility hack, remove when we don't depend on init methods + if (isFunction(Class.prototype.init)) { + instance.init(); + } + } + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$new + * @function + * + * @description + * Creates a new {@link angular.scope scope}, that: + * + * - is a child of the current scope + * - will {@link angular.scope.$become $become} of type specified via `constructor` + * + * @param {function()} constructor Constructor function of the type the new scope should assume. + * @returns {Object} The newly created child scope. + * + */ + $new: function(constructor) { + var child = createScope(instance); + child.$become.apply(instance, concat([constructor], arguments, 1)); + instance.$onEval(child.$eval); + return child; + } + + }); + + if (!parent.$root) { + instance.$root = instance; + instance.$parent = instance; + (instance.$inject = createInjector(instance, providers, instanceCache))(); + } + + return instance; +} +/** + * @ngdoc function + * @name angular.injector + * @function + * + * @description + * Creates an inject function that can be used for dependency injection. + * + * @param {Object=} [providerScope={}] provider's `this` + * @param {Object.=} [providers=angular.service] Map of provider (factory) + * function. + * @param {Object.=} [cache={}] Place where instances are saved for reuse. Can + * also be used to override services speciafied by `providers` (useful in tests). + * @returns {function()} Injector function. + * + * @TODO These docs need a lot of work. Specifically the returned function should be described in + * great detail + we need to provide some examples. + */ +function createInjector(providerScope, providers, cache) { + providers = providers || angularService; + cache = cache || {}; + providerScope = providerScope || {}; + /** + * injection function + * @param value: string, array, object or function. + * @param scope: optional function "this" + * @param args: optional arguments to pass to function after injection + * parameters + * @returns depends on value: + * string: return an instance for the injection key. + * array of keys: returns an array of instances. + * function: look at $inject property of function to determine instances + * and then call the function with instances and `scope`. Any + * additional arguments (`args`) are appended to the function + * arguments. + * object: initialize eager providers and publish them the ones with publish here. + * none: same as object but use providerScope as place to publish. + */ + return function inject(value, scope, args){ + var returnValue, provider, creation; + if (isString(value)) { + if (!cache.hasOwnProperty(value)) { + provider = providers[value]; + if (!provider) throw "Unknown provider for '"+value+"'."; + cache[value] = inject(provider, providerScope); + } + returnValue = cache[value]; + } else if (isArray(value)) { + returnValue = []; + foreach(value, function(name) { + returnValue.push(inject(name)); + }); + } else if (isFunction(value)) { + returnValue = inject(value.$inject || []); + returnValue = value.apply(scope, concat(returnValue, arguments, 2)); + } else if (isObject(value)) { + foreach(providers, function(provider, name){ + creation = provider.$creation; + if (creation == 'eager') { + inject(name); + } + if (creation == 'eager-published') { + setter(value, name, inject(name)); + } + }); + } else { + returnValue = inject(providerScope); + } + return returnValue; + }; +}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", "'":"'", '"':'"'}; + +function lex(text, parseStringsForObjects){ + var dateParseLength = parseStringsForObjects ? DATE_ISOSTRING_LN : -1, + 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 { + throwError("Unexpected next character ", index, index+1); + } + } + lastCh = ch; + } + return tokens; + + function is(chars) { + return chars.indexOf(ch) != -1; + } + + 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); + } + + 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 + "]."); + } + + 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; + } + } + 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); + } + }) + }); + } + + 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; + } + } + 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); + } +} + +///////////////////////////////////////// + +function parser(text, json){ + var ZERO = valueFn(0), + tokens = lex(text, json); + return { + assertAllConsumed: assertAllConsumed, + primary: primary, + statements: statements, + validator: validator, + filter: filter, + //TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular) + watch: watch + }; + + /////////////////////////////////// + function throwError(msg, token) { + throw Error("Parse Error: Token '" + token.text + + "' " + msg + " at column " + + (token.index + 1) + " of expression [" + + text + "] starting at [" + text.substring(token.index) + "]."); + } + + function peekToken() { + if (tokens.length === 0) + throw Error("Unexpected end of expression: " + text); + return tokens[0]; + } + + 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) { + index = token.index; + throwError("is not valid json", token); + } + tokens.shift(); + this.currentToken = token; + 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; + } + + function assertAllConsumed(){ + if (tokens.length !== 0) { + throwError("is extra token not part of expression", tokens[0]); + } + } + + function statements(){ + var statements = []; + while(true) { + if (tokens.length > 0 && !peek('}', ')', ';', ']')) + statements.push(filterChain()); + if (!expect(';')) { + return function (self){ + var value; + for ( var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement) + value = statement(self); + } + return value; + }; + } + } + } + + function filterChain(){ + var left = expression(); + var token; + while(true) { + if ((token = expect('|'))) { + left = binaryFn(left, token.fn, filter()); + } else { + return left; + } + } + } + + function filter(){ + return pipeFunction(angularFilter); + } + + function validator(){ + return pipeFunction(angularValidator); + } + + 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; + }; + } + } + } + + function expression(){ + return assignment(); + } + + 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; + } + } + + function logicalOR(){ + var left = logicalAND(); + var token; + while(true) { + if ((token = expect('||'))) { + left = binaryFn(left, token.fn, logicalAND()); + } else { + return left; + } + } + } + + function logicalAND(){ + var left = equality(); + var token; + if ((token = expect('&&'))) { + left = binaryFn(left, token.fn, logicalAND()); + } + return left; + } + + function equality(){ + var left = relational(); + var token; + if ((token = expect('==','!='))) { + left = binaryFn(left, token.fn, equality()); + } + return left; + } + + function relational(){ + var left = additive(); + var token; + if (token = expect('<', '>', '<=', '>=')) { + left = binaryFn(left, token.fn, relational()); + } + return left; + } + + function additive(){ + var left = multiplicative(); + var token; + while(token = expect('+','-')) { + left = binaryFn(left, token.fn, multiplicative()); + } + return left; + } + + function multiplicative(){ + var left = unary(); + var token; + while(token = expect('*','/','%')) { + left = binaryFn(left, token.fn, unary()); + } + 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(); + } + } + + 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 (typeof instance != $function) { + throwError("should be a function", token); + } + return instance; + } + + 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); + } + } + 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); + var i = indexFn(self); + return (o) ? o[i] : _undefined; + }, { + assign:function(self, value){ + return obj(self)[indexFn(self)] = value; + } + }); + } + + 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]); + }; + } + + // 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; + }; + } + + 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; + }; + } + + //TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular) + function watch () { + var decl = []; + while(hasTokens()) { + decl.push(watchDecl()); + if (!expect(';')) { + assertAllConsumed(); + } + } + assertAllConsumed(); + return function (self){ + for ( var i = 0; i < decl.length; i++) { + var d = decl[i](self); + self.addListener(d.name, d.fn); + } + }; + } + + 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}; + }; + } +} + + + + + + +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; + } + }); +} + +Route.prototype = { + url: function(params) { + var path = []; + var self = this; + var url = this.template; + params = params || {}; + foreach(this.urlParams, function(_, urlParam){ + var value = params[urlParam] || self.defaults[urlParam] || ""; + url = url.replace(new RegExp(":" + urlParam + "(\\W)"), value + "$1"); + }); + url = url.replace(/\/?#$/, ''); + var query = []; + foreachSorted(params, function(value, key){ + if (!self.urlParams[key]) { + query.push(encodeURI(key) + '=' + encodeURI(value)); + } + }); + url = url.replace(/\/*$/, ''); + return url + (query.length ? '?' + query.join('&') : ''); + } +}; + +function ResourceFactory(xhr) { + this.xhr = xhr; +} + +ResourceFactory.DEFAULT_ACTIONS = { + 'get': {method:'GET'}, + 'save': {method:'POST'}, + 'query': {method:'GET', isArray:true}, + 'remove': {method:'DELETE'}, + 'delete': {method:'DELETE'} +}; + +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 Resource(value){ + copy(value || {}, this); + } + + foreach(actions, function(action, name){ + var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; + Resource[name] = function (a1, a2, a3) { + var params = {}; + var data; + var callback = noop; + switch(arguments.length) { + case 3: callback = a3; + case 2: + if (isFunction(a2)) { + callback = a2; + } else { + params = a1; + data = a2; + break; + } + case 1: + if (isFunction(a1)) callback = a1; + else if (isPostOrPut) data = a1; + else params = a1; + break; + case 0: break; + default: + throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments."; + } + + 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, clear) { + if (status == 200) { + if (action.isArray) { + value.length = 0; + foreach(response, function(item){ + value.push(new Resource(item)); + }); + } else { + copy(response, value); + } + (callback||noop)(value); + } else { + throw {status: status, response:response, message: status + ": " + response}; + } + }, + action.verifyCache); + return value; + }; + + Resource.bind = function(additionalParamDefaults){ + return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + Resource.prototype['$' + name] = function(a1, a2){ + var params = extractParams(this); + var callback = noop; + switch(arguments.length) { + case 2: params = a1; callback = a2; + case 1: if (typeof a1 == $function) callback = a1; else params = a1; + case 0: break; + default: + throw "Expected between 1-2 arguments [params, callback], got " + arguments.length + " arguments."; + } + var data = isPostOrPut ? this : _undefined; + Resource[name].call(this, params, data, callback); + }; + }); + return Resource; + } +}; +////////////////////////////// +// 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."); +}; + +function Browser(location, document, head, XHR, $log, setTimeout) { + var self = this; + self.isMock = false; + + ////////////////////////////////////////////////////////////// + // XHR API + ////////////////////////////////////////////////////////////// + var idCounter = 0; + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; + + + /** + * 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, slice.call(arguments, 1)); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while(outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + $log.error(e); + } + } + } + } + } + + /** + * @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 + * @param {function(number, string)} callback Function that will be called on response + * + * @description + * Send ajax request + */ + self.xhr = function(method, url, post, callback) { + if (isFunction(post)) { + callback = post; + post = _null; + } + if (lowercase(method) == 'json') { + var callbackId = "angular_" + Math.random() + '_' + (idCounter++); + callbackId = callbackId.replace(/\d\./, ''); + var script = document[0].createElement('script'); + script.type = 'text/javascript'; + script.src = url.replace('JSON_CALLBACK', callbackId); + window[callbackId] = function(data){ + window[callbackId] = _undefined; + callback(200, data); + }; + head.append(script); + } else { + var xhr = new XHR(); + xhr.open(method, url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.setRequestHeader("Accept", "application/json, text/plain, */*"); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + outstandingRequestCount ++; + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + completeOutstandingRequest(callback, xhr.status || 200, xhr.responseText); + } + }; + xhr.send(post || ''); + } + }; + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$browser#notifyWhenNoOutstandingRequests + * @methodOf angular.service.$browser + * + * @param {function()} callback Function that will be called when no outstanding request + */ + self.notifyWhenNoOutstandingRequests = function(callback) { + if (outstandingRequestCount === 0) { + callback(); + } else { + outstandingRequestCallbacks.push(callback); + } + }; + + ////////////////////////////////////////////////////////////// + // Poll Watcher API + ////////////////////////////////////////////////////////////// + var pollFns = []; + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$browser#poll + * @methodOf angular.service.$browser + */ + self.poll = function() { + foreach(pollFns, function(pollFn){ pollFn(); }); + }; + + /** + * @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 + * + * @returns {function()} the added function + */ + self.addPollFn = function(fn) { + pollFns.push(fn); + return fn; + }; + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$browser#startPoller + * @methodOf angular.service.$browser + * + * @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. + */ + self.startPoller = function(interval, setTimeout) { + (function check(){ + self.poll(); + setTimeout(check, interval); + })(); + }; + + ////////////////////////////////////////////////////////////// + // URL API + ////////////////////////////////////////////////////////////// + + /** + * @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) { + var existingURL = location.href; + if (!existingURL.match(/#/)) existingURL += '#'; + if (!url.match(/#/)) url += '#'; + location.href = url; + }; + + /** + * @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 location.href; + }; + + ////////////////////////////////////////////////////////////// + // Cookies API + ////////////////////////////////////////////////////////////// + var rawDocument = document[0]; + 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, i, keyValue; + + 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 )"); + } + } + } + } else { + if (rawDocument.cookie !== lastCookieString) { + lastCookieString = rawDocument.cookie; + cookieArray = lastCookieString.split("; "); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + keyValue = cookieArray[i].split("="); + if (keyValue.length === 2) { //ignore nameless cookies + lastCookies[unescape(keyValue[0])] = unescape(keyValue[1]); + } + } + } + return lastCookies; + } + }; + + + /** + * @workInProgress + * @ngdoc + * @name angular.service.$browser#defer + * @methodOf angular.service.$browser + * + * @description + * Executes a fn asynchroniously via `setTimeout(fn, 0)`. + * + * 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 programaticaly flushed via + * `$browser.defer.flush()`. + * + * @param {function()} fn A function, who's execution should be defered. + */ + self.defer = function(fn) { + outstandingRequestCount++; + setTimeout(function() { completeOutstandingRequest(fn); }, 0); + }; + + ////////////////////////////////////////////////////////////// + // Misc API + ////////////////////////////////////////////////////////////// + var hoverListener = noop; + + /** + * @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 hover event + * occurs. + */ + self.hover = function(listener) { hoverListener = listener; }; + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$browser#bind + * @methodOf angular.service.$browser + * + * @description + * Register hover function to real browser + */ + 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; + }); + }; + + + /** + * @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. + */ + self.addCss = function(url) { + var link = jqLite(rawDocument.createElement('link')); + link.attr('rel', 'stylesheet'); + link.attr('type', 'text/css'); + link.attr('href', url); + head.append(link); + }; + + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$browser#addJs + * @methodOf angular.service.$browser + * + * @param {string} url Url to js file + * @param {string=} dom_id Optional id for the script tag + * + * @description + * Adds a script tag to the head. + */ + self.addJs = function(url, dom_id) { + var script = jqLite(rawDocument.createElement('script')); + script.attr('type', 'text/javascript'); + script.attr('src', url); + if (dom_id) script.attr('id', dom_id); + head.append(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 + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + +// 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) + +// Empty Elements - HTML 4.01 +var emptyElements = makeMap("area,br,col,hr,img"); + +// 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"); + +// 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); + +//see: http://www.w3.org/TR/html4/index/attributes.html +//Attributes that have their values filled in disabled="disabled" +var fillAttrs = makeMap("compact,ismap,nohref,nowrap"); +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,href,longdesc,src,usemap"); +var validAttrs = extend({}, fillAttrs, uriAttrs, makeMap( + 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ + 'color,cols,colspan,coords,dir,face,headers,height,hreflang,hspace,'+ + 'lang,language,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; + } + + // 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; + } + + // 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; + } + } + + if ( chars ) { + index = html.indexOf("<"); + + var text = index < 0 ? html : html.substring( 0, index ); + html = index < 0 ? "" : html.substring( index ); + + handler.chars( decodeEntities(text) ); + } + + } else { + html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ + text = text. + replace(COMMENT_REGEXP, "$1"). + replace(CDATA_REGEXP, "$1"); + + handler.chars( decodeEntities(text) ); + + return ""; + }); + + parseEndTag( "", stack.last() ); + } + + if ( html == last ) { + throw "Parse Error: " + html; + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag( tag, tagName, rest, unary ) { + tagName = lowercase(tagName); + if ( blockElements[ tagName ] ) { + while ( stack.last() && inlineElements[ stack.last() ] ) { + parseEndTag( "", stack.last() ); + } + } + + if ( closeSelfElements[ tagName ] && stack.last() == tagName ) { + parseEndTag( "", tagName ); + } + + unary = emptyElements[ tagName ] || !!unary; + + if ( !unary ) + stack.push( tagName ); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, function(match, name) { + var value = arguments[2] ? arguments[2] : + arguments[3] ? arguments[3] : + arguments[4] ? arguments[4] : + fillAttrs[name] ? name : ""; + + attrs[name] = decodeEntities(value); //value.replace(/(^|[^\\])"/g, '$1\\\"') //" + }); + + 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; + + if ( pos >= 0 ) { + // Close all the open elements, up the stack + for ( i = stack.length - 1; i >= pos; i-- ) + handler.end( stack[ i ] ); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +/** + * @param str 'key1,key2,...' + * @returns {object} in the form of {key1:true, key2:true, ...} + */ +function makeMap(str){ + var obj = {}, items = str.split(","), i; + for ( i = 0; i < items.length; i++ ) + obj[ items[i] ] = true; + return obj; +} + +/** + * 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, '>'); +} + +/** + * 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('"'); + } + }); + 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)); + } + } + }; +} +////////////////////////////////// +//JQLite +////////////////////////////////// + +var jqCache = {}, + jqName = 'ng-' + new Date().getTime(), + jqId = 1, + addEventListener = (window.document.attachEvent ? + function(element, type, fn) {element.attachEvent('on' + type, fn);} : + function(element, type, fn) {element.addEventListener(type, fn, false);}), + removeEventListener = (window.document.detachEvent ? + function(element, type, fn) {element.detachEvent('on' + type, fn); } : + function(element, type, fn) { element.removeEventListener(type, fn, false); }); + +function jqNextId() { return (jqId++); } + +function jqClearData(element) { + var cacheId = element[jqName], + cache = jqCache[cacheId]; + if (cache) { + foreach(cache.bind || {}, function(fn, type){ + removeEventListener(element, type, fn); + }); + delete jqCache[cacheId]; + if (msie) + element[jqName] = ''; // ie does not allow deletion of attributes on elements. + else + delete element[jqName]; + } +} + +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; +} + +function JQLite(element) { + if (isElement(element)) { + this[0] = element; + this.length = 1; + } else if (isDefined(element.length) && element.item) { + for(var i=0; i < element.length; i++) { + this[i] = element[i]; + } + this.length = element.length; + } +} + +JQLite.prototype = { + data: function(key, value) { + var element = this[0], + 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; + } + }, + + removeData: function(){ + jqClearData(this[0]); + }, + + dealoc: function(){ + (function dealoc(element){ + jqClearData(element); + for ( var i = 0, children = element.childNodes; i < children.length; i++) { + dealoc(children[i]); + } + })(this[0]); + }, + + bind: function(type, fn){ + var self = this, + element = self[0], + bind = self.data('bind'), + eventHandler; + if (!bind) this.data('bind', bind = {}); + foreach(type.split(' '), function(type){ + 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 + }; + } + foreach(eventHandler.fns, function(fn){ + fn.call(self, event); + }); + }; + eventHandler.fns = []; + addEventListener(element, type, eventHandler); + } + eventHandler.fns.push(fn); + }); + }, + + replaceWith: function(replaceNode) { + this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); + }, + + children: function() { + return new JQLite(this[0].childNodes); + }, + + append: function(node) { + var self = this[0]; + node = jqLite(node); + foreach(node, function(child){ + self.appendChild(child); + }); + }, + + remove: function() { + this.dealoc(); + var parentNode = this[0].parentNode; + if (parentNode) parentNode.removeChild(this[0]); + }, + + removeAttr: function(name) { + this[0].removeAttribute(name); + }, + + after: function(element) { + this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling); + }, + + hasClass: function(selector) { + var className = " " + selector + " "; + if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) { + return true; + } + return false; + }, + + removeClass: function(selector) { + this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", "")); + }, + + toggleClass: function(selector, condition) { + var self = this; + (condition ? self.addClass : self.removeClass).call(self, selector); + }, + + addClass: function( selector ) { + if (!this.hasClass(selector)) { + this[0].className = trim(this[0].className + ' ' + selector); + } + }, + + css: function(name, value) { + var style = this[0].style; + if (isString(name)) { + if (isDefined(value)) { + style[name] = value; + } else { + return style[name]; + } + } else { + extend(style, name); + } + }, + + attr: function(name, value){ + var e = this[0]; + if (isObject(name)) { + foreach(name, function(value, name){ + e.setAttribute(name, value); + }); + } else if (isDefined(value)) { + e.setAttribute(name, value); + } else { + // the extra argument is to get the right thing for a.href in IE, see jQuery code + return e.getAttribute(name, 2); + } + }, + + text: function(value) { + if (isDefined(value)) { + this[0].textContent = value; + } + return this[0].textContent; + }, + + val: function(value) { + if (isDefined(value)) { + this[0].value = value; + } + return this[0].value; + }, + + html: function(value) { + if (isDefined(value)) { + var i = 0, childNodes = this[0].childNodes; + for ( ; i < childNodes.length; i++) { + jqLite(childNodes[i]).dealoc(); + } + this[0].innerHTML = value; + } + return this[0].innerHTML; + }, + + parent: function() { + return jqLite(this[0].parentNode); + }, + + clone: function() { return jqLite(this[0].cloneNode(true)); } +}; + +if (msie) { + extend(JQLite.prototype, { + text: function(value) { + var e = this[0]; + // NodeType == 3 is text node + if (e.nodeType == 3) { + if (isDefined(value)) e.nodeValue = value; + return e.nodeValue; + } else { + if (isDefined(value)) e.innerText = value; + return e.innerText; + } + } + }); +} +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; + } + return type; + } +}; + + +/** + * @workInProgress + * @ngdoc overview + * @name angular.Object + * @function + * + * @description + * Utility functions for manipulation with JavaScript objects. + * + * These functions are exposed in two ways: + * + * - **in angular expressions**: the functions are bound to all objects and augment the Object + * type. The names of these methods are prefixed with `$` character to minimize naming collisions. + * To call a method, invoke the function without the first argument, e.g, `myObject.$foo(param2)`. + * + * - **in JavaScript code**: the functions don't augment the Object type and must be invoked as + * functions of `angular.Object` as `angular.Object.foo(myObject, param2)`. + * + */ +var angularCollection = { + 'copy': copy, + 'size': size, + 'equals': equals +}; +var angularObject = { + 'extend': extend +}; + +/** + * @workInProgress + * @ngdoc overview + * @name angular.Array + * + * @description + * Utility functions for manipulation with JavaScript Array objects. + * + * These functions are exposed in two ways: + * + * - **in angular expressions**: the functions are bound to the Array objects and augment the Array + * type as array methods. The names of these methods are prefixed with `$` character to minimize + * naming collisions. To call a method, invoke `myArrayObject.$foo(params)`. + * + * - **in JavaScript code**: the functions don't augment the Array type and must be invoked as + * functions of `angular.Array` as `angular.Array.foo(myArrayObject, params)`. + * + */ +var angularArray = { + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.indexOf + * @function + * + * @description + * Determines the index of `value` in `array`. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array Array to search. + * @param {*} value Value to search for. + * @returns {number} The position of the element in `array`. The position is 0-based. `-1` is returned if the value can't be found. + * + * @example +
+
+ Index of '{{bookName}}' in the list {{books}} is {{books.$indexOf(bookName)}}. + + @scenario + it('should correctly calculate the initial index', function() { + expect(binding('books.$indexOf(bookName)')).toBe('2'); + }); + + it('should recalculate', function() { + input('bookName').enter('foo'); + expect(binding('books.$indexOf(bookName)')).toBe('-1'); + + input('bookName').enter('Moby Dick'); + expect(binding('books.$indexOf(bookName)')).toBe('0'); + }); + */ + 'indexOf': indexOf, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.sum + * @function + * + * @description + * This function calculates the sum of all numbers in `array`. If the `expressions` is supplied, + * it is evaluated once for each element in `array` and then the sum of these values is returned. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @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}}
+ + @scenario + //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'); + }); + + it('should add an entry and recalculate', function() { + element('.doc-example 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'); + + 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 = angular['Function']['compile'](expression); + var sum = 0; + for (var i = 0; i < array.length; i++) { + var value = 1 * fn(array[i]); + if (!isNaN(value)){ + sum += value; + } + } + return sum; + }, + + + /** + * @workInProgress + * @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 info. + * + * @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}} + + @scenario + it('should initialize the task list with for tasks', function() { + expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(4); + expect(repeater('.doc-example ul li', 'task in tasks').column('task')). + toEqual(['Learn Angular', 'Read Documentation', 'Check out demos', + 'Build cool applications']); + }); + + it('should initialize the task list with for tasks', function() { + element('.doc-example ul li a:contains("X"):first').click(); + expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(3); + + element('.doc-example ul li a:contains("X"):last').click(); + expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(2); + + expect(repeater('.doc-example 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; + }, + + + /** + * @workInProgress + * @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 info. + * + * @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}}
+ + @scenario + 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']); + }); + */ + 'filter':function(array, expression) { + 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){ + if (text.charAt(0) === '!') { + 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; + } + } + return false; + case "array": + for ( var i = 0; i < obj.length; i++) { + if (search(obj[i], text)) { + return true; + } + } + return false; + default: + return false; + } + }; + switch (typeof expression) { + case "boolean": + case "number": + case "string": + expression = {$:expression}; + case "object": + for (var key in expression) { + if (key == '$') { + (function(){ + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(value, text); + }); + })(); + } else { + (function(){ + var path = key; + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(getter(value, path), text); + }); + })(); + } + } + break; + case $function: + predicates.push(expression); + break; + default: + return array; + } + var filtered = []; + for ( var j = 0; j < array.length; j++) { + var value = array[j]; + if (predicates.check(value)) { + filtered.push(value); + } + } + return filtered; + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.add + * @function + * + * @description + * `add` is a function similar to JavaScript's `Array#push` method, in that it appends a new + * element to an array, but it differs in that the value being added is optional and defaults to + * an emty object. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array The array expand. + * @param {*=} [value={}] The value to be added. + * @returns {Array} The expanded array. + * + * @exampleDescription + * This example shows how an initially empty array can be filled with objects created from user + * input via the `$add` method. + * + * @example + [add empty] + [add 'John'] + [add 'Mary'] + +
    +
  • + + + [X] +
  • +
+
people = {{people}}
+ + @scenario + beforeEach(function() { + expect(binding('people')).toBe('people = []'); + }); + + it('should create an empty record when "add empty" is clicked', function() { + element('.doc-example a:contains("add empty")').click(); + expect(binding('people')).toBe('people = [{\n "name":"",\n "sex":null}]'); + }); + + it('should create a "John" record when "add \'John\'" is clicked', function() { + element('.doc-example a:contains("add \'John\'")').click(); + expect(binding('people')).toBe('people = [{\n "name":"John",\n "sex":"male"}]'); + }); + + it('should create a "Mary" record when "add \'Mary\'" is clicked', function() { + element('.doc-example 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 a:contains("add empty")').click(); + element('.doc-example li a:contains("X"):first').click(); + expect(binding('people')).toBe('people = []'); + }); + */ + 'add':function(array, value) { + array.push(isUndefined(value)? {} : value); + return array; + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.count + * @function + * + * @description + * Determines the number of elements in an array. Optionally it will count only those elements + * for which the `condition` evaluets to `true`. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array The array to count elements in. + * @param {(function()|string)=} condition A function to be evaluated or angular expression to be + * compiled and evaluated. The element that is currently being iterated over, is exposed to + * the `condition` as `this`. + * @returns {number} Number of elements in the array (for which the 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')}}

+ + @scenario + it('should calculate counts', function() { + expect(binding('items.$count(\'points==1\')')).toEqual(2); + expect(binding('items.$count(\'points>1\')')).toEqual(1); + }); + + it('should recalculate when updated', function() { + using('.doc-example 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 = angular['Function']['compile'](condition), count = 0; + foreach(array, function(value){ + if (fn(value)) { + count ++; + } + }); + return count; + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.orderBy + * @function + * + * @description + * Orders `array` by the `expression` predicate. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @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`: JavaScript's Array#sort comparator function + * - `string`: 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 (e.g. +name or -name). + * - `Array`: array of function or string predicates, such that a first predicate in the array + * is used for sorting, but when the items are equivalent next predicate is used. + * + * @param {boolean=} reverse Reverse the order the array. + * @returns {Array} Sorted copy of the source array. + * + * @example +
+ +
Sorting predicate = {{predicate}}
+
+ + + + + + + + + + + +
Name + (^)Phone + (^)Age + (^)
{{friend.name}}{{friend.phone}}{{friend.age}}
+ + @scenario + it('should be reverse ordered by aged', function() { + expect(binding('predicate')).toBe('Sorting predicate = -age'); + expect(repeater('.doc-example table', 'friend in friends').column('friend.age')). + toEqual(['35', '29', '21', '19', '10']); + expect(repeater('.doc-example table', '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 a:contains("Name")').click(); + expect(repeater('.doc-example table', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); + expect(repeater('.doc-example table', 'friend in friends').column('friend.age')). + toEqual(['35', '10', '29', '19', '21']); + + element('.doc-example a:contains("Phone")+a:contains("^")').click(); + expect(repeater('.doc-example table', 'friend in friends').column('friend.phone')). + toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); + expect(repeater('.doc-example table', 'friend in friends').column('friend.name')). + toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); + }); + */ + //TODO: WTH is descend param for and how/when it should be used, how is it affected by +/- in + // predicate? the code below is impossible to read and specs are not very good. + 'orderBy':function(array, expression, descend) { + expression = isArray(expression) ? expression: [expression]; + expression = map(expression, function($){ + var descending = false, get = $ || identity; + if (isString($)) { + if (($.charAt(0) == '+' || $.charAt(0) == '-')) { + descending = $.charAt(0) == '-'; + $ = $.substring(1); + } + get = expressionCompile($).fnSelf; + } + return reverse(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(reverse(comparator, descend)); + + function comparator(o1, o2){ + for ( var i = 0; i < expression.length; i++) { + var comp = expression[i](o1, o2); + if (comp !== 0) return comp; + } + return 0; + } + function reverse(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; + } + } + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.limitTo + * @function + * + * @description + * Creates a new array containing only the first, or last `limit` number of elements of the + * source `array`. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array Source array to be limited. + * @param {string|Number} limit The length of the returned array. If the number is positive, the + * first `limit` items from the source array will be copied, if the number is negative, the + * last `limit` items will be copied. + * @returns {Array} New array of length `limit`. + * + */ + limitTo: function(array, limit) { + limit = parseInt(limit, 10); + var out = [], + i, n; + + if (limit > 0) { + i = 0; + n = limit; + } else { + i = array.length + limit; + n = array.length; + } + + for (; i
+ {{amount | currency}} + * + * @scenario + it('should init with 1234.56', function(){ + expect(binding('amount | currency')).toBe('$1,234.56'); + }); + it('should update', function(){ + input('amount').enter('-1234'); + expect(binding('amount | currency')).toBe('$-1,234.00'); + expect(element('.doc-example-live .ng-binding').attr('className')). + toMatch(/ng-format-negative/); + }); + */ +angularFilter.currency = function(amount){ + this.$element.toggleClass('ng-format-negative', amount < 0); + return '$' + angularFilter['number'].apply(this, [amount, 2]); +}; + +/** + * @workInProgress + * @ngdoc filter + * @name angular.filter.number + * @function + * + * @description + * Formats a number as text. + * + * If the input is not a number 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}} + + * @scenario + it('should format numbers', function(){ + expect(binding('val | number')).toBe('1,234.57'); + 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.33'); + expect(binding('val | number:0')).toBe('3,374'); + expect(binding('-val | number:4')).toBe('-3,374.3330'); + }); + */ +angularFilter.number = function(number, fractionSize){ + if (isNaN(number) || !isFinite(number)) { + return ''; + } + fractionSize = typeof fractionSize == $undefined ? 2 : fractionSize; + var isNegative = number < 0; + number = Math.abs(number); + var pow = Math.pow(10, fractionSize); + var text = "" + Math.round(number * pow); + var whole = text.substring(0, text.length - fractionSize); + whole = whole || '0'; + var frc = text.substring(text.length - fractionSize); + text = isNegative ? '-' : ''; + for (var i = 0; i < whole.length; i++) { + if ((whole.length - i)%3 === 0 && i !== 0) { + text += ','; + } + text += whole.charAt(i); + } + if (fractionSize > 0) { + for (var j = frc.length; j < fractionSize; j++) { + frc += '0'; + } + text += '.' + frc.substring(0, fractionSize); + } + return text; +}; + + +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); + }; +} + + +var DATE_FORMATS = { + yyyy: dateGetter('FullYear', 4), + yy: dateGetter('FullYear', 2, 0, 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), + a: function(date){return date.getHours() < 12 ? 'am' : 'pm';}, + Z: function(date){ + var offset = date.getTimezoneOffset(); + return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2); + } +}; + + +var DATE_FORMATS_SPLIT = /([^yMdHhmsaZ]*)(y+|M+|d+|H+|h+|m+|s+|a|Z)(.*)/; +var NUMBER_STRING = /^\d+$/; + + +/** + * @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. 2010 + * * `'yy'`: 2 digit representation of year, padded (00-99) + * * `'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) + * * `'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) + * + * @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. 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:'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'}}
+ * + * @scenario + it('should format date', function(){ + 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) { + if (isString(date)) { + if (NUMBER_STRING.test(date)) { + date = parseInt(date, 10); + } else { + date = angularString.toDate(date); + } + } + + if (isNumber(date)) { + date = new Date(date); + } + + if (!isDate(date)) { + return date; + } + + var text = date.toLocaleDateString(), fn; + if (format && isString(format)) { + text = ''; + var parts = []; + while(format) { + parts = concat(parts, DATE_FORMATS_SPLIT.exec(format), 1); + format = parts.pop(); + } + foreach(parts, function(value){ + fn = DATE_FORMATS[value]; + text += fn ? fn(date) : value; + }); + } + 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: + +
{{ obj | json }}
+ * + * @scenario + it('should jsonify filtered objects', function() { + expect(binding('obj | json')).toBe('{\n "a":1,\n "b":[]}'); + }); + + it('should update', function() { + input('objTxt').enter('[1, 2, 3]'); + expect(binding('obj | json')).toBe('[1,2,3]'); + }); + * + */ +angularFilter.json = function(object) { + this.$element.addClass("ng-monospace"); + return toJson(object, true); +}; + + +/** + * @workInProgress + * @ngdoc filter + * @name angular.filter.lowercase + * @function + * + * @see angular.lowercase + */ +angularFilter.lowercase = lowercase; + + +/** + * @workInProgress + * @ngdoc filter + * @name angular.filter.uppercase + * @function + * + * @see angular.uppercase + */ +angularFilter.uppercase = uppercase; + + +/** + * @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>
+ * + * @scenario + 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'); + }); + */ +angularFilter.html = function(html, option){ + return new HTML(html, option); +}; + + +/** + * @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 + * plane 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>
+ + @scenario + 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.'); + }); + */ +//TODO: externalize all regexps +angularFilter.linky = function(text){ + if (!text) return text; + var URL = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/; + var match; + var raw = text; + var html = []; + var writer = htmlSanitizeWriter(html); + var url; + var i; + while (match=raw.match(URL)) { + // 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:/, '')); + writer.end('a'); + raw = raw.substring(i + match[0].length); + } + writer.chars(raw); + return new HTML(html.join('')); +}; +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 + * + * @description + * Formats the user input as JSON text. + * + * @returns {string} A JSON string representation of the model. + * + * @example + *
+ * + *
data={{data}}
+ *
+ * + * @scenario + * 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, fromJson); + +/** + * @workInProgress + * @ngdoc formatter + * @name angular.formatter.boolean + * + * @description + * Use boolean formatter if you wish to store the data as boolean. + * + * @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`. + * + * @example + * Enter truthy text: + * + * + *
value={{value}}
+ * + * @scenario + * 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 + * + * @description + * Use number formatter if you wish to convert the user entered string to a number. + * + * @returns {number} Number from the parsed string. + * + * @example + * Enter valid number: + * + *
value={{value}}
+ * + * @scenario + * 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 + * + * @description + * Use list formatter if you wish to convert the user entered string to an array. + * + * @returns {Array} Array parsed from the entered string. + * + * @example + * Enter a list of items: + * + * + *
value={{value}}
+ * + * @scenario + * 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 :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 + * + * @description + * Use trim formatter if you wish to trim extra spaces in user text. + * + * @returns {String} Trim excess leading and trailing space. + * + * @example + * Enter text with leading/trailing spaces: + * + * + *
value={{value|json}}
+ * + * @scenario + * it('should format trim', function(){ + * expect(binding('value')).toEqual('value="book"'); + * this.addFutureAction('change to XYZ', function($window, $document, done){ + * $document.elements('.doc-example :input:last').val(' text ').trigger('change'); + * done(); + * }); + * expect(binding('value')).toEqual('value="text"'); + * }); + */ +angularFormatter.trim = formatter( + function(obj) { return obj ? trim("" + obj) : ""; } +); +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 {regexp} expression regular expression. + * @css ng-validation-error + * + * @example + * + * Enter valid SSN: + * + * + * @scenario + * it('should invalidate non ssn', function(){ + * var textBox = element('.doc-example :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:
+ * + * @scenario + * it('should invalidate number', function(){ + * var n1 = element('.doc-example :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 :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 :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):
+ * + * @scenario + * it('should invalidate integer', function(){ + * var n1 = element('.doc-example :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 :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 :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: + * + * + * @scenario + * it('should invalidate date', function(){ + * var n1 = element('.doc-example :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: + * + * + * @scenario + * it('should invalidate email', function(){ + * var n1 = element('.doc-example :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 + * @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: + * + * + * @scenario + * it('should invalidate phone', function(){ + * var n1 = element('.doc-example :input'); + * expect(n1.attr('className')).not().toMatch(/ng-validation-error/); + * input('text').enter('+12345678'); + * expect(n1.attr('className')).toMatch(/ng-validation-error/); + * }); + * + */ + '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 internationaly."; + }, + + /** + * @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 phone number: + * + * + * @scenario + * it('should invalidate url', function(){ + * var n1 = element('.doc-example :input'); + * expect(n1.attr('className')).not().toMatch(/ng-validation-error/); + * input('text').enter('abc://server/path'); + * expect(n1.attr('className')).toMatch(/ng-validation-error/); + * }); + * + */ + '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."; + }, + + /** + * @workInProgress + * @ngdoc validator + * @name angular.validator.json + * @description + * Use json validator if you wish to restrict the user input to a valid JSON. + * + * @param {string} value value to validate + * @css ng-validation-error + * + * @example + * + * + * @scenario + * it('should invalidate json', function(){ + * var n1 = element('.doc-example :input'); + * expect(n1.attr('className')).not().toMatch(/ng-validation-error/); + * input('json').enter('{name}'); + * expect(n1.attr('className')).toMatch(/ng-validation-error/); + * }); + * + */ + 'json': function(value) { + try { + fromJson(value); + return _null; + } catch (e) { + return e.toString(); + } + }, + + /** + * @workInProgress + * @ngdoc validator + * @name angular.validator.asynchronous + * @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: + * + * + * @scenario + * it('should change color in delayed way', function(){ + * var textBox = element('.doc-example :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/); + * + * pause(.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' + * } + * + */ + '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:{}}); + } + + cache.current = input; + + var inputState = cache.inputs[input]; + if (!inputState) { + cache.inputs[input] = inputState = { inFlight: true }; + scope.$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'); + scope.$invalidWidgets.markValid(element); + } + element.data($$validate)(); + scope.$root.$eval(); + }); + } else if (inputState.inFlight) { + // request in flight, mark widget invalid, but don't show it to user + scope.$invalidWidgets.markInvalid(scope.$element); + } else { + (updateFn||noop)(inputState.response); + } + return inputState.error; + } + +}); +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, + HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}, + EAGER = 'eager', + EAGER_PUBLISHED = EAGER + '-published'; + +function angularServiceInject(name, fn, inject, eager) { + angularService(name, fn, {$inject:inject, $creation:eager}); +} + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$window + * + * @description + * Is 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 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), [], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$document + * @requires $window + * + * @description + * Reference to the browser window.document, but wrapped into angular.element(). + */ +angularServiceInject("$document", function(window){ + return jqLite(window.document); +}, ['$window'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$location + * @requires $browser + * + * @property {string} href + * @property {string} protocol + * @property {string} host + * @property {number} port + * @property {string} path + * @property {Object.} search + * @property {string} hash + * @property {string} hashPath + * @property {Object.} hashSearch + * + * @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 to url. + * Notice that using browser's forward/back buttons changes the $location. + * + * @example + clear hash | + test hash
+ +
$location = {{$location}}
+ */ +angularServiceInject("$location", function(browser) { + var scope = this, + location = {toString:toString, update:update, updateHash: updateHash}, + lastBrowserUrl = browser.getUrl(), + lastLocationHref, + lastLocationHash; + + browser.addPollFn(function() { + if (lastBrowserUrl != browser.getUrl()) { + update(lastBrowserUrl = browser.getUrl()); + updateLastLocation(); + scope.$eval(); + } + }); + + this.$onEval(PRIORITY_FIRST, updateBrowser); + this.$onEval(PRIORITY_LAST, updateBrowser); + + update(lastBrowserUrl); + updateLastLocation(); + + return location; + + // PUBLIC METHODS + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$location#update + * @methodOf angular.service.$location + * + * @description + * Update location object + * Does not immediately update the browser + * Browser is updated at the end of $eval() + * + * @example + * scope.$location.update('http://www.angularjs.org/path#hash?search=x'); + * scope.$location.update({host: 'www.google.com', protocol: 'https'}); + * scope.$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}}); + * + * @param {(string|Object)} href Full href as a string or hash object with properties + */ + function update(href) { + if (isString(href)) { + extend(location, parseHref(href)); + } else { + if (isDefined(href.hash)) { + extend(href, parseHash(href.hash)); + } + + extend(location, href); + + if (isDefined(href.hashPath || href.hashSearch)) { + location.hash = composeHash(location); + } + + location.href = composeHref(location); + } + } + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$location#updateHash + * @methodOf angular.service.$location + * + * @description + * Update location hash part + * @see update() + * + * @example + * 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 = {}; + + if (isString(path)) { + hash.hashPath = path; + if (isDefined(search)) + hash.hashSearch = search; + } else + hash.hashSearch = path; + + update(hash); + } + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$location#toString + * @methodOf angular.service.$location + * + * @description + * Returns string representation - href + */ + function toString() { + updateLocation(); + return location.href; + } + + // INNER METHODS + + /** + * Update location object + * + * User is allowed to change properties, so after property change, + * location object is not in consistent state. + * + * @example + * scope.$location.href = 'http://www.angularjs.org/path#a/b' + * immediately after this call, other properties are still the old ones... + * + * This method checks the changes and update location to the consistent state + */ + function updateLocation() { + if (location.href == lastLocationHref) { + if (location.hash == lastLocationHash) { + location.hash = composeHash(location); + } + location.href = composeHref(location); + } + update(location.href); + } + + /** + * Update information about last location + */ + function updateLastLocation() { + lastLocationHref = location.href; + lastLocationHash = location.hash; + } + + /** + * If location has changed, update the browser + * This method is called at the end of $eval() phase + */ + function updateBrowser() { + updateLocation(); + + if (location.href != lastLocationHref) { + browser.setUrl(lastBrowserUrl = location.href); + updateLastLocation(); + } + } + + /** + * Compose href string from a location object + * + * @param {Object} loc The location object with all properties + * @return {string} Composed href + */ + function composeHref(loc) { + var url = toKeyValue(loc.search); + var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? _null : loc.port); + + return loc.protocol + '://' + loc.host + + (port ? ':' + port : '') + loc.path + + (url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : ''); + } + + /** + * Compose hash string from location object + * + * @param {Object} loc Object with hashPath and hashSearch properties + * @return {string} Hash string + */ + function composeHash(loc) { + var hashSearch = toKeyValue(loc.hashSearch); + //TODO: temporary fix for issue #158 + return escape(loc.hashPath).replace(/%21/gi, '!').replace(/%3A/gi, ':').replace(/%24/gi, '$') + + (hashSearch ? '?' + hashSearch : ''); + } + + /** + * Parse href string into location object + * + * @param {string} href + * @return {Object} The location object + */ + function parseHref(href) { + var loc = {}; + var match = URL_MATCH.exec(href); + + if (match) { + loc.href = href.replace(/#$/, ''); + loc.protocol = match[1]; + loc.host = match[3] || ''; + loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || _null; + loc.path = match[6] || ''; + loc.search = parseKeyValue(match[8]); + loc.hash = match[10] || ''; + + extend(loc, parseHash(loc.hash)); + } + + return loc; + } + + /** + * Parse hash string into object + * + * @param {string} hash + */ + function parseHash(hash) { + var h = {}; + var match = HASH_MATCH.exec(hash); + + if (match) { + h.hash = hash; + h.hashPath = unescape(match[1] || ''); + h.hashSearch = parseKeyValue(match[3]); + } + + return h; + } +}, ['$browser'], EAGER_PUBLISHED); + + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$log + * @requires $window + * + * @description + * Is simple service for logging. Default implementation writes the message + * into the browser's console (if present). + * + * This is useful for debugging. + * + * @example +

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

+ Message: + + + + + + */ +angularServiceInject("$log", 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 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'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$exceptionHandler + * @requires $log + * + * @description + * Any uncaught exception in is delegated to this service. + * The default implementation simply delegates to $log.error which logs it into + * the browser console. + * + * When unit testing it is useful to have uncaught exceptions propagate + * to the test so the test will fail rather than silently log the exception + * to the browser console. For this purpose you can override this service with + * a simple rethrow. + * + * @example + */ +angularServiceInject('$exceptionHandler', function($log){ + return function(e) { + $log.error(e); + }; +}, ['$log'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$hover + * @requires $browser + * @requires $document + * + * @description + * + * @example + */ +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'], EAGER); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$invalidWidgets + * + * @description + * Keeps references to all invalid widgets found during validation. + * Can be queried to find whether there are any invalid widgets currently displayed. + * + * @example + */ +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); + }; + + + /** 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.$onEval(PRIORITY_LAST, 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++; + } + } + }); + + + /** + * Traverses DOM element's (widget's) parents and considers the element to be an orphant 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; +}, [], EAGER_PUBLISHED); + + + +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); + } + } + }); + var match = on.match(new RegExp(regex)); + if (match) { + foreach(params, function(name, index){ + dst[name] = match[index + 1]; + }); + if (dstName) this.$set(dstName, dst); + } + return match ? dst : _null; +} + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$route + * @requires $location + * + * @property {Object} current Name of the current route + * @property {Array.} routes List of configured routes + * + * @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). + * + * $route is typically used in conjunction with ng:include widget. + * + * @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. +

+ + + +Chose: +Moby | +Moby: Ch1 | +Gatsby | +Gatsby: Ch4
+ +
$location={{$location}}
+
$route.current.template={{$route.current.template}}
+
$route.current.params={{$route.current.params}}
+
$route.current.scope.name={{$route.current.scope.name}}
+
+ + */ +angularServiceInject('$route', function(location) { + var routes = {}, + onChange = [], + matcher = switchRouteMatcher, + parentScope = this, + dirty = 0, + $route = { + routes: routes, + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#onChange + * @methodOf angular.service.$route + * + * @param {function()} fn Function that will be called on route change + * + * @description + * Register a handler function that will be called when route changes + */ + onChange: bind(onChange, onChange.push), + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#when + * @methodOf angular.service.$route + * + * @param {string} path Route path (matched against $location.hash) + * @param {Object} params Mapping information to be assigned to `$route.current` on route + * match. + * @returns {Object} route object + * + * @description + * Add new route + */ + when:function (path, params) { + if (angular.isUndefined(path)) return routes; + var route = routes[path]; + if (!route) route = routes[path] = {}; + if (params) angular.extend(route, params); + dirty++; + return route; + } + }; + function updateRoute(){ + var childScope; + $route.current = _null; + angular.foreach(routes, function(routeParams, route) { + if (!childScope) { + var pathParams = matcher(location.hashPath, route); + if (pathParams) { + childScope = angular.scope(parentScope); + $route.current = angular.extend({}, routeParams, { + scope: childScope, + params: angular.extend({}, location.hashSearch, pathParams) + }); + } + } + }); + angular.foreach(onChange, parentScope.$tryEval); + if (childScope) { + childScope.$become($route.current.controller); + } + } + this.$watch(function(){return dirty + location.hash;}, updateRoute); + return $route; +}, ['$location'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr + * @requires $browser + * @requires $xhr.error + * @requires $log + * + * @description + * + * @example + */ +angularServiceInject('$xhr', function($browser, $error, $log){ + var self = this; + return function(method, url, post, callback){ + if (isFunction(post)) { + callback = post; + post = _null; + } + if (post && isObject(post)) { + post = toJson(post); + } + $browser.xhr(method, url, post, function(code, response){ + try { + if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { + response = fromJson(response, true); + } + if (code == 200) { + callback(code, response); + } else { + $error( + {method: method, url:url, data:post, callback:callback}, + {status: code, body:response}); + } + } catch (e) { + $log.error(e); + } finally { + self.$eval(); + } + }); + }; +}, ['$browser', '$xhr.error', '$log']); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr.error + * @requires $log + * + * @description + * + * @example + */ +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.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, callback) { + if (isFunction(post)) { + callback = post; + post = _null; + } + var currentQueue; + foreach(bulkXHR.urls, function(queue){ + if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) { + currentQueue = queue; + } + }); + if (currentQueue) { + if (!currentQueue.requests) currentQueue.requests = []; + currentQueue.requests.push({method: method, url: url, data:post, callback:callback}); + } else { + $xhr(method, url, post, callback); + } + } + bulkXHR.urls = {}; + bulkXHR.flush = function(callback){ + 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].callback || noop)(response.status, response.response); + } else { + $error(currentRequests[i], response); + } + } catch(e) { + $log.error(e); + } + }); + (callback || noop)(); + }); + scope.$eval(); + } + }); + }; + this.$onEval(PRIORITY_LAST, bulkXHR.flush); + return bulkXHR; +}, ['$xhr', '$xhr.error', '$log']); + + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$defer + * @requires $browser + * @requires $log + * + * @description + * Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function + * into a try/catch block and delegates any exceptions to + * {@link angular.services.$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. + */ +angularServiceInject('$defer', function($browser, $exceptionHandler) { + var scope = this; + + return function(fn) { + $browser.defer(function() { + try { + fn(); + } catch(e) { + $exceptionHandler(e); + } finally { + scope.$eval(); + } + }); + }; +}, ['$browser', '$exceptionHandler']); + + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr.cache + * @requires $xhr + * + * @description + * + * @example + */ +angularServiceInject('$xhr.cache', function($xhr, $defer){ + var inflight = {}, self = this; + function cache(method, url, post, callback, verifyCache){ + if (isFunction(post)) { + callback = post; + post = _null; + } + if (method == 'GET') { + var data, dataCached; + if (dataCached = cache.data[url]) { + $defer(function() { callback(200, copy(dataCached.value)); }); + if (!verifyCache) + return; + } + + if (data = inflight[url]) { + data.callbacks.push(callback); + } else { + inflight[url] = {callbacks: [callback]}; + cache.delegate(method, url, post, function(status, response){ + if (status == 200) + cache.data[url] = { value: response }; + var callbacks = inflight[url].callbacks; + delete inflight[url]; + foreach(callbacks, function(callback){ + try { + (callback||noop)(status, copy(response)); + } catch(e) { + self.$log.error(e); + } + }); + }); + } + + } else { + cache.data = {}; + cache.delegate(method, url, post, callback); + } + } + cache.data = {}; + cache.delegate = $xhr; + return cache; +}, ['$xhr.bulk', '$defer']); + + +/** + * @workInProgress + * @ngdoc function + * @name angular.service.$resource + * @requires $xhr + * + * @description + * Is a factory which creates a resource object which lets you interact with + * RESTful + * server-side data sources. + * Resource object has action methods which provide high-level behaviors without + * the need to interact with the low level $xhr or XMLHttpRequest(). + * + *
+     // 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 `$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. + +
+     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 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();
+     });
+   
+ * + * + * @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. + * @param {Object.=} actions Map of actions available for the resource. + * + * Each resource comes preconfigured with `get`, `save`, `query`, `remove`, and `delete` to + * mimic the RESTful philosophy. + * + * To create your own actions, pass in a map keyed on action names (e.g. `'charge'`) with + * elements consisting of these properties: + * + * - `{string} method`: Request method type. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, + * and [`JSON`](http://en.wikipedia.org/wiki/JSON#JSONP) (also known as JSONP). + * - `{Object=} params`: Set of pre-bound parameters for the action. + * - `{boolean=} isArray`: If true then the returned object for this action is an array, see the + * pre-binding section. + * - `{boolean=} verifyCache`: If true then items returned from cache, are double checked by + * running the query again and updating the resource asynchroniously. + * + * Each service comes preconfigured with the following overridable actions: + *
+ *       { 'get':    {method:'GET'},
+           'save':   {method:'POST'},
+           'query':  {method:'GET', isArray:true},
+           'remove': {method:'DELETE'},
+           'delete': {method:'DELETE'} };
+ *     
+ * + * @returns {Object} A resource "class". + * + * @example + + +
+ + +
+
+

+ + {{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.$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; + + //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); + rootScope.$eval(); + } + })(); + + //at the end of each eval, push cookies + this.$onEval(PRIORITY_LAST, push); + + return cookies; + + + /** + * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. + */ + function push(){ + var name, + browserCookies, + updated; + + //delete any cookies deleted in $cookies + for (name in lastCookies) { + if (isUndefined(cookies[name])) { + $browser.cookies(name, _undefined); + } + } + + //update all cookies updated in $cookies + for(name in cookies) { + if (cookies[name] !== lastCookies[name]) { + $browser.cookies(name, cookies[name]); + updated = true; + } + } + + //verify what was actually stored + if (updated){ + updated = !updated; + 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; + } + + } + + if (updated) { + rootScope.$eval(); + } + } + } +}, ['$browser'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$cookieStore + * @requires $cookies + * + * @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. + * @example + */ +angularServiceInject('$cookieStore', function($store) { + + return { + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$cookieStore#get + * @methodOf angular.service.$cookieStore + * + * @description + * Returns the value of given cookie key + * + * @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 + * + * @description + * Sets a value for given cookie key + * + * @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 + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + */ + remove: function(key) { + delete $store[key]; + } + }; + +}, ['$cookies']); +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:init + * + * @description + * `ng:init` attribute allows the for initialization tasks to be executed + * before the template enters execution mode during bootstrap. + * + * @element ANY + * @param {expression} expression to eval. + * + * @example +
+ {{greeting}} {{person}}! +
+ * + * @scenario + it('should check greeting', function(){ + expect(binding('greeting')).toBe('Hello'); + expect(binding('person')).toBe('World'); + }); + */ +angularDirective("ng:init", function(expression){ + return function(element){ + this.$tryEval(expression, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:controller + * + * @description + * To support the Model-View-Controller design pattern, it is possible + * to assign behavior to a scope through `ng:controller`. The scope is + * the MVC model. The HTML (with data bindings) is the MVC view. + * The `ng:controller` directive specifies the MVC controller class + * + * @element ANY + * @param {expression} expression to eval. + * + * @example + +
+ Name: + [ greet ]
+ Contact: +
    +
  • + + + [ clear + | X ] +
  • +
  • [ add ]
  • +
+
+ * + * @scenario + 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()).toBe('408 555 1212'); + expect(element('.doc-example-live li[ng\\:repeat-index="1"] 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()).toBe('yourname@example.org'); + }); + */ +angularDirective("ng:controller", function(expression){ + this.scope(true); + return function(element){ + var controller = getter(window, expression, true) || getter(this, expression, true); + if (!controller) + throw "Can not find '"+expression+"' controller."; + if (!isFunction(controller)) + throw "Reference '"+expression+"' is not a class."; + this.$become(controller); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:eval + * + * @description + * The `ng:eval` allows you to execute a binding which has side effects + * without displaying the result to the user. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * Notice that `{{` `obj.multiplied = obj.a * obj.b` `}}` has a side effect of assigning + * a value to `obj.multiplied` and displaying the result to the user. Sometimes, + * however, it is desirable to execute a side effect without showing the value to + * the user. In such a case `ng:eval` allows you to execute code without updating + * the display. + * + * @example + * + * * + * = {{obj.multiplied = obj.a * obj.b}}
+ * + * + * obj.divide = {{obj.divide}}
+ * obj.updateCount = {{obj.updateCount}} + * + * @scenario + it('should check eval', function(){ + expect(binding('obj.divide')).toBe('3'); + expect(binding('obj.updateCount')).toBe('2'); + input('obj.a').enter('12'); + expect(binding('obj.divide')).toBe('6'); + expect(binding('obj.updateCount')).toBe('3'); + }); + */ +angularDirective("ng:eval", function(expression){ + return function(element){ + this.$onEval(expression, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:bind + * + * @description + * The `ng:bind` attribute asks to replace the text content of this + * HTML element with the value of the given expression and kept it up to + * date when the expression's value changes. Usually you just write + * {{expression}} and let compile it into + * `` at bootstrap time. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * Try it here: enter text in text box and watch the greeting change. + * @example + * Enter name: .
+ * Hello ! + * + * @scenario + 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'); + }); + */ +angularDirective("ng:bind", function(expression, element){ + element.addClass('ng-binding'); + return function(element) { + var lastValue = noop, lastError = noop; + this.$onEval(function() { + var error, value, html, isHtml, isDomElement, + oldElement = this.hasOwnProperty($$element) ? this.$element : _undefined; + this.$element = element; + value = this.$tryEval(expression, function(e){ + error = formatError(e); + }); + this.$element = oldElement; + // If we are HTML than save the raw HTML data so that we don't + // recompute sanitization since it 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); + } + 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); + } + } + }, element); + }; +}); + +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(element){ + var error, value = this.$tryEval(exp, function(e){ + error = toJson(e); + }); + elementError(element, NG_EXCEPTION, error); + return error ? error : value; + } : function() { + return text; + }); + }); + bindTemplateCache[template] = fn = function(element){ + var parts = [], self = this, + oldElement = this.hasOwnProperty($$element) ? self.$element : _undefined; + self.$element = element; + for ( var i = 0; i < bindings.length; i++) { + var value = bindings[i].call(self, element); + if (isElement(value)) + value = ''; + else if (isObject(value)) + value = toJson(value, true); + parts.push(value); + } + self.$element = oldElement; + return parts.join(''); + }; + } + return fn; +} + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:bind-template + * + * @description + * The `ng:bind-template` attribute specifies that the element + * text should be replaced with the template in ng:bind-template. + * Unlike ng:bind the ng:bind-template can contain multiple `{{` `}}` + * expressions. (This is required since some HTML elements + * can not have SPAN elements such as TITLE, or OPTION to name a few. + * + * @element ANY + * @param {string} template of form + * {{ expression }} to eval. + * + * @exampleDescription + * Try it here: enter text in text box and watch the greeting change. + * @example + Salutation:
+ Name:
+

+ * 
+ * @scenario
+   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');
+     using('.doc-example-live').input('name').enter('user');
+     expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
+       toBe('Greetings user!');
+   });
+ */
+angularDirective("ng:bind-template", function(expression, element){
+  element.addClass('ng-binding');
+  var templateFn = compileBindTemplate(expression);
+  return function(element) {
+    var lastValue;
+    this.$onEval(function() {
+      var value = templateFn.call(this, element);
+      if (value != lastValue) {
+        element.text(value);
+        lastValue = value;
+      }
+    }, element);
+  };
+});
+
+var REMOVE_ATTRIBUTES = {
+  'disabled':'disabled',
+  'readonly':'readOnly',
+  'checked':'checked',
+  'selected':'selected'
+};
+/**
+ * @workInProgress
+ * @ngdoc directive
+ * @name angular.directive.ng:bind-attr
+ *
+ * @description
+ * The `ng:bind-attr` attribute specifies that the element attributes 
+ * which should be replaced by the expression in it. Unlike `ng:bind` 
+ * the `ng:bind-attr` contains a JSON key value pairs representing 
+ * which attributes need to be changed. You don’t usually write the 
+ * `ng:bind-attr` in the HTML since embedding 
+ * {{expression}} into the 
+ * attribute directly is the preferred way. The attributes get
+ * translated into `` at
+ * bootstrap time.
+ * 
+ * This HTML snippet is preferred way of working with `ng:bind-attr`
+ * 
+ *   Google
+ * 
+ * + * The above gets translated to bellow during bootstrap time. + *
+ *   Google
+ * 
+ * + * @element ANY + * @param {string} attribute_json a JSON key-value pairs representing + * the attributes to replace. Each key matches the attribute + * which needs to be replaced. Each value is a text template of + * the attribute with embedded + * {{expression}}s. Any number of + * key-value pairs can be specified. + * + * @exampleDescription + * Try it here: enter text in text box and click Google. + * @example + Google for: + + Google + * + * @scenario + 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'); + expect(using('.doc-example-live').element('a').attr('href')). + toBe('http://www.google.com/search?q=google'); + }); + */ +angularDirective("ng:bind-attr", function(expression){ + return function(element){ + var lastValue = {}; + var updateFn = element.data($$update) || noop; + this.$onEval(function(){ + var values = this.$eval(expression), + dirty = noop; + for(var key in values) { + var value = compileBindTemplate(values[key]).call(this, element), + specialName = REMOVE_ATTRIBUTES[lowercase(key)]; + 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); + } + dirty = updateFn; + } + } + dirty(); + }, element); + }; +}); + + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:click + * + * @description + * The ng:click allows you to specify custom behavior when + * element is clicked. + * + * @element ANY + * @param {expression} expression to eval upon click. + * + * @example + + count: {{count}} + * @scenario + it('should check ng:click', function(){ + expect(binding('count')).toBe('0'); + element('.doc-example-live :button').click(); + expect(binding('count')).toBe('1'); + }); + */ +/* + * A directive that allows creation of custom onclick handlers that are defined as angular + * expressions and are compiled and executed within the current scope. + * + * Events that are handled via these handler are always configured not to propagate further. + * + * TODO: maybe we should consider allowing users to control event propagation in the future. + */ +angularDirective("ng:click", function(expression, element){ + return function(element){ + var self = this; + element.bind('click', function(event){ + self.$tryEval(expression, element); + self.$root.$eval(); + event.stopPropagation(); + }); + }; +}); + + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:submit + * + * @description + * + * @element form + * @param {expression} expression to eval. + * + * @exampleDescription + * @example + *
+ * Enter text and hit enter: + * + *
+ *
list={{list}}
+ * @scenario + it('should check ng:submit', function(){ + expect(binding('list')).toBe('list=[]'); + element('.doc-example-live form input').click(); + this.addFutureAction('submit from', function($window, $document, done) { + $window.angular.element( + $document.elements('.doc-example-live form')). + trigger('submit'); + done(); + }); + expect(binding('list')).toBe('list=["hello"]'); + }); + */ +/** + * Enables binding angular expressions to onsubmit events. + * + * Additionally it prevents the default action (which for form means sending the request to the + * server and reloading the current page). + */ +angularDirective("ng:submit", function(expression, element) { + return function(element) { + var self = this; + element.bind('submit', function(event) { + self.$tryEval(expression, element); + self.$root.$eval(); + event.preventDefault(); + }); + }; +}); + + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:watch + * + * @description + * The `ng:watch` allows you watch a variable and then execute + * an evaluation on variable change. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * Notice that the counter is incremented + * every time you change the text. + * @example +
+
+ Change counter: {{counter}} Name: {{name}} +
+ * @scenario + it('should check ng:watch', function(){ + expect(using('.doc-example-live').binding('counter')).toBe('2'); + using('.doc-example-live').input('name').enter('abc'); + expect(using('.doc-example-live').binding('counter')).toBe('3'); + }); + */ +//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular) +angularDirective("ng:watch", function(expression, element){ + return function(element){ + var self = this; + parser(expression).watch()({ + addListener:function(watch, exp){ + self.$watch(watch, function(){ + return exp(self); + }, element); + } + }); + }; +}); + +function ngClass(selector) { + return function(expression, element){ + var existing = element[0].className + ' '; + return function(element){ + this.$onEval(function(){ + if (selector(this.$index)) { + var value = this.$eval(expression); + if (isArray(value)) value = value.join(' '); + element[0].className = trim(existing + value); + } + }, element); + }; + }; +} + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:class + * + * @description + * The `ng:class` allows you to set CSS class on HTML element + * conditionally. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * @example + + +
+ Sample Text      + * + * @scenario + it('should check ng:class', function(){ + expect(element('.doc-example-live span').attr('className')).not(). + toMatch(/ng-input-indicator-wait/); + + using('.doc-example-live').element(':button:first').click(); + + expect(element('.doc-example-live span').attr('className')). + toMatch(/ng-input-indicator-wait/); + + using('.doc-example-live').element(':button:last').click(); + + expect(element('.doc-example-live span').attr('className')).not(). + toMatch(/ng-input-indicator-wait/); + }); + */ +angularDirective("ng:class", ngClass(function(){return true;})); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:class-odd + * + * @description + * The `ng:class-odd` and `ng:class-even` works exactly as + * `ng:class`, except it works in conjunction with `ng:repeat` + * and takes affect only on odd (even) rows. + * + * @element ANY + * @param {expression} expression to eval. Must be inside + * `ng:repeat`. + + * + * @exampleDescription + * @example +
    +
  1. + + {{name}}       + +
  2. +
+ * + * @scenario + 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/); + }); + */ +angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;})); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:class-even + * + * @description + * The `ng:class-odd` and `ng:class-even` works exactly as + * `ng:class`, except it works in conjunction with `ng:repeat` + * and takes affect only on odd (even) rows. + * + * @element ANY + * @param {expression} expression to eval. Must be inside + * `ng:repeat`. + + * + * @exampleDescription + * @example +
    +
  1. + + {{name}}       + +
  2. +
+ * + * @scenario + 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/); + }); + */ +angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;})); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:show + * + * @description + * The `ng:show` and `ng:hide` allows you to show or hide a portion + * of the HTML conditionally. + * + * @element ANY + * @param {expression} expression if truthy then the element is + * shown or hidden respectively. + * + * @exampleDescription + * @example + Click me:
+ Show: I show up when you checkbox is checked?
+ Hide: I hide when you checkbox is checked? + * + * @scenario + 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); + + input('checked').check(); + + expect(element('.doc-example-live span:first:visible').count()).toEqual(1); + expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); + }); + */ +angularDirective("ng:show", function(expression, element){ + return function(element){ + this.$onEval(function(){ + element.css($display, toBoolean(this.$eval(expression)) ? '' : $none); + }, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:hide + * + * @description + * The `ng:show` and `ng:hide` allows you to show or hide a portion + * of the HTML conditionally. + * + * @element ANY + * @param {expression} expression if truthy then the element is + * shown or hidden respectively. + * + * @exampleDescription + * @example + Click me:
+ Show: I show up when you checkbox is checked?
+ Hide: I hide when you checkbox is checked? + * + * @scenario + 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); + + input('checked').check(); + + expect(element('.doc-example-live span:first:visible').count()).toEqual(1); + expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); + }); + */ +angularDirective("ng:hide", function(expression, element){ + return function(element){ + this.$onEval(function(){ + element.css($display, toBoolean(this.$eval(expression)) ? $none : ''); + }, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:style + * + * @description + * The ng:style allows you to set CSS style on an HTML element conditionally. + * + * @element ANY + * @param {expression} expression which evals to an object whes key's are + * CSS style names and values are coresponding values for those + * CSS keys. + * + * @exampleDescription + * @example + + +
+ Sample Text +
myStyle={{myStyle}}
+ * + * @scenario + 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'); + 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.$onEval(function(){ + var style = this.$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]; + } + element.css(mergedStyle); + }, element); + }; +}); + +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), + self = this; + if (hasBindings(bindings)) { + if (isLeafNode(parentElement[0])) { + parentElement.attr('ng:bind-template', text); + } else { + var cursor = textNode, newElement; + foreach(parseBindings(text), function(text){ + var exp = binding(text); + if (exp) { + newElement = self.element('span'); + newElement.attr('ng:bind', exp); + } else { + newElement = self.text(text); + } + if (msie && text.charAt(0) == ' ') { + newElement = jqLite(' '); + var nbsp = newElement.html(); + newElement.text(text.substr(1)); + newElement.html(nbsp + newElement.html()); + } + cursor.after(newElement); + cursor = newElement; + }); + textNode.remove(); + } + } +}); + +// TODO: this should be widget not a markup +angularTextMarkup('OPTION', function(text, textNode, parentElement){ + if (nodeName(parentElement) == "OPTION") { + var select = document.createElement('select'); + select.insertBefore(parentElement[0].cloneNode(true), _null); + if (!select.innerHTML.match(/.*<\/\s*option\s*>/gi)) { + parentElement.attr('value', text); + } + } +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:href + * + * @description + * Using markup like {{hash}} in an href attribute makes + * the page open to a wrong URL, ff the user clicks that link before + * angular has a chance to replace the {{hash}} with actual URL, the + * link will be broken and will most likely return a 404 error. + * The `ng:href` solves this problem by placing the `href` 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. + */ + +/** + * @workInProgress + * @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. + */ + +var NG_BIND_ATTR = 'ng:bind-attr'; +var SPECIAL_ATTRS = {'ng:src': 'src', 'ng:href': 'href'}; +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)) { + 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 widget + * @name angular.widget.HTML + * + * @description + * The most common widgets you will use will be in the from 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. + * + * @usageContent + * see example below for usage + * + * + * + {{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}} + + + + * @scenario + * 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"]'); + * }); + */ + +function modelAccessor(scope, element) { + var expr = element.attr('name'); + if (expr) { + return { + get: function() { + return scope.$eval(expr); + }, + set: function(value) { + if (value !== _undefined) { + return scope.$tryEval(expr + '=' + toJson(value), element); + } + } + }; + } +} + +function modelFormattedAccessor(scope, element) { + var accessor = modelAccessor(scope, element), + formatterName = element.attr('ng:format') || NOOP, + formatter = angularFormatter(formatterName); + if (!formatter) throw "Formatter named '" + formatterName + "' not found."; + if (accessor) { + return { + get: function() { + return formatter.format(accessor.get()); + }, + set: function(value) { + return accessor.set(formatter.parse(value)); + } + }; + } +} + +function compileValidator(expr) { + return parser(expr).validator()(); +} + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.@ng:validate + * + * @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. + * + * @element INPUT + * @css ng-validation-error + * + * @exampleDescription + * This example shows how the input element becomes red when it contains invalid input. Correct + * the input to make the error disappear. + * + * @example + I don't validate: +
+ + I need an integer or nothing: +
+ * + * @scenario + 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 + * + * @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. + * + * @element INPUT + * @css ng-validation-error + * + * @exampleDescription + * This example shows how the input element becomes red when it contains invalid input. Correct + * the input to make the error disappear. + * + * @example + I cannot be blank:
+ * + * @scenario + 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. + * + * @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter} + * to be used. + * + * @element INPUT + * + * @exampleDescription + * This example shows how the user input is converted from a string and internally represented as an + * array. + * + * @example + Enter a comma separated list of items: + +
list={{list}}
+ * + * @scenario + 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 = angularFormatter(formatterName), + format, parse, lastError, required, + invalidWidgets = scope.$invalidWidgets || {markValid:noop, markInvalid:noop}; + if (!validator) throw "Validator named '" + validatorName + "' not found."; + if (!formatter) throw "Formatter named '" + formatterName + "' not found."; + format = formatter.format; + parse = formatter.parse; + if (requiredExpr) { + scope.$watch(requiredExpr, function(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(element.val()); + validate(); + return value; + } catch (e) { + lastError = e; + elementError(element, NG_VALIDATION_ERROR, e); + } + }, + set: function(value) { + var oldValue = element.val(), + newValue = format(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 options = element[0].options; + return { + get: function(){ + var values = []; + foreach(options, function(option){ + if (option.selected) values.push(option.value); + }); + return values; + }, + set: function(values){ + var keys = {}; + foreach(values, function(value){ keys[value] = true; }); + foreach(options, function(option){ + option.selected = keys[option.value]; + }); + } + }; +} + +function noopAccessor() { return { get: noop, set: noop }; } + +var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue(), true), + buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), + INPUT_TYPE = { + 'text': textWidget, + 'textarea': textWidget, + 'hidden': textWidget, + 'password': textWidget, + 'button': buttonWidget, + 'submit': buttonWidget, + 'reset': buttonWidget, + 'image': buttonWidget, + 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)), + 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), + 'select-one': inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(_null)), + 'select-multiple': inputWidget('change', modelFormattedAccessor, 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 + * + * @description + * The directive executes an expression whenever the input widget changes. + * + * @element INPUT + * @param {expression} expression to execute. + * + * @exampleDescription + * @example +
+ + changeCount {{textCount}}
+ + changeCount {{checkboxCount}}
+ * + * @scenario + it('should check ng:change', function(){ + expect(binding('textCount')).toBe('0'); + expect(binding('checkboxCount')).toBe('0'); + + using('.doc-example-live').input('text').enter('abc'); + expect(binding('textCount')).toBe('1'); + expect(binding('checkboxCount')).toBe('0'); + + + using('.doc-example-live').input('checkbox').check(); + expect(binding('textCount')).toBe('1'); + expect(binding('checkboxCount')).toBe('1'); + }); + */ +function inputWidget(events, modelAccessor, viewAccessor, initFn, dirtyChecking) { + return function(element) { + var scope = this, + model = modelAccessor(scope, element), + view = viewAccessor(scope, element), + action = element.attr('ng:change') || '', + lastValue; + if (model) { + initFn.call(scope, model, view, element); + this.$eval(element.attr('ng:init')||''); + // Don't register a handler if we are a button (noopAccessor) and there is no action + if (action || modelAccessor !== noopAccessor) { + element.bind(events, function (){ + var value = view.get(); + if (!dirtyChecking || value != lastValue) { + model.set(value); + lastValue = model.get(); + scope.$tryEval(action, element); + scope.$root.$eval(); + } + }); + } + scope.$watch(model.get, function(value){ + if (lastValue !== value) { + view.set(lastValue = value); + } + }); + } + }; +} + +function inputWidgetSelector(element){ + this.directives(true); + return INPUT_TYPE[lowercase(element[0].type)] || noop; +} + +angularWidget('input', inputWidgetSelector); +angularWidget('textarea', inputWidgetSelector); +angularWidget('button', inputWidgetSelector); +angularWidget('select', function(element){ + this.descend(true); + return inputWidgetSelector.call(this, element); +}); + + +/* + * Consider this: + * + * + * The issue is that the select gets evaluated before option is unrolled. + * This means that the selection is undefined, but the browser + * default behavior is to show the top selection in the list. + * To fix that we register a $update function on the select element + * and the option creation then calls the $update function when it is + * unrolled. The $update function then calls this update function, which + * then tries to determine if the model is unassigned, and if so it tries to + * chose one of the options from the list. + */ +angularWidget('option', function(){ + this.descend(true); + this.directives(true); + return function(element) { + var select = element.parent(); + var scope = retrieveScope(select); + var model = modelFormattedAccessor(scope, select); + var view = valueAccessor(scope, select); + var option = element; + var lastValue = option.attr($value); + var lastSelected = option.attr('ng-' + $selected); + element.data($$update, function(){ + var value = option.attr($value); + var selected = option.attr('ng-' + $selected); + var modelValue = model.get(); + if (lastSelected != selected || lastValue != value) { + lastSelected = selected; + lastValue = value; + if (selected || modelValue == _null || modelValue == _undefined) + model.set(value); + if (value == modelValue) { + view.set(lastValue); + } + } + }); + }; +}); + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.ng:include + * + * @description + * Include external HTML fragment. + * + * Keep in mind that Same Origin Policy applies to included resources + * (e.g. ng:include won't work for file:// access). + * + * @param {string} src expression evaluating to URL. + * @param {Scope=} [scope=new_child_scope] expression evaluating to angular.scope + * @param {string=} onload Expression to evaluate when a new partial is loaded. + * + * @example + * + * url =
{{url}} + *
+ * + * + * @scenario + * it('should load date filter', function(){ + * expect(element('.doc-example ng\\:include').text()).toMatch(/angular\.filter\.date/); + * }); + * it('should change to hmtl filter', function(){ + * select('url').option('angular.filter.html.html'); + * expect(element('.doc-example ng\\:include').text()).toMatch(/angular\.filter\.html/); + * }); + * it('should change to blank', function(){ + * select('url').option('(blank)'); + * expect(element('.doc-example ng\\:include').text()).toEqual(''); + * }); + */ +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 + if (element[0]['ng:compiled']) { + this.descend(true); + this.directives(true); + } else { + element[0]['ng:compiled'] = true; + return extend(function(xhr, element){ + var scope = this, childScope; + var changeCounter = 0; + var preventRecursion = false; + function incrementChange(){ changeCounter++;} + this.$watch(srcExp, incrementChange); + this.$watch(scopeExp, incrementChange); + scope.$onEval(function(){ + if (childScope && !preventRecursion) { + preventRecursion = true; + try { + childScope.$eval(); + } finally { + preventRecursion = false; + } + } + }); + this.$watch(function(){return changeCounter;}, function(){ + var src = this.$eval(srcExp), + useScope = this.$eval(scopeExp); + + if (src) { + xhr('GET', src, function(code, response){ + element.html(response); + childScope = useScope || createScope(scope); + compiler.compile(element)(element, childScope); + childScope.$init(); + scope.$eval(onloadExp); + }); + } else { + childScope = null; + element.html(''); + } + }); + }, {$inject:['$xhr.cache']}); + } +}); + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.ng:switch + * + * @description + * 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 + + switch={{switch}} + + +
Settings Div
+ Home Span + default +
+ + * + * @scenario + * it('should start in settings', function(){ + * expect(element('.doc-example ng\\:switch').text()).toEqual('Settings Div'); + * }); + * it('should change to home', function(){ + * select('switch').option('home'); + * expect(element('.doc-example ng\\:switch').text()).toEqual('Home Span'); + * }); + * it('should select deafault', function(){ + * select('switch').option('other'); + * expect(element('.doc-example ng\\:switch').text()).toEqual('default'); + * }); + */ +var ngSwitch = angularWidget('ng:switch', function (element){ + var compiler = this, + watchExpr = element.attr("on"), + usingExpr = (element.attr("using") || 'equals'), + usingExprParams = usingExpr.split(":"), + usingFn = ngSwitch[usingExprParams.shift()], + changeExpr = element.attr('change') || '', + cases = []; + if (!usingFn) throw "Using expression '" + usingExpr + "' unknown."; + if (!watchExpr) throw "Missing 'on' attribute."; + eachNode(element, function(caseElement){ + var when = caseElement.attr('ng:switch-when'); + var switchCase = { + change: changeExpr, + element: caseElement, + template: compiler.compile(caseElement) + }; + if (isString(when)) { + switchCase.when = function(scope, value){ + var args = [value, when]; + foreach(usingExprParams, function(arg){ + args.push(arg); + }); + return usingFn.apply(scope, args); + }; + cases.unshift(switchCase); + } else if (isString(caseElement.attr('ng:switch-default'))) { + switchCase.when = valueFn(true); + cases.push(switchCase); + } + }); + + // this needs to be here for IE + foreach(cases, function(_case){ + _case.element.remove(); + }); + + element.html(''); + return function(element){ + var scope = this, childScope; + this.$watch(watchExpr, function(value){ + var found = false; + element.html(''); + childScope = createScope(scope); + foreach(cases, function(switchCase){ + if (!found && switchCase.when(childScope, value)) { + found = true; + var caseElement = quickClone(switchCase.element); + element.append(caseElement); + childScope.$tryEval(switchCase.change, element); + switchCase.template(caseElement, childScope); + childScope.$init(); + } + }); + }); + scope.$onEval(function(){ + if (childScope) childScope.$eval(); + }); + }; +}, { + equals: function(on, when) { + return ''+on == when; + }, + route: switchRouteMatcher +}); + + +/* + * Modifies the default behavior of html A tag, so that the default action is prevented when href + * attribute is empty. + * + * 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) { + if (element.attr('href') === '') { + element.bind('click', function(event){ + event.preventDefault(); + }); + } + }; +}); + + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.@ng:repeat + * + * @description + * `ng:repeat` instantiates a template once per item from a collection. The collection is enumerated + * with `ng:repeat-index` attribute starting from 0. 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. + * + * There are special properties exposed on the local scope of each template instance: + * + * * `$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'` or `'last'`. + * + * NOTE: `ng:repeat` looks like a directive, but 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}`. + * + * @exampleDescription + * This example initializes the scope to a list of names and + * than uses `ng:repeat` to display every person. + * @example +
+ I have {{friends.length}} friends. They are: +
    +
  • + [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +
  • +
+
+ * @scenario + 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"]); + }); + */ +angularWidget("@ng:repeat", function(expression, element){ + element.removeAttr('ng:repeat'); + element.replaceWith(this.comment("ng:repeat: " + expression)); + var template = this.compile(element); + return function(reference){ + 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 children = [], currentScope = this; + this.$onEval(function(){ + var index = 0, + childCount = children.length, + lastElement = reference, + collection = this.$tryEval(rhs, reference), + is_array = isArray(collection), + collectionLength = 0, + childScope, + key; + + if (is_array) { + collectionLength = collection.length; + } else { + for (key in collection) + if (collection.hasOwnProperty(key)) + collectionLength++; + } + + for (key in collection) { + if (!is_array || collection.hasOwnProperty(key)) { + if (index < childCount) { + // reuse existing child + childScope = children[index]; + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + } else { + // grow children + childScope = template(quickClone(element), createScope(currentScope)); + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + lastElement.after(childScope.$element); + childScope.$index = index; + childScope.$position = index == 0 ? + 'first' : + (index == collectionLength - 1 ? 'last' : 'middle'); + childScope.$element.attr('ng:repeat-index', index); + childScope.$init(); + children.push(childScope); + } + childScope.$eval(); + lastElement = childScope.$element; + index ++; + } + } + // shrink children + while(children.length > index) { + children.pop().$element.remove(); + } + }, reference); + }; +}); + + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.@ng:non-bindable + * + * @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. + * + * NOTE: `ng:non-bindable` looks like a directive, but is actually an attribute widget. + * + * @element ANY + * + * @exampleDescription + * In this example there are two location where a siple binding (`{{}}`) is present, but the one + * wrapped in `ng:non-bindable` is left alone. + * + * @example +
Normal: {{1 + 2}}
+
Ignored: {{1 + 2}}
+ * + * @scenario + 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); +var browserSingleton; +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$browser + * @requires $log + * + * @description + * Represents the browser. + */ +angularService('$browser', function($log){ + if (!browserSingleton) { + browserSingleton = new Browser( + window.location, + jqLite(window.document), + jqLite(window.document.getElementsByTagName('head')[0]), + XHR, + $log, + window.setTimeout); + browserSingleton.startPoller(50, function(delay, fn){setTimeout(delay,fn);}); + browserSingleton.bind(); + } + return browserSingleton; +}, {inject:['$log']}); + +extend(angular, { + 'element': jqLite, + '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 +}); + + + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + angularInit(angularJsConfig(document)); + }; + +})(window, document, window.onload); +document.write(''); \ No newline at end of file diff --git a/app/lib/angular/angular.min.js b/app/lib/angular/angular.min.js new file mode 100644 index 0000000..3f5a675 --- /dev/null +++ b/app/lib/angular/angular.min.js @@ -0,0 +1,106 @@ +(function(H,Y,ub){function u(a,b,c){var d;if(a)if(J(a))for(d in a)d!="prototype"&&d!=fc&&d!=gc&&a.hasOwnProperty(d)&&b.call(c,a[d],d);else if(a.forEach)a.forEach(b,c);else if(T(a)&&wa(a.length))for(d=0;d2?Na.call(arguments,2,arguments.length):[];return typeof b==ia?c.length?function(){return arguments.length?b.apply(a,c.concat(Na.call(arguments, +0,arguments.length))):b.apply(a,c)}:function(){return arguments.length?b.apply(a,arguments):b.call(a)}:b}function ya(a){if(a&&a.length!==0){a=Q(""+a);a=!(a=="f"||a=="0"||a=="false"||a=="no"||a=="n"||a=="[]")}else a=false;return a}function yb(a,b){var c=new zb(eb,Ab,K,Z),d=A(a);return c.compile(d)(d,b)}function fb(a){var b={},c,d;u((a||"").split("&"),function(e){if(e){c=e.split("=");d=unescape(c[0]);b[d]=E(c[1])?unescape(c[1]):true}});return b}function Bb(a){var b=[];u(a,function(c,d){b.push(escape(d)+ +(c===true?"":"="+escape(c)))});return b.length?b.join("&"):""}function pc(a,b){var c=a.getElementsByTagName("script"),d;b=D({ie_compat_id:"ng-ie-compat"},b);for(var e=0;e1;d++){var e=b.shift(),f=a[e];if(!f){f={};a[e]=f}a=f}return a[b.shift()]=c}function Fb(a){var b=Gb[a];if(b)return b;var c="var l, fn, t;\n";u(a.split("."),function(d){d=Hb[d]?'["'+d+'"]':"."+d;c+="if(!s) return s;\nl=s;\ns=s"+d+';\nif(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l'+d+".apply(l, arguments); };\n";if(d.charAt(1)== +"$"){d=d.substr(2);c+='if(!s) {\n t = angular.Global.typeOf(l);\n fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["'+d+'"];\n if (fn) s = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0, arguments.length))); };\n}\n'}});c+="return s;";b=Function("s",c);b.toString=function(){return c};return Gb[a]=b}function ta(a){if(typeof a===ia)return a;var b=Ib[a];if(!b){b=Pa(a);var c=b.statements();b.assertAllConsumed();b=Ib[a]=D(function(){return c(this)},{fnSelf:c})}return b} +function ua(a,b,c){function d(){}a=d.prototype=a||{};var e=new d,f={sorted:[]};D(e,{"this":e,$id:tc++,$parent:a,$bind:M(e,M,e),$get:M(e,Ca,e),$set:M(e,Ra,e),$eval:function(g){var h=typeof g,i,j,k;if(h==qa){g=0;for(h=f.sorted.length;g0){var G=ja[0],da=G.text;if(da==l||da==q||da==w||da==x||!l&&!q&&!w&&!x)return G}return false}function f(l,q,w,x){if(l=e(l,q,w,x)){if(b&&!l.json){index=l.index;c("is not valid json",l)}ja.shift();return this.currentToken=l}return false}function g(l){f(l)||c("is unexpected, expecting ["+l+"]",e())}function h(l,q){return function(w){return l(w,q(w))}}function i(l,q,w){return function(x){return q(x,l(x),w(x))}}function j(){ja.length!==0&&c("is extra token not part of expression", +ja[0])}function k(){for(var l=[];;){ja.length>0&&!e("}",")",";","]")&&l.push(p());if(!f(";"))return function(q){for(var w,x=0;x","<=",">="))l=i(l,q.fn,z());return l}function O(){for(var l=L(),q;q= +f("*","/","%");)l=i(l,q.fn,L());return l}function L(){var l;return f("+")?N():(l=f("-"))?i(wc,l.fn,L()):(l=f("!"))?h(l.fn,L()):N()}function v(l){var q=f(),w=q.text.split(".");l=l;for(var x,G=0;G0;){l.push(yc());f(";")||j()}j();return function(q){for(var w=0;w4096&&e.warn("Cookie '"+n+"' possibly not set or overflowed because it was too large ("+z+" > 4096 bytes)!");o.length>20&&e.warn("Cookie '"+n+"' possibly not set or overflowed because too many cookies were already set ("+o.length+" > 20 )")}}else{if(t.cookie!== +m){m=t.cookie;z=m.split("; ");o={};for(O=0;O=0;k--)if(g[k]== +j)break;if(k>=0){for(p=g.length-1;p>=k;p--)b.end(g[p]);g.length=k}}var e,f,g=[],h=a;for(g.last=function(){return g[g.length-1]};a;){f=true;if(!g.last()||!Rb[g.last()]){if(a.indexOf("/g, + CDATA_REGEXP = //g, + URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) + +// Empty Elements - HTML 4.01 +var emptyElements = makeMap("area,br,col,hr,img"); + +// 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"); + +// 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); + +//see: http://www.w3.org/TR/html4/index/attributes.html +//Attributes that have their values filled in disabled="disabled" +var fillAttrs = makeMap("compact,ismap,nohref,nowrap"); +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,href,longdesc,src,usemap"); +var validAttrs = extend({}, fillAttrs, uriAttrs, makeMap( + 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ + 'color,cols,colspan,coords,dir,face,headers,height,hreflang,hspace,'+ + 'lang,language,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; + } + + // 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; + } + + // 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; + } + } + + if ( chars ) { + index = html.indexOf("<"); + + var text = index < 0 ? html : html.substring( 0, index ); + html = index < 0 ? "" : html.substring( index ); + + handler.chars( decodeEntities(text) ); + } + + } else { + html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ + text = text. + replace(COMMENT_REGEXP, "$1"). + replace(CDATA_REGEXP, "$1"); + + handler.chars( decodeEntities(text) ); + + return ""; + }); + + parseEndTag( "", stack.last() ); + } + + if ( html == last ) { + throw "Parse Error: " + html; + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag( tag, tagName, rest, unary ) { + tagName = lowercase(tagName); + if ( blockElements[ tagName ] ) { + while ( stack.last() && inlineElements[ stack.last() ] ) { + parseEndTag( "", stack.last() ); + } + } + + if ( closeSelfElements[ tagName ] && stack.last() == tagName ) { + parseEndTag( "", tagName ); + } + + unary = emptyElements[ tagName ] || !!unary; + + if ( !unary ) + stack.push( tagName ); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, function(match, name) { + var value = arguments[2] ? arguments[2] : + arguments[3] ? arguments[3] : + arguments[4] ? arguments[4] : + fillAttrs[name] ? name : ""; + + attrs[name] = decodeEntities(value); //value.replace(/(^|[^\\])"/g, '$1\\\"') //" + }); + + 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; + + if ( pos >= 0 ) { + // Close all the open elements, up the stack + for ( i = stack.length - 1; i >= pos; i-- ) + handler.end( stack[ i ] ); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +/** + * @param str 'key1,key2,...' + * @returns {object} in the form of {key1:true, key2:true, ...} + */ +function makeMap(str){ + var obj = {}, items = str.split(","), i; + for ( i = 0; i < items.length; i++ ) + obj[ items[i] ] = true; + return obj; +} + +/** + * 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, '>'); +} + +/** + * 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('"'); + } + }); + 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)); + } + } + }; +} +////////////////////////////////// +//JQLite +////////////////////////////////// + +var jqCache = {}, + jqName = 'ng-' + new Date().getTime(), + jqId = 1, + addEventListener = (window.document.attachEvent ? + function(element, type, fn) {element.attachEvent('on' + type, fn);} : + function(element, type, fn) {element.addEventListener(type, fn, false);}), + removeEventListener = (window.document.detachEvent ? + function(element, type, fn) {element.detachEvent('on' + type, fn); } : + function(element, type, fn) { element.removeEventListener(type, fn, false); }); + +function jqNextId() { return (jqId++); } + +function jqClearData(element) { + var cacheId = element[jqName], + cache = jqCache[cacheId]; + if (cache) { + foreach(cache.bind || {}, function(fn, type){ + removeEventListener(element, type, fn); + }); + delete jqCache[cacheId]; + if (msie) + element[jqName] = ''; // ie does not allow deletion of attributes on elements. + else + delete element[jqName]; + } +} + +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; +} + +function JQLite(element) { + if (isElement(element)) { + this[0] = element; + this.length = 1; + } else if (isDefined(element.length) && element.item) { + for(var i=0; i < element.length; i++) { + this[i] = element[i]; + } + this.length = element.length; + } +} + +JQLite.prototype = { + data: function(key, value) { + var element = this[0], + 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; + } + }, + + removeData: function(){ + jqClearData(this[0]); + }, + + dealoc: function(){ + (function dealoc(element){ + jqClearData(element); + for ( var i = 0, children = element.childNodes; i < children.length; i++) { + dealoc(children[i]); + } + })(this[0]); + }, + + bind: function(type, fn){ + var self = this, + element = self[0], + bind = self.data('bind'), + eventHandler; + if (!bind) this.data('bind', bind = {}); + foreach(type.split(' '), function(type){ + 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 + }; + } + foreach(eventHandler.fns, function(fn){ + fn.call(self, event); + }); + }; + eventHandler.fns = []; + addEventListener(element, type, eventHandler); + } + eventHandler.fns.push(fn); + }); + }, + + replaceWith: function(replaceNode) { + this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); + }, + + children: function() { + return new JQLite(this[0].childNodes); + }, + + append: function(node) { + var self = this[0]; + node = jqLite(node); + foreach(node, function(child){ + self.appendChild(child); + }); + }, + + remove: function() { + this.dealoc(); + var parentNode = this[0].parentNode; + if (parentNode) parentNode.removeChild(this[0]); + }, + + removeAttr: function(name) { + this[0].removeAttribute(name); + }, + + after: function(element) { + this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling); + }, + + hasClass: function(selector) { + var className = " " + selector + " "; + if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) { + return true; + } + return false; + }, + + removeClass: function(selector) { + this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", "")); + }, + + toggleClass: function(selector, condition) { + var self = this; + (condition ? self.addClass : self.removeClass).call(self, selector); + }, + + addClass: function( selector ) { + if (!this.hasClass(selector)) { + this[0].className = trim(this[0].className + ' ' + selector); + } + }, + + css: function(name, value) { + var style = this[0].style; + if (isString(name)) { + if (isDefined(value)) { + style[name] = value; + } else { + return style[name]; + } + } else { + extend(style, name); + } + }, + + attr: function(name, value){ + var e = this[0]; + if (isObject(name)) { + foreach(name, function(value, name){ + e.setAttribute(name, value); + }); + } else if (isDefined(value)) { + e.setAttribute(name, value); + } else { + // the extra argument is to get the right thing for a.href in IE, see jQuery code + return e.getAttribute(name, 2); + } + }, + + text: function(value) { + if (isDefined(value)) { + this[0].textContent = value; + } + return this[0].textContent; + }, + + val: function(value) { + if (isDefined(value)) { + this[0].value = value; + } + return this[0].value; + }, + + html: function(value) { + if (isDefined(value)) { + var i = 0, childNodes = this[0].childNodes; + for ( ; i < childNodes.length; i++) { + jqLite(childNodes[i]).dealoc(); + } + this[0].innerHTML = value; + } + return this[0].innerHTML; + }, + + parent: function() { + return jqLite(this[0].parentNode); + }, + + clone: function() { return jqLite(this[0].cloneNode(true)); } +}; + +if (msie) { + extend(JQLite.prototype, { + text: function(value) { + var e = this[0]; + // NodeType == 3 is text node + if (e.nodeType == 3) { + if (isDefined(value)) e.nodeValue = value; + return e.nodeValue; + } else { + if (isDefined(value)) e.innerText = value; + return e.innerText; + } + } + }); +} +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; + } + return type; + } +}; + + +/** + * @workInProgress + * @ngdoc overview + * @name angular.Object + * @function + * + * @description + * Utility functions for manipulation with JavaScript objects. + * + * These functions are exposed in two ways: + * + * - **in angular expressions**: the functions are bound to all objects and augment the Object + * type. The names of these methods are prefixed with `$` character to minimize naming collisions. + * To call a method, invoke the function without the first argument, e.g, `myObject.$foo(param2)`. + * + * - **in JavaScript code**: the functions don't augment the Object type and must be invoked as + * functions of `angular.Object` as `angular.Object.foo(myObject, param2)`. + * + */ +var angularCollection = { + 'copy': copy, + 'size': size, + 'equals': equals +}; +var angularObject = { + 'extend': extend +}; + +/** + * @workInProgress + * @ngdoc overview + * @name angular.Array + * + * @description + * Utility functions for manipulation with JavaScript Array objects. + * + * These functions are exposed in two ways: + * + * - **in angular expressions**: the functions are bound to the Array objects and augment the Array + * type as array methods. The names of these methods are prefixed with `$` character to minimize + * naming collisions. To call a method, invoke `myArrayObject.$foo(params)`. + * + * - **in JavaScript code**: the functions don't augment the Array type and must be invoked as + * functions of `angular.Array` as `angular.Array.foo(myArrayObject, params)`. + * + */ +var angularArray = { + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.indexOf + * @function + * + * @description + * Determines the index of `value` in `array`. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array Array to search. + * @param {*} value Value to search for. + * @returns {number} The position of the element in `array`. The position is 0-based. `-1` is returned if the value can't be found. + * + * @example +
+
+ Index of '{{bookName}}' in the list {{books}} is {{books.$indexOf(bookName)}}. + + @scenario + it('should correctly calculate the initial index', function() { + expect(binding('books.$indexOf(bookName)')).toBe('2'); + }); + + it('should recalculate', function() { + input('bookName').enter('foo'); + expect(binding('books.$indexOf(bookName)')).toBe('-1'); + + input('bookName').enter('Moby Dick'); + expect(binding('books.$indexOf(bookName)')).toBe('0'); + }); + */ + 'indexOf': indexOf, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.sum + * @function + * + * @description + * This function calculates the sum of all numbers in `array`. If the `expressions` is supplied, + * it is evaluated once for each element in `array` and then the sum of these values is returned. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @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}}
+ + @scenario + //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'); + }); + + it('should add an entry and recalculate', function() { + element('.doc-example 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'); + + 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 = angular['Function']['compile'](expression); + var sum = 0; + for (var i = 0; i < array.length; i++) { + var value = 1 * fn(array[i]); + if (!isNaN(value)){ + sum += value; + } + } + return sum; + }, + + + /** + * @workInProgress + * @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 info. + * + * @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}} + + @scenario + it('should initialize the task list with for tasks', function() { + expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(4); + expect(repeater('.doc-example ul li', 'task in tasks').column('task')). + toEqual(['Learn Angular', 'Read Documentation', 'Check out demos', + 'Build cool applications']); + }); + + it('should initialize the task list with for tasks', function() { + element('.doc-example ul li a:contains("X"):first').click(); + expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(3); + + element('.doc-example ul li a:contains("X"):last').click(); + expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(2); + + expect(repeater('.doc-example 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; + }, + + + /** + * @workInProgress + * @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 info. + * + * @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}}
+ + @scenario + 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']); + }); + */ + 'filter':function(array, expression) { + 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){ + if (text.charAt(0) === '!') { + 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; + } + } + return false; + case "array": + for ( var i = 0; i < obj.length; i++) { + if (search(obj[i], text)) { + return true; + } + } + return false; + default: + return false; + } + }; + switch (typeof expression) { + case "boolean": + case "number": + case "string": + expression = {$:expression}; + case "object": + for (var key in expression) { + if (key == '$') { + (function(){ + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(value, text); + }); + })(); + } else { + (function(){ + var path = key; + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(getter(value, path), text); + }); + })(); + } + } + break; + case $function: + predicates.push(expression); + break; + default: + return array; + } + var filtered = []; + for ( var j = 0; j < array.length; j++) { + var value = array[j]; + if (predicates.check(value)) { + filtered.push(value); + } + } + return filtered; + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.add + * @function + * + * @description + * `add` is a function similar to JavaScript's `Array#push` method, in that it appends a new + * element to an array, but it differs in that the value being added is optional and defaults to + * an emty object. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array The array expand. + * @param {*=} [value={}] The value to be added. + * @returns {Array} The expanded array. + * + * @exampleDescription + * This example shows how an initially empty array can be filled with objects created from user + * input via the `$add` method. + * + * @example + [add empty] + [add 'John'] + [add 'Mary'] + +
    +
  • + + + [X] +
  • +
+
people = {{people}}
+ + @scenario + beforeEach(function() { + expect(binding('people')).toBe('people = []'); + }); + + it('should create an empty record when "add empty" is clicked', function() { + element('.doc-example a:contains("add empty")').click(); + expect(binding('people')).toBe('people = [{\n "name":"",\n "sex":null}]'); + }); + + it('should create a "John" record when "add \'John\'" is clicked', function() { + element('.doc-example a:contains("add \'John\'")').click(); + expect(binding('people')).toBe('people = [{\n "name":"John",\n "sex":"male"}]'); + }); + + it('should create a "Mary" record when "add \'Mary\'" is clicked', function() { + element('.doc-example 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 a:contains("add empty")').click(); + element('.doc-example li a:contains("X"):first').click(); + expect(binding('people')).toBe('people = []'); + }); + */ + 'add':function(array, value) { + array.push(isUndefined(value)? {} : value); + return array; + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.count + * @function + * + * @description + * Determines the number of elements in an array. Optionally it will count only those elements + * for which the `condition` evaluets to `true`. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array The array to count elements in. + * @param {(function()|string)=} condition A function to be evaluated or angular expression to be + * compiled and evaluated. The element that is currently being iterated over, is exposed to + * the `condition` as `this`. + * @returns {number} Number of elements in the array (for which the 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')}}

+ + @scenario + it('should calculate counts', function() { + expect(binding('items.$count(\'points==1\')')).toEqual(2); + expect(binding('items.$count(\'points>1\')')).toEqual(1); + }); + + it('should recalculate when updated', function() { + using('.doc-example 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 = angular['Function']['compile'](condition), count = 0; + foreach(array, function(value){ + if (fn(value)) { + count ++; + } + }); + return count; + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.orderBy + * @function + * + * @description + * Orders `array` by the `expression` predicate. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @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`: JavaScript's Array#sort comparator function + * - `string`: 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 (e.g. +name or -name). + * - `Array`: array of function or string predicates, such that a first predicate in the array + * is used for sorting, but when the items are equivalent next predicate is used. + * + * @param {boolean=} reverse Reverse the order the array. + * @returns {Array} Sorted copy of the source array. + * + * @example +
+ +
Sorting predicate = {{predicate}}
+
+ + + + + + + + + + + +
Name + (^)Phone + (^)Age + (^)
{{friend.name}}{{friend.phone}}{{friend.age}}
+ + @scenario + it('should be reverse ordered by aged', function() { + expect(binding('predicate')).toBe('Sorting predicate = -age'); + expect(repeater('.doc-example table', 'friend in friends').column('friend.age')). + toEqual(['35', '29', '21', '19', '10']); + expect(repeater('.doc-example table', '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 a:contains("Name")').click(); + expect(repeater('.doc-example table', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); + expect(repeater('.doc-example table', 'friend in friends').column('friend.age')). + toEqual(['35', '10', '29', '19', '21']); + + element('.doc-example a:contains("Phone")+a:contains("^")').click(); + expect(repeater('.doc-example table', 'friend in friends').column('friend.phone')). + toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); + expect(repeater('.doc-example table', 'friend in friends').column('friend.name')). + toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); + }); + */ + //TODO: WTH is descend param for and how/when it should be used, how is it affected by +/- in + // predicate? the code below is impossible to read and specs are not very good. + 'orderBy':function(array, expression, descend) { + expression = isArray(expression) ? expression: [expression]; + expression = map(expression, function($){ + var descending = false, get = $ || identity; + if (isString($)) { + if (($.charAt(0) == '+' || $.charAt(0) == '-')) { + descending = $.charAt(0) == '-'; + $ = $.substring(1); + } + get = expressionCompile($).fnSelf; + } + return reverse(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(reverse(comparator, descend)); + + function comparator(o1, o2){ + for ( var i = 0; i < expression.length; i++) { + var comp = expression[i](o1, o2); + if (comp !== 0) return comp; + } + return 0; + } + function reverse(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; + } + } + }, + + + /** + * @workInProgress + * @ngdoc function + * @name angular.Array.limitTo + * @function + * + * @description + * Creates a new array containing only the first, or last `limit` number of elements of the + * source `array`. + * + * Note: this function is used to augment the Array type in angular expressions. See + * {@link angular.Array} for more info. + * + * @param {Array} array Source array to be limited. + * @param {string|Number} limit The length of the returned array. If the number is positive, the + * first `limit` items from the source array will be copied, if the number is negative, the + * last `limit` items will be copied. + * @returns {Array} New array of length `limit`. + * + */ + limitTo: function(array, limit) { + limit = parseInt(limit, 10); + var out = [], + i, n; + + if (limit > 0) { + i = 0; + n = limit; + } else { + i = array.length + limit; + n = array.length; + } + + for (; i
+ {{amount | currency}} + * + * @scenario + it('should init with 1234.56', function(){ + expect(binding('amount | currency')).toBe('$1,234.56'); + }); + it('should update', function(){ + input('amount').enter('-1234'); + expect(binding('amount | currency')).toBe('$-1,234.00'); + expect(element('.doc-example-live .ng-binding').attr('className')). + toMatch(/ng-format-negative/); + }); + */ +angularFilter.currency = function(amount){ + this.$element.toggleClass('ng-format-negative', amount < 0); + return '$' + angularFilter['number'].apply(this, [amount, 2]); +}; + +/** + * @workInProgress + * @ngdoc filter + * @name angular.filter.number + * @function + * + * @description + * Formats a number as text. + * + * If the input is not a number 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}} + + * @scenario + it('should format numbers', function(){ + expect(binding('val | number')).toBe('1,234.57'); + 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.33'); + expect(binding('val | number:0')).toBe('3,374'); + expect(binding('-val | number:4')).toBe('-3,374.3330'); + }); + */ +angularFilter.number = function(number, fractionSize){ + if (isNaN(number) || !isFinite(number)) { + return ''; + } + fractionSize = typeof fractionSize == $undefined ? 2 : fractionSize; + var isNegative = number < 0; + number = Math.abs(number); + var pow = Math.pow(10, fractionSize); + var text = "" + Math.round(number * pow); + var whole = text.substring(0, text.length - fractionSize); + whole = whole || '0'; + var frc = text.substring(text.length - fractionSize); + text = isNegative ? '-' : ''; + for (var i = 0; i < whole.length; i++) { + if ((whole.length - i)%3 === 0 && i !== 0) { + text += ','; + } + text += whole.charAt(i); + } + if (fractionSize > 0) { + for (var j = frc.length; j < fractionSize; j++) { + frc += '0'; + } + text += '.' + frc.substring(0, fractionSize); + } + return text; +}; + + +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); + }; +} + + +var DATE_FORMATS = { + yyyy: dateGetter('FullYear', 4), + yy: dateGetter('FullYear', 2, 0, 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), + a: function(date){return date.getHours() < 12 ? 'am' : 'pm';}, + Z: function(date){ + var offset = date.getTimezoneOffset(); + return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2); + } +}; + + +var DATE_FORMATS_SPLIT = /([^yMdHhmsaZ]*)(y+|M+|d+|H+|h+|m+|s+|a|Z)(.*)/; +var NUMBER_STRING = /^\d+$/; + + +/** + * @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. 2010 + * * `'yy'`: 2 digit representation of year, padded (00-99) + * * `'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) + * * `'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) + * + * @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. 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:'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'}}
+ * + * @scenario + it('should format date', function(){ + 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) { + if (isString(date)) { + if (NUMBER_STRING.test(date)) { + date = parseInt(date, 10); + } else { + date = angularString.toDate(date); + } + } + + if (isNumber(date)) { + date = new Date(date); + } + + if (!isDate(date)) { + return date; + } + + var text = date.toLocaleDateString(), fn; + if (format && isString(format)) { + text = ''; + var parts = []; + while(format) { + parts = concat(parts, DATE_FORMATS_SPLIT.exec(format), 1); + format = parts.pop(); + } + foreach(parts, function(value){ + fn = DATE_FORMATS[value]; + text += fn ? fn(date) : value; + }); + } + 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: + +
{{ obj | json }}
+ * + * @scenario + it('should jsonify filtered objects', function() { + expect(binding('obj | json')).toBe('{\n "a":1,\n "b":[]}'); + }); + + it('should update', function() { + input('objTxt').enter('[1, 2, 3]'); + expect(binding('obj | json')).toBe('[1,2,3]'); + }); + * + */ +angularFilter.json = function(object) { + this.$element.addClass("ng-monospace"); + return toJson(object, true); +}; + + +/** + * @workInProgress + * @ngdoc filter + * @name angular.filter.lowercase + * @function + * + * @see angular.lowercase + */ +angularFilter.lowercase = lowercase; + + +/** + * @workInProgress + * @ngdoc filter + * @name angular.filter.uppercase + * @function + * + * @see angular.uppercase + */ +angularFilter.uppercase = uppercase; + + +/** + * @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>
+ * + * @scenario + 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'); + }); + */ +angularFilter.html = function(html, option){ + return new HTML(html, option); +}; + + +/** + * @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 + * plane 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>
+ + @scenario + 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.'); + }); + */ +//TODO: externalize all regexps +angularFilter.linky = function(text){ + if (!text) return text; + var URL = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/; + var match; + var raw = text; + var html = []; + var writer = htmlSanitizeWriter(html); + var url; + var i; + while (match=raw.match(URL)) { + // 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:/, '')); + writer.end('a'); + raw = raw.substring(i + match[0].length); + } + writer.chars(raw); + return new HTML(html.join('')); +}; +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 + * + * @description + * Formats the user input as JSON text. + * + * @returns {string} A JSON string representation of the model. + * + * @example + *
+ * + *
data={{data}}
+ *
+ * + * @scenario + * 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, fromJson); + +/** + * @workInProgress + * @ngdoc formatter + * @name angular.formatter.boolean + * + * @description + * Use boolean formatter if you wish to store the data as boolean. + * + * @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`. + * + * @example + * Enter truthy text: + * + * + *
value={{value}}
+ * + * @scenario + * 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 + * + * @description + * Use number formatter if you wish to convert the user entered string to a number. + * + * @returns {number} Number from the parsed string. + * + * @example + * Enter valid number: + * + *
value={{value}}
+ * + * @scenario + * 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 + * + * @description + * Use list formatter if you wish to convert the user entered string to an array. + * + * @returns {Array} Array parsed from the entered string. + * + * @example + * Enter a list of items: + * + * + *
value={{value}}
+ * + * @scenario + * 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 :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 + * + * @description + * Use trim formatter if you wish to trim extra spaces in user text. + * + * @returns {String} Trim excess leading and trailing space. + * + * @example + * Enter text with leading/trailing spaces: + * + * + *
value={{value|json}}
+ * + * @scenario + * it('should format trim', function(){ + * expect(binding('value')).toEqual('value="book"'); + * this.addFutureAction('change to XYZ', function($window, $document, done){ + * $document.elements('.doc-example :input:last').val(' text ').trigger('change'); + * done(); + * }); + * expect(binding('value')).toEqual('value="text"'); + * }); + */ +angularFormatter.trim = formatter( + function(obj) { return obj ? trim("" + obj) : ""; } +); +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 {regexp} expression regular expression. + * @css ng-validation-error + * + * @example + * + * Enter valid SSN: + * + * + * @scenario + * it('should invalidate non ssn', function(){ + * var textBox = element('.doc-example :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:
+ * + * @scenario + * it('should invalidate number', function(){ + * var n1 = element('.doc-example :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 :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 :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):
+ * + * @scenario + * it('should invalidate integer', function(){ + * var n1 = element('.doc-example :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 :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 :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: + * + * + * @scenario + * it('should invalidate date', function(){ + * var n1 = element('.doc-example :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: + * + * + * @scenario + * it('should invalidate email', function(){ + * var n1 = element('.doc-example :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 + * @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: + * + * + * @scenario + * it('should invalidate phone', function(){ + * var n1 = element('.doc-example :input'); + * expect(n1.attr('className')).not().toMatch(/ng-validation-error/); + * input('text').enter('+12345678'); + * expect(n1.attr('className')).toMatch(/ng-validation-error/); + * }); + * + */ + '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 internationaly."; + }, + + /** + * @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 phone number: + * + * + * @scenario + * it('should invalidate url', function(){ + * var n1 = element('.doc-example :input'); + * expect(n1.attr('className')).not().toMatch(/ng-validation-error/); + * input('text').enter('abc://server/path'); + * expect(n1.attr('className')).toMatch(/ng-validation-error/); + * }); + * + */ + '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."; + }, + + /** + * @workInProgress + * @ngdoc validator + * @name angular.validator.json + * @description + * Use json validator if you wish to restrict the user input to a valid JSON. + * + * @param {string} value value to validate + * @css ng-validation-error + * + * @example + * + * + * @scenario + * it('should invalidate json', function(){ + * var n1 = element('.doc-example :input'); + * expect(n1.attr('className')).not().toMatch(/ng-validation-error/); + * input('json').enter('{name}'); + * expect(n1.attr('className')).toMatch(/ng-validation-error/); + * }); + * + */ + 'json': function(value) { + try { + fromJson(value); + return _null; + } catch (e) { + return e.toString(); + } + }, + + /** + * @workInProgress + * @ngdoc validator + * @name angular.validator.asynchronous + * @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: + * + * + * @scenario + * it('should change color in delayed way', function(){ + * var textBox = element('.doc-example :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/); + * + * pause(.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' + * } + * + */ + '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:{}}); + } + + cache.current = input; + + var inputState = cache.inputs[input]; + if (!inputState) { + cache.inputs[input] = inputState = { inFlight: true }; + scope.$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'); + scope.$invalidWidgets.markValid(element); + } + element.data($$validate)(); + scope.$root.$eval(); + }); + } else if (inputState.inFlight) { + // request in flight, mark widget invalid, but don't show it to user + scope.$invalidWidgets.markInvalid(scope.$element); + } else { + (updateFn||noop)(inputState.response); + } + return inputState.error; + } + +}); +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, + HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}, + EAGER = 'eager', + EAGER_PUBLISHED = EAGER + '-published'; + +function angularServiceInject(name, fn, inject, eager) { + angularService(name, fn, {$inject:inject, $creation:eager}); +} + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$window + * + * @description + * Is 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 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), [], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$document + * @requires $window + * + * @description + * Reference to the browser window.document, but wrapped into angular.element(). + */ +angularServiceInject("$document", function(window){ + return jqLite(window.document); +}, ['$window'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$location + * @requires $browser + * + * @property {string} href + * @property {string} protocol + * @property {string} host + * @property {number} port + * @property {string} path + * @property {Object.} search + * @property {string} hash + * @property {string} hashPath + * @property {Object.} hashSearch + * + * @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 to url. + * Notice that using browser's forward/back buttons changes the $location. + * + * @example + clear hash | + test hash
+ +
$location = {{$location}}
+ */ +angularServiceInject("$location", function(browser) { + var scope = this, + location = {toString:toString, update:update, updateHash: updateHash}, + lastBrowserUrl = browser.getUrl(), + lastLocationHref, + lastLocationHash; + + browser.addPollFn(function() { + if (lastBrowserUrl != browser.getUrl()) { + update(lastBrowserUrl = browser.getUrl()); + updateLastLocation(); + scope.$eval(); + } + }); + + this.$onEval(PRIORITY_FIRST, updateBrowser); + this.$onEval(PRIORITY_LAST, updateBrowser); + + update(lastBrowserUrl); + updateLastLocation(); + + return location; + + // PUBLIC METHODS + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$location#update + * @methodOf angular.service.$location + * + * @description + * Update location object + * Does not immediately update the browser + * Browser is updated at the end of $eval() + * + * @example + * scope.$location.update('http://www.angularjs.org/path#hash?search=x'); + * scope.$location.update({host: 'www.google.com', protocol: 'https'}); + * scope.$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}}); + * + * @param {(string|Object)} href Full href as a string or hash object with properties + */ + function update(href) { + if (isString(href)) { + extend(location, parseHref(href)); + } else { + if (isDefined(href.hash)) { + extend(href, parseHash(href.hash)); + } + + extend(location, href); + + if (isDefined(href.hashPath || href.hashSearch)) { + location.hash = composeHash(location); + } + + location.href = composeHref(location); + } + } + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$location#updateHash + * @methodOf angular.service.$location + * + * @description + * Update location hash part + * @see update() + * + * @example + * 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 = {}; + + if (isString(path)) { + hash.hashPath = path; + if (isDefined(search)) + hash.hashSearch = search; + } else + hash.hashSearch = path; + + update(hash); + } + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$location#toString + * @methodOf angular.service.$location + * + * @description + * Returns string representation - href + */ + function toString() { + updateLocation(); + return location.href; + } + + // INNER METHODS + + /** + * Update location object + * + * User is allowed to change properties, so after property change, + * location object is not in consistent state. + * + * @example + * scope.$location.href = 'http://www.angularjs.org/path#a/b' + * immediately after this call, other properties are still the old ones... + * + * This method checks the changes and update location to the consistent state + */ + function updateLocation() { + if (location.href == lastLocationHref) { + if (location.hash == lastLocationHash) { + location.hash = composeHash(location); + } + location.href = composeHref(location); + } + update(location.href); + } + + /** + * Update information about last location + */ + function updateLastLocation() { + lastLocationHref = location.href; + lastLocationHash = location.hash; + } + + /** + * If location has changed, update the browser + * This method is called at the end of $eval() phase + */ + function updateBrowser() { + updateLocation(); + + if (location.href != lastLocationHref) { + browser.setUrl(lastBrowserUrl = location.href); + updateLastLocation(); + } + } + + /** + * Compose href string from a location object + * + * @param {Object} loc The location object with all properties + * @return {string} Composed href + */ + function composeHref(loc) { + var url = toKeyValue(loc.search); + var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? _null : loc.port); + + return loc.protocol + '://' + loc.host + + (port ? ':' + port : '') + loc.path + + (url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : ''); + } + + /** + * Compose hash string from location object + * + * @param {Object} loc Object with hashPath and hashSearch properties + * @return {string} Hash string + */ + function composeHash(loc) { + var hashSearch = toKeyValue(loc.hashSearch); + //TODO: temporary fix for issue #158 + return escape(loc.hashPath).replace(/%21/gi, '!').replace(/%3A/gi, ':').replace(/%24/gi, '$') + + (hashSearch ? '?' + hashSearch : ''); + } + + /** + * Parse href string into location object + * + * @param {string} href + * @return {Object} The location object + */ + function parseHref(href) { + var loc = {}; + var match = URL_MATCH.exec(href); + + if (match) { + loc.href = href.replace(/#$/, ''); + loc.protocol = match[1]; + loc.host = match[3] || ''; + loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || _null; + loc.path = match[6] || ''; + loc.search = parseKeyValue(match[8]); + loc.hash = match[10] || ''; + + extend(loc, parseHash(loc.hash)); + } + + return loc; + } + + /** + * Parse hash string into object + * + * @param {string} hash + */ + function parseHash(hash) { + var h = {}; + var match = HASH_MATCH.exec(hash); + + if (match) { + h.hash = hash; + h.hashPath = unescape(match[1] || ''); + h.hashSearch = parseKeyValue(match[3]); + } + + return h; + } +}, ['$browser'], EAGER_PUBLISHED); + + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$log + * @requires $window + * + * @description + * Is simple service for logging. Default implementation writes the message + * into the browser's console (if present). + * + * This is useful for debugging. + * + * @example +

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

+ Message: + + + + + + */ +angularServiceInject("$log", 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 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'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$exceptionHandler + * @requires $log + * + * @description + * Any uncaught exception in is delegated to this service. + * The default implementation simply delegates to $log.error which logs it into + * the browser console. + * + * When unit testing it is useful to have uncaught exceptions propagate + * to the test so the test will fail rather than silently log the exception + * to the browser console. For this purpose you can override this service with + * a simple rethrow. + * + * @example + */ +angularServiceInject('$exceptionHandler', function($log){ + return function(e) { + $log.error(e); + }; +}, ['$log'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$hover + * @requires $browser + * @requires $document + * + * @description + * + * @example + */ +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'], EAGER); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$invalidWidgets + * + * @description + * Keeps references to all invalid widgets found during validation. + * Can be queried to find whether there are any invalid widgets currently displayed. + * + * @example + */ +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); + }; + + + /** 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.$onEval(PRIORITY_LAST, 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++; + } + } + }); + + + /** + * Traverses DOM element's (widget's) parents and considers the element to be an orphant 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; +}, [], EAGER_PUBLISHED); + + + +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); + } + } + }); + var match = on.match(new RegExp(regex)); + if (match) { + foreach(params, function(name, index){ + dst[name] = match[index + 1]; + }); + if (dstName) this.$set(dstName, dst); + } + return match ? dst : _null; +} + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$route + * @requires $location + * + * @property {Object} current Name of the current route + * @property {Array.} routes List of configured routes + * + * @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). + * + * $route is typically used in conjunction with ng:include widget. + * + * @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. +

+ + + +Chose: +Moby | +Moby: Ch1 | +Gatsby | +Gatsby: Ch4
+ +
$location={{$location}}
+
$route.current.template={{$route.current.template}}
+
$route.current.params={{$route.current.params}}
+
$route.current.scope.name={{$route.current.scope.name}}
+
+ + */ +angularServiceInject('$route', function(location) { + var routes = {}, + onChange = [], + matcher = switchRouteMatcher, + parentScope = this, + dirty = 0, + $route = { + routes: routes, + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#onChange + * @methodOf angular.service.$route + * + * @param {function()} fn Function that will be called on route change + * + * @description + * Register a handler function that will be called when route changes + */ + onChange: bind(onChange, onChange.push), + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#when + * @methodOf angular.service.$route + * + * @param {string} path Route path (matched against $location.hash) + * @param {Object} params Mapping information to be assigned to `$route.current` on route + * match. + * @returns {Object} route object + * + * @description + * Add new route + */ + when:function (path, params) { + if (angular.isUndefined(path)) return routes; + var route = routes[path]; + if (!route) route = routes[path] = {}; + if (params) angular.extend(route, params); + dirty++; + return route; + } + }; + function updateRoute(){ + var childScope; + $route.current = _null; + angular.foreach(routes, function(routeParams, route) { + if (!childScope) { + var pathParams = matcher(location.hashPath, route); + if (pathParams) { + childScope = angular.scope(parentScope); + $route.current = angular.extend({}, routeParams, { + scope: childScope, + params: angular.extend({}, location.hashSearch, pathParams) + }); + } + } + }); + angular.foreach(onChange, parentScope.$tryEval); + if (childScope) { + childScope.$become($route.current.controller); + } + } + this.$watch(function(){return dirty + location.hash;}, updateRoute); + return $route; +}, ['$location'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr + * @requires $browser + * @requires $xhr.error + * @requires $log + * + * @description + * + * @example + */ +angularServiceInject('$xhr', function($browser, $error, $log){ + var self = this; + return function(method, url, post, callback){ + if (isFunction(post)) { + callback = post; + post = _null; + } + if (post && isObject(post)) { + post = toJson(post); + } + $browser.xhr(method, url, post, function(code, response){ + try { + if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { + response = fromJson(response); + } + if (code == 200) { + callback(code, response); + } else { + $error( + {method: method, url:url, data:post, callback:callback}, + {status: code, body:response}); + } + } catch (e) { + $log.error(e); + } finally { + self.$eval(); + } + }); + }; +}, ['$browser', '$xhr.error', '$log']); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr.error + * @requires $log + * + * @description + * + * @example + */ +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.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, callback) { + if (isFunction(post)) { + callback = post; + post = _null; + } + var currentQueue; + foreach(bulkXHR.urls, function(queue){ + if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) { + currentQueue = queue; + } + }); + if (currentQueue) { + if (!currentQueue.requests) currentQueue.requests = []; + currentQueue.requests.push({method: method, url: url, data:post, callback:callback}); + } else { + $xhr(method, url, post, callback); + } + } + bulkXHR.urls = {}; + bulkXHR.flush = function(callback){ + 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].callback || noop)(response.status, response.response); + } else { + $error(currentRequests[i], response); + } + } catch(e) { + $log.error(e); + } + }); + (callback || noop)(); + }); + scope.$eval(); + } + }); + }; + this.$onEval(PRIORITY_LAST, bulkXHR.flush); + return bulkXHR; +}, ['$xhr', '$xhr.error', '$log']); + + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$defer + * @requires $browser + * @requires $log + * + * @description + * Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function + * into a try/catch block and delegates any exceptions to + * {@link angular.services.$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. + */ +angularServiceInject('$defer', function($browser, $exceptionHandler) { + var scope = this; + + return function(fn) { + $browser.defer(function() { + try { + fn(); + } catch(e) { + $exceptionHandler(e); + } finally { + scope.$eval(); + } + }); + }; +}, ['$browser', '$exceptionHandler']); + + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr.cache + * @requires $xhr + * + * @description + * + * @example + */ +angularServiceInject('$xhr.cache', function($xhr, $defer){ + var inflight = {}, self = this; + function cache(method, url, post, callback, verifyCache){ + if (isFunction(post)) { + callback = post; + post = _null; + } + if (method == 'GET') { + var data, dataCached; + if (dataCached = cache.data[url]) { + $defer(function() { callback(200, copy(dataCached.value)); }); + if (!verifyCache) + return; + } + + if (data = inflight[url]) { + data.callbacks.push(callback); + } else { + inflight[url] = {callbacks: [callback]}; + cache.delegate(method, url, post, function(status, response){ + if (status == 200) + cache.data[url] = { value: response }; + var callbacks = inflight[url].callbacks; + delete inflight[url]; + foreach(callbacks, function(callback){ + try { + (callback||noop)(status, copy(response)); + } catch(e) { + self.$log.error(e); + } + }); + }); + } + + } else { + cache.data = {}; + cache.delegate(method, url, post, callback); + } + } + cache.data = {}; + cache.delegate = $xhr; + return cache; +}, ['$xhr.bulk', '$defer']); + + +/** + * @workInProgress + * @ngdoc function + * @name angular.service.$resource + * @requires $xhr + * + * @description + * Is a factory which creates a resource object which lets you interact with + * RESTful + * server-side data sources. + * Resource object has action methods which provide high-level behaviors without + * the need to interact with the low level $xhr or XMLHttpRequest(). + * + *
+     // 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 `$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. + +
+     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 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();
+     });
+   
+ * + * + * @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. + * @param {Object.=} actions Map of actions available for the resource. + * + * Each resource comes preconfigured with `get`, `save`, `query`, `remove`, and `delete` to + * mimic the RESTful philosophy. + * + * To create your own actions, pass in a map keyed on action names (e.g. `'charge'`) with + * elements consisting of these properties: + * + * - `{string} method`: Request method type. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, + * and [`JSON`](http://en.wikipedia.org/wiki/JSON#JSONP) (also known as JSONP). + * - `{Object=} params`: Set of pre-bound parameters for the action. + * - `{boolean=} isArray`: If true then the returned object for this action is an array, see the + * pre-binding section. + * - `{boolean=} verifyCache`: If true then items returned from cache, are double checked by + * running the query again and updating the resource asynchroniously. + * + * Each service comes preconfigured with the following overridable actions: + *
+ *       { 'get':    {method:'GET'},
+           'save':   {method:'POST'},
+           'query':  {method:'GET', isArray:true},
+           'remove': {method:'DELETE'},
+           'delete': {method:'DELETE'} };
+ *     
+ * + * @returns {Object} A resource "class". + * + * @example + + +
+ + +
+
+

+ + {{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.$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; + + //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); + rootScope.$eval(); + } + })(); + + //at the end of each eval, push cookies + this.$onEval(PRIORITY_LAST, push); + + return cookies; + + + /** + * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. + */ + function push(){ + var name, + browserCookies, + updated; + + //delete any cookies deleted in $cookies + for (name in lastCookies) { + if (isUndefined(cookies[name])) { + $browser.cookies(name, _undefined); + } + } + + //update all cookies updated in $cookies + for(name in cookies) { + if (cookies[name] !== lastCookies[name]) { + $browser.cookies(name, cookies[name]); + updated = true; + } + } + + //verify what was actually stored + if (updated){ + updated = !updated; + 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; + } + + } + + if (updated) { + rootScope.$eval(); + } + } + } +}, ['$browser'], EAGER_PUBLISHED); + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$cookieStore + * @requires $cookies + * + * @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. + * @example + */ +angularServiceInject('$cookieStore', function($store) { + + return { + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$cookieStore#get + * @methodOf angular.service.$cookieStore + * + * @description + * Returns the value of given cookie key + * + * @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 + * + * @description + * Sets a value for given cookie key + * + * @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 + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + */ + remove: function(key) { + delete $store[key]; + } + }; + +}, ['$cookies']); +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:init + * + * @description + * `ng:init` attribute allows the for initialization tasks to be executed + * before the template enters execution mode during bootstrap. + * + * @element ANY + * @param {expression} expression to eval. + * + * @example +
+ {{greeting}} {{person}}! +
+ * + * @scenario + it('should check greeting', function(){ + expect(binding('greeting')).toBe('Hello'); + expect(binding('person')).toBe('World'); + }); + */ +angularDirective("ng:init", function(expression){ + return function(element){ + this.$tryEval(expression, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:controller + * + * @description + * To support the Model-View-Controller design pattern, it is possible + * to assign behavior to a scope through `ng:controller`. The scope is + * the MVC model. The HTML (with data bindings) is the MVC view. + * The `ng:controller` directive specifies the MVC controller class + * + * @element ANY + * @param {expression} expression to eval. + * + * @example + +
+ Name: + [ greet ]
+ Contact: +
    +
  • + + + [ clear + | X ] +
  • +
  • [ add ]
  • +
+
+ * + * @scenario + 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()).toBe('408 555 1212'); + expect(element('.doc-example-live li[ng\\:repeat-index="1"] 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()).toBe('yourname@example.org'); + }); + */ +angularDirective("ng:controller", function(expression){ + this.scope(true); + return function(element){ + var controller = getter(window, expression, true) || getter(this, expression, true); + if (!controller) + throw "Can not find '"+expression+"' controller."; + if (!isFunction(controller)) + throw "Reference '"+expression+"' is not a class."; + this.$become(controller); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:eval + * + * @description + * The `ng:eval` allows you to execute a binding which has side effects + * without displaying the result to the user. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * Notice that `{{` `obj.multiplied = obj.a * obj.b` `}}` has a side effect of assigning + * a value to `obj.multiplied` and displaying the result to the user. Sometimes, + * however, it is desirable to execute a side effect without showing the value to + * the user. In such a case `ng:eval` allows you to execute code without updating + * the display. + * + * @example + * + * * + * = {{obj.multiplied = obj.a * obj.b}}
+ * + * + * obj.divide = {{obj.divide}}
+ * obj.updateCount = {{obj.updateCount}} + * + * @scenario + it('should check eval', function(){ + expect(binding('obj.divide')).toBe('3'); + expect(binding('obj.updateCount')).toBe('2'); + input('obj.a').enter('12'); + expect(binding('obj.divide')).toBe('6'); + expect(binding('obj.updateCount')).toBe('3'); + }); + */ +angularDirective("ng:eval", function(expression){ + return function(element){ + this.$onEval(expression, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:bind + * + * @description + * The `ng:bind` attribute asks to replace the text content of this + * HTML element with the value of the given expression and kept it up to + * date when the expression's value changes. Usually you just write + * {{expression}} and let compile it into + * `` at bootstrap time. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * Try it here: enter text in text box and watch the greeting change. + * @example + * Enter name: .
+ * Hello ! + * + * @scenario + 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'); + }); + */ +angularDirective("ng:bind", function(expression, element){ + element.addClass('ng-binding'); + return function(element) { + var lastValue = noop, lastError = noop; + this.$onEval(function() { + var error, value, html, isHtml, isDomElement, + oldElement = this.hasOwnProperty($$element) ? this.$element : _undefined; + this.$element = element; + value = this.$tryEval(expression, function(e){ + error = formatError(e); + }); + this.$element = oldElement; + // If we are HTML than save the raw HTML data so that we don't + // recompute sanitization since it 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); + } + 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); + } + } + }, element); + }; +}); + +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(element){ + var error, value = this.$tryEval(exp, function(e){ + error = toJson(e); + }); + elementError(element, NG_EXCEPTION, error); + return error ? error : value; + } : function() { + return text; + }); + }); + bindTemplateCache[template] = fn = function(element){ + var parts = [], self = this, + oldElement = this.hasOwnProperty($$element) ? self.$element : _undefined; + self.$element = element; + for ( var i = 0; i < bindings.length; i++) { + var value = bindings[i].call(self, element); + if (isElement(value)) + value = ''; + else if (isObject(value)) + value = toJson(value, true); + parts.push(value); + } + self.$element = oldElement; + return parts.join(''); + }; + } + return fn; +} + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:bind-template + * + * @description + * The `ng:bind-template` attribute specifies that the element + * text should be replaced with the template in ng:bind-template. + * Unlike ng:bind the ng:bind-template can contain multiple `{{` `}}` + * expressions. (This is required since some HTML elements + * can not have SPAN elements such as TITLE, or OPTION to name a few. + * + * @element ANY + * @param {string} template of form + * {{ expression }} to eval. + * + * @exampleDescription + * Try it here: enter text in text box and watch the greeting change. + * @example + Salutation:
+ Name:
+

+ * 
+ * @scenario
+   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');
+     using('.doc-example-live').input('name').enter('user');
+     expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
+       toBe('Greetings user!');
+   });
+ */
+angularDirective("ng:bind-template", function(expression, element){
+  element.addClass('ng-binding');
+  var templateFn = compileBindTemplate(expression);
+  return function(element) {
+    var lastValue;
+    this.$onEval(function() {
+      var value = templateFn.call(this, element);
+      if (value != lastValue) {
+        element.text(value);
+        lastValue = value;
+      }
+    }, element);
+  };
+});
+
+var REMOVE_ATTRIBUTES = {
+  'disabled':'disabled',
+  'readonly':'readOnly',
+  'checked':'checked',
+  'selected':'selected'
+};
+/**
+ * @workInProgress
+ * @ngdoc directive
+ * @name angular.directive.ng:bind-attr
+ *
+ * @description
+ * The `ng:bind-attr` attribute specifies that the element attributes 
+ * which should be replaced by the expression in it. Unlike `ng:bind` 
+ * the `ng:bind-attr` contains a JSON key value pairs representing 
+ * which attributes need to be changed. You don’t usually write the 
+ * `ng:bind-attr` in the HTML since embedding 
+ * {{expression}} into the 
+ * attribute directly is the preferred way. The attributes get
+ * translated into `` at
+ * bootstrap time.
+ * 
+ * This HTML snippet is preferred way of working with `ng:bind-attr`
+ * 
+ *   Google
+ * 
+ * + * The above gets translated to bellow during bootstrap time. + *
+ *   Google
+ * 
+ * + * @element ANY + * @param {string} attribute_json a JSON key-value pairs representing + * the attributes to replace. Each key matches the attribute + * which needs to be replaced. Each value is a text template of + * the attribute with embedded + * {{expression}}s. Any number of + * key-value pairs can be specified. + * + * @exampleDescription + * Try it here: enter text in text box and click Google. + * @example + Google for: + + Google + * + * @scenario + 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'); + expect(using('.doc-example-live').element('a').attr('href')). + toBe('http://www.google.com/search?q=google'); + }); + */ +angularDirective("ng:bind-attr", function(expression){ + return function(element){ + var lastValue = {}; + var updateFn = element.data($$update) || noop; + this.$onEval(function(){ + var values = this.$eval(expression), + dirty = noop; + for(var key in values) { + var value = compileBindTemplate(values[key]).call(this, element), + specialName = REMOVE_ATTRIBUTES[lowercase(key)]; + 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); + } + dirty = updateFn; + } + } + dirty(); + }, element); + }; +}); + + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:click + * + * @description + * The ng:click allows you to specify custom behavior when + * element is clicked. + * + * @element ANY + * @param {expression} expression to eval upon click. + * + * @example + + count: {{count}} + * @scenario + it('should check ng:click', function(){ + expect(binding('count')).toBe('0'); + element('.doc-example-live :button').click(); + expect(binding('count')).toBe('1'); + }); + */ +/* + * A directive that allows creation of custom onclick handlers that are defined as angular + * expressions and are compiled and executed within the current scope. + * + * Events that are handled via these handler are always configured not to propagate further. + * + * TODO: maybe we should consider allowing users to control event propagation in the future. + */ +angularDirective("ng:click", function(expression, element){ + return function(element){ + var self = this; + element.bind('click', function(event){ + self.$tryEval(expression, element); + self.$root.$eval(); + event.stopPropagation(); + }); + }; +}); + + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:submit + * + * @description + * + * @element form + * @param {expression} expression to eval. + * + * @exampleDescription + * @example + *
+ * Enter text and hit enter: + * + *
+ *
list={{list}}
+ * @scenario + it('should check ng:submit', function(){ + expect(binding('list')).toBe('list=[]'); + element('.doc-example-live form input').click(); + this.addFutureAction('submit from', function($window, $document, done) { + $window.angular.element( + $document.elements('.doc-example-live form')). + trigger('submit'); + done(); + }); + expect(binding('list')).toBe('list=["hello"]'); + }); + */ +/** + * Enables binding angular expressions to onsubmit events. + * + * Additionally it prevents the default action (which for form means sending the request to the + * server and reloading the current page). + */ +angularDirective("ng:submit", function(expression, element) { + return function(element) { + var self = this; + element.bind('submit', function(event) { + self.$tryEval(expression, element); + self.$root.$eval(); + event.preventDefault(); + }); + }; +}); + + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:watch + * + * @description + * The `ng:watch` allows you watch a variable and then execute + * an evaluation on variable change. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * Notice that the counter is incremented + * every time you change the text. + * @example +
+
+ Change counter: {{counter}} Name: {{name}} +
+ * @scenario + it('should check ng:watch', function(){ + expect(using('.doc-example-live').binding('counter')).toBe('2'); + using('.doc-example-live').input('name').enter('abc'); + expect(using('.doc-example-live').binding('counter')).toBe('3'); + }); + */ +//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular) +angularDirective("ng:watch", function(expression, element){ + return function(element){ + var self = this; + parser(expression).watch()({ + addListener:function(watch, exp){ + self.$watch(watch, function(){ + return exp(self); + }, element); + } + }); + }; +}); + +function ngClass(selector) { + return function(expression, element){ + var existing = element[0].className + ' '; + return function(element){ + this.$onEval(function(){ + if (selector(this.$index)) { + var value = this.$eval(expression); + if (isArray(value)) value = value.join(' '); + element[0].className = trim(existing + value); + } + }, element); + }; + }; +} + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:class + * + * @description + * The `ng:class` allows you to set CSS class on HTML element + * conditionally. + * + * @element ANY + * @param {expression} expression to eval. + * + * @exampleDescription + * @example + + +
+ Sample Text      + * + * @scenario + it('should check ng:class', function(){ + expect(element('.doc-example-live span').attr('className')).not(). + toMatch(/ng-input-indicator-wait/); + + using('.doc-example-live').element(':button:first').click(); + + expect(element('.doc-example-live span').attr('className')). + toMatch(/ng-input-indicator-wait/); + + using('.doc-example-live').element(':button:last').click(); + + expect(element('.doc-example-live span').attr('className')).not(). + toMatch(/ng-input-indicator-wait/); + }); + */ +angularDirective("ng:class", ngClass(function(){return true;})); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:class-odd + * + * @description + * The `ng:class-odd` and `ng:class-even` works exactly as + * `ng:class`, except it works in conjunction with `ng:repeat` + * and takes affect only on odd (even) rows. + * + * @element ANY + * @param {expression} expression to eval. Must be inside + * `ng:repeat`. + + * + * @exampleDescription + * @example +
    +
  1. + + {{name}}       + +
  2. +
+ * + * @scenario + 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/); + }); + */ +angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;})); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:class-even + * + * @description + * The `ng:class-odd` and `ng:class-even` works exactly as + * `ng:class`, except it works in conjunction with `ng:repeat` + * and takes affect only on odd (even) rows. + * + * @element ANY + * @param {expression} expression to eval. Must be inside + * `ng:repeat`. + + * + * @exampleDescription + * @example +
    +
  1. + + {{name}}       + +
  2. +
+ * + * @scenario + 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/); + }); + */ +angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;})); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:show + * + * @description + * The `ng:show` and `ng:hide` allows you to show or hide a portion + * of the HTML conditionally. + * + * @element ANY + * @param {expression} expression if truthy then the element is + * shown or hidden respectively. + * + * @exampleDescription + * @example + Click me:
+ Show: I show up when you checkbox is checked?
+ Hide: I hide when you checkbox is checked? + * + * @scenario + 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); + + input('checked').check(); + + expect(element('.doc-example-live span:first:visible').count()).toEqual(1); + expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); + }); + */ +angularDirective("ng:show", function(expression, element){ + return function(element){ + this.$onEval(function(){ + element.css($display, toBoolean(this.$eval(expression)) ? '' : $none); + }, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:hide + * + * @description + * The `ng:show` and `ng:hide` allows you to show or hide a portion + * of the HTML conditionally. + * + * @element ANY + * @param {expression} expression if truthy then the element is + * shown or hidden respectively. + * + * @exampleDescription + * @example + Click me:
+ Show: I show up when you checkbox is checked?
+ Hide: I hide when you checkbox is checked? + * + * @scenario + 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); + + input('checked').check(); + + expect(element('.doc-example-live span:first:visible').count()).toEqual(1); + expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); + }); + */ +angularDirective("ng:hide", function(expression, element){ + return function(element){ + this.$onEval(function(){ + element.css($display, toBoolean(this.$eval(expression)) ? $none : ''); + }, element); + }; +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:style + * + * @description + * The ng:style allows you to set CSS style on an HTML element conditionally. + * + * @element ANY + * @param {expression} expression which evals to an object whes key's are + * CSS style names and values are coresponding values for those + * CSS keys. + * + * @exampleDescription + * @example + + +
+ Sample Text +
myStyle={{myStyle}}
+ * + * @scenario + 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'); + 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.$onEval(function(){ + var style = this.$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]; + } + element.css(mergedStyle); + }, element); + }; +}); + +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), + self = this; + if (hasBindings(bindings)) { + if (isLeafNode(parentElement[0])) { + parentElement.attr('ng:bind-template', text); + } else { + var cursor = textNode, newElement; + foreach(parseBindings(text), function(text){ + var exp = binding(text); + if (exp) { + newElement = self.element('span'); + newElement.attr('ng:bind', exp); + } else { + newElement = self.text(text); + } + if (msie && text.charAt(0) == ' ') { + newElement = jqLite(' '); + var nbsp = newElement.html(); + newElement.text(text.substr(1)); + newElement.html(nbsp + newElement.html()); + } + cursor.after(newElement); + cursor = newElement; + }); + textNode.remove(); + } + } +}); + +// TODO: this should be widget not a markup +angularTextMarkup('OPTION', function(text, textNode, parentElement){ + if (nodeName(parentElement) == "OPTION") { + var select = document.createElement('select'); + select.insertBefore(parentElement[0].cloneNode(true), _null); + if (!select.innerHTML.match(/.*<\/\s*option\s*>/gi)) { + parentElement.attr('value', text); + } + } +}); + +/** + * @workInProgress + * @ngdoc directive + * @name angular.directive.ng:href + * + * @description + * Using markup like {{hash}} in an href attribute makes + * the page open to a wrong URL, ff the user clicks that link before + * angular has a chance to replace the {{hash}} with actual URL, the + * link will be broken and will most likely return a 404 error. + * The `ng:href` solves this problem by placing the `href` 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. + */ + +/** + * @workInProgress + * @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. + */ + +var NG_BIND_ATTR = 'ng:bind-attr'; +var SPECIAL_ATTRS = {'ng:src': 'src', 'ng:href': 'href'}; +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)) { + 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 widget + * @name angular.widget.HTML + * + * @description + * The most common widgets you will use will be in the from 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. + * + * @usageContent + * see example below for usage + * + * + * + {{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}} + + + + * @scenario + * 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"]'); + * }); + */ + +function modelAccessor(scope, element) { + var expr = element.attr('name'); + if (!expr) throw "Required field 'name' not found."; + return { + get: function() { + return scope.$eval(expr); + }, + set: function(value) { + if (value !== _undefined) { + return scope.$tryEval(expr + '=' + toJson(value), element); + } + } + }; +} + +function modelFormattedAccessor(scope, element) { + var accessor = modelAccessor(scope, element), + formatterName = element.attr('ng:format') || NOOP, + formatter = angularFormatter(formatterName); + if (!formatter) throw "Formatter named '" + formatterName + "' not found."; + return { + get: function() { + return formatter.format(accessor.get()); + }, + set: function(value) { + return accessor.set(formatter.parse(value)); + } + }; +} + +function compileValidator(expr) { + return parser(expr).validator()(); +} + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.@ng:validate + * + * @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. + * + * @element INPUT + * @css ng-validation-error + * + * @exampleDescription + * This example shows how the input element becomes red when it contains invalid input. Correct + * the input to make the error disappear. + * + * @example + I don't validate: +
+ + I need an integer or nothing: +
+ * + * @scenario + 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 + * + * @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. + * + * @element INPUT + * @css ng-validation-error + * + * @exampleDescription + * This example shows how the input element becomes red when it contains invalid input. Correct + * the input to make the error disappear. + * + * @example + I cannot be blank:
+ * + * @scenario + 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. + * + * @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter} + * to be used. + * + * @element INPUT + * + * @exampleDescription + * This example shows how the user input is converted from a string and internally represented as an + * array. + * + * @example + Enter a comma separated list of items: + +
list={{list}}
+ * + * @scenario + 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 = angularFormatter(formatterName), + format, parse, lastError, required, + invalidWidgets = scope.$invalidWidgets || {markValid:noop, markInvalid:noop}; + if (!validator) throw "Validator named '" + validatorName + "' not found."; + if (!formatter) throw "Formatter named '" + formatterName + "' not found."; + format = formatter.format; + parse = formatter.parse; + if (requiredExpr) { + scope.$watch(requiredExpr, function(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(element.val()); + validate(); + return value; + } catch (e) { + lastError = e; + elementError(element, NG_VALIDATION_ERROR, e); + } + }, + set: function(value) { + var oldValue = element.val(), + newValue = format(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 options = element[0].options; + return { + get: function(){ + var values = []; + foreach(options, function(option){ + if (option.selected) values.push(option.value); + }); + return values; + }, + set: function(values){ + var keys = {}; + foreach(values, function(value){ keys[value] = true; }); + foreach(options, function(option){ + option.selected = keys[option.value]; + }); + } + }; +} + +function noopAccessor() { return { get: noop, set: noop }; } + +var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue()), + buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), + INPUT_TYPE = { + 'text': textWidget, + 'textarea': textWidget, + 'hidden': textWidget, + 'password': textWidget, + 'button': buttonWidget, + 'submit': buttonWidget, + 'reset': buttonWidget, + 'image': buttonWidget, + 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)), + 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), + 'select-one': inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(_null)), + 'select-multiple': inputWidget('change', modelFormattedAccessor, 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 + * + * @description + * The directive executes an expression whenever the input widget changes. + * + * @element INPUT + * @param {expression} expression to execute. + * + * @exampleDescription + * @example +
+ + changeCount {{textCount}}
+ + changeCount {{checkboxCount}}
+ * + * @scenario + it('should check ng:change', function(){ + expect(binding('textCount')).toBe('0'); + expect(binding('checkboxCount')).toBe('0'); + + using('.doc-example-live').input('text').enter('abc'); + expect(binding('textCount')).toBe('1'); + expect(binding('checkboxCount')).toBe('0'); + + + using('.doc-example-live').input('checkbox').check(); + expect(binding('textCount')).toBe('1'); + expect(binding('checkboxCount')).toBe('1'); + }); + */ +function inputWidget(events, modelAccessor, viewAccessor, initFn) { + return function(element) { + var scope = this, + model = modelAccessor(scope, element), + view = viewAccessor(scope, element), + action = element.attr('ng:change') || '', + lastValue; + initFn.call(scope, model, view, element); + this.$eval(element.attr('ng:init')||''); + // Don't register a handler if we are a button (noopAccessor) and there is no action + if (action || modelAccessor !== noopAccessor) { + element.bind(events, function (){ + model.set(view.get()); + lastValue = model.get(); + scope.$tryEval(action, element); + scope.$root.$eval(); + }); + } + scope.$watch(model.get, function(value){ + if (lastValue !== value) { + view.set(lastValue = value); + } + }); + }; +} + +function inputWidgetSelector(element){ + this.directives(true); + return INPUT_TYPE[lowercase(element[0].type)] || noop; +} + +angularWidget('input', inputWidgetSelector); +angularWidget('textarea', inputWidgetSelector); +angularWidget('button', inputWidgetSelector); +angularWidget('select', function(element){ + this.descend(true); + return inputWidgetSelector.call(this, element); +}); + + +/* + * Consider this: + * + * + * The issue is that the select gets evaluated before option is unrolled. + * This means that the selection is undefined, but the browser + * default behavior is to show the top selection in the list. + * To fix that we register a $update function on the select element + * and the option creation then calls the $update function when it is + * unrolled. The $update function then calls this update function, which + * then tries to determine if the model is unassigned, and if so it tries to + * chose one of the options from the list. + */ +angularWidget('option', function(){ + this.descend(true); + this.directives(true); + return function(element) { + var select = element.parent(); + var scope = retrieveScope(select); + var model = modelFormattedAccessor(scope, select); + var view = valueAccessor(scope, select); + var option = element; + var lastValue = option.attr($value); + var lastSelected = option.attr('ng-' + $selected); + element.data($$update, function(){ + var value = option.attr($value); + var selected = option.attr('ng-' + $selected); + var modelValue = model.get(); + if (lastSelected != selected || lastValue != value) { + lastSelected = selected; + lastValue = value; + if (selected || modelValue == _null || modelValue == _undefined) + model.set(value); + if (value == modelValue) { + view.set(lastValue); + } + } + }); + }; +}); + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.ng:include + * + * @description + * Include external HTML fragment. + * + * Keep in mind that Same Origin Policy applies to included resources + * (e.g. ng:include won't work for file:// access). + * + * @param {string} src expression evaluating to URL. + * @param {Scope=} [scope=new_child_scope] expression evaluating to angular.scope + * @param {string=} onload Expression to evaluate when a new partial is loaded. + * + * @example + * + * url =
{{url}} + *
+ * + * + * @scenario + * it('should load date filter', function(){ + * expect(element('.doc-example ng\\:include').text()).toMatch(/angular\.filter\.date/); + * }); + * it('should change to hmtl filter', function(){ + * select('url').option('angular.filter.html.html'); + * expect(element('.doc-example ng\\:include').text()).toMatch(/angular\.filter\.html/); + * }); + * it('should change to blank', function(){ + * select('url').option('(blank)'); + * expect(element('.doc-example ng\\:include').text()).toEqual(''); + * }); + */ +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 + if (element[0]['ng:compiled']) { + this.descend(true); + this.directives(true); + } else { + element[0]['ng:compiled'] = true; + return extend(function(xhr, element){ + var scope = this, childScope; + var changeCounter = 0; + var preventRecursion = false; + function incrementChange(){ changeCounter++;} + this.$watch(srcExp, incrementChange); + this.$watch(scopeExp, incrementChange); + scope.$onEval(function(){ + if (childScope && !preventRecursion) { + preventRecursion = true; + try { + childScope.$eval(); + } finally { + preventRecursion = false; + } + } + }); + this.$watch(function(){return changeCounter;}, function(){ + var src = this.$eval(srcExp), + useScope = this.$eval(scopeExp); + + if (src) { + xhr('GET', src, function(code, response){ + element.html(response); + childScope = useScope || createScope(scope); + compiler.compile(element)(element, childScope); + childScope.$init(); + scope.$eval(onloadExp); + }); + } else { + childScope = null; + element.html(''); + } + }); + }, {$inject:['$xhr.cache']}); + } +}); + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.ng:switch + * + * @description + * 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 + + switch={{switch}} + + +
Settings Div
+ Home Span + default +
+ + * + * @scenario + * it('should start in settings', function(){ + * expect(element('.doc-example ng\\:switch').text()).toEqual('Settings Div'); + * }); + * it('should change to home', function(){ + * select('switch').option('home'); + * expect(element('.doc-example ng\\:switch').text()).toEqual('Home Span'); + * }); + * it('should select deafault', function(){ + * select('switch').option('other'); + * expect(element('.doc-example ng\\:switch').text()).toEqual('default'); + * }); + */ +var ngSwitch = angularWidget('ng:switch', function (element){ + var compiler = this, + watchExpr = element.attr("on"), + usingExpr = (element.attr("using") || 'equals'), + usingExprParams = usingExpr.split(":"), + usingFn = ngSwitch[usingExprParams.shift()], + changeExpr = element.attr('change') || '', + cases = []; + if (!usingFn) throw "Using expression '" + usingExpr + "' unknown."; + if (!watchExpr) throw "Missing 'on' attribute."; + eachNode(element, function(caseElement){ + var when = caseElement.attr('ng:switch-when'); + var switchCase = { + change: changeExpr, + element: caseElement, + template: compiler.compile(caseElement) + }; + if (isString(when)) { + switchCase.when = function(scope, value){ + var args = [value, when]; + foreach(usingExprParams, function(arg){ + args.push(arg); + }); + return usingFn.apply(scope, args); + }; + cases.unshift(switchCase); + } else if (isString(caseElement.attr('ng:switch-default'))) { + switchCase.when = valueFn(true); + cases.push(switchCase); + } + }); + + // this needs to be here for IE + foreach(cases, function(_case){ + _case.element.remove(); + }); + + element.html(''); + return function(element){ + var scope = this, childScope; + this.$watch(watchExpr, function(value){ + var found = false; + element.html(''); + childScope = createScope(scope); + foreach(cases, function(switchCase){ + if (!found && switchCase.when(childScope, value)) { + found = true; + var caseElement = quickClone(switchCase.element); + element.append(caseElement); + childScope.$tryEval(switchCase.change, element); + switchCase.template(caseElement, childScope); + childScope.$init(); + } + }); + }); + scope.$onEval(function(){ + if (childScope) childScope.$eval(); + }); + }; +}, { + equals: function(on, when) { + return ''+on == when; + }, + route: switchRouteMatcher +}); + + +/* + * Modifies the default behavior of html A tag, so that the default action is prevented when href + * attribute is empty. + * + * 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) { + if (element.attr('href') === '') { + element.bind('click', function(event){ + event.preventDefault(); + }); + } + }; +}); + + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.@ng:repeat + * + * @description + * `ng:repeat` instantiates a template once per item from a collection. The collection is enumerated + * with `ng:repeat-index` attribute starting from 0. 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. + * + * There are special properties exposed on the local scope of each template instance: + * + * * `$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'` or `'last'`. + * + * NOTE: `ng:repeat` looks like a directive, but 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}`. + * + * @exampleDescription + * This example initializes the scope to a list of names and + * than uses `ng:repeat` to display every person. + * @example +
+ I have {{friends.length}} friends. They are: +
    +
  • + [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +
  • +
+
+ * @scenario + 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"]); + }); + */ +angularWidget("@ng:repeat", function(expression, element){ + element.removeAttr('ng:repeat'); + element.replaceWith(this.comment("ng:repeat: " + expression)); + var template = this.compile(element); + return function(reference){ + 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 children = [], currentScope = this; + this.$onEval(function(){ + var index = 0, + childCount = children.length, + lastElement = reference, + collection = this.$tryEval(rhs, reference), + is_array = isArray(collection), + collectionLength = 0, + childScope, + key; + + if (is_array) { + collectionLength = collection.length; + } else { + for (key in collection) + if (collection.hasOwnProperty(key)) + collectionLength++; + } + + for (key in collection) { + if (!is_array || collection.hasOwnProperty(key)) { + if (index < childCount) { + // reuse existing child + childScope = children[index]; + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + } else { + // grow children + childScope = template(quickClone(element), createScope(currentScope)); + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + lastElement.after(childScope.$element); + childScope.$index = index; + childScope.$position = index == 0 ? + 'first' : + (index == collectionLength - 1 ? 'last' : 'middle'); + childScope.$element.attr('ng:repeat-index', index); + childScope.$init(); + children.push(childScope); + } + childScope.$eval(); + lastElement = childScope.$element; + index ++; + } + } + // shrink children + while(children.length > index) { + children.pop().$element.remove(); + } + }, reference); + }; +}); + + +/** + * @workInProgress + * @ngdoc widget + * @name angular.widget.@ng:non-bindable + * + * @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. + * + * NOTE: `ng:non-bindable` looks like a directive, but is actually an attribute widget. + * + * @element ANY + * + * @exampleDescription + * In this example there are two location where a siple binding (`{{}}`) is present, but the one + * wrapped in `ng:non-bindable` is left alone. + * + * @example +
Normal: {{1 + 2}}
+
Ignored: {{1 + 2}}
+ * + * @scenario + 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); +var browserSingleton; +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$browser + * @requires $log + * + * @description + * Represents the browser. + */ +angularService('$browser', function($log){ + if (!browserSingleton) { + browserSingleton = new Browser( + window.location, + jqLite(window.document), + jqLite(window.document.getElementsByTagName('head')[0]), + XHR, + $log, + window.setTimeout); + browserSingleton.startPoller(50, function(delay, fn){setTimeout(delay,fn);}); + browserSingleton.bind(); + } + return browserSingleton; +}, {inject:['$log']}); + +extend(angular, { + 'element': jqLite, + '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 +}); + + +/** + * Setup file for the Scenario. + * Must be first in the compilation/bootstrap list. + */ + +// Public namespace +angular.scenario = angular.scenario || {}; + +/** + * Defines a new output format. + * + * @param {string} name the name of the new output format + * @param {Function} fn function(context, runner) that generates the output + */ +angular.scenario.output = angular.scenario.output || function(name, fn) { + angular.scenario.output[name] = fn; +}; + +/** + * Defines a new DSL statement. If your factory function returns a Future + * it's returned, otherwise the result is assumed to be a map of functions + * for chaining. Chained functions are subject to the same rules. + * + * Note: All functions on the chain are bound to the chain scope so values + * set on "this" in your statement function are available in the chained + * functions. + * + * @param {string} name The name of the statement + * @param {Function} fn Factory function(), return a function for + * the statement. + */ +angular.scenario.dsl = angular.scenario.dsl || function(name, fn) { + angular.scenario.dsl[name] = function() { + function executeStatement(statement, args) { + var result = statement.apply(this, args); + if (angular.isFunction(result) || result instanceof angular.scenario.Future) + return result; + var self = this; + var chain = angular.extend({}, result); + angular.foreach(chain, function(value, name) { + if (angular.isFunction(value)) { + chain[name] = function() { + return executeStatement.call(self, value, arguments); + }; + } else { + chain[name] = value; + } + }); + return chain; + } + var statement = fn.apply(this, arguments); + return function() { + return executeStatement.call(this, statement, arguments); + }; + }; +}; + +/** + * Defines a new matcher for use with the expects() statement. The value + * this.actual (like in Jasmine) is available in your matcher to compare + * against. Your function should return a boolean. The future is automatically + * created for you. + * + * @param {string} name The name of the matcher + * @param {Function} fn The matching function(expected). + */ +angular.scenario.matcher = angular.scenario.matcher || function(name, fn) { + angular.scenario.matcher[name] = function(expected) { + var prefix = 'expect ' + this.future.name + ' '; + if (this.inverse) { + prefix += 'not '; + } + var self = this; + this.addFuture(prefix + name + ' ' + angular.toJson(expected), + function(done) { + var error; + self.actual = self.future.value; + if ((self.inverse && fn.call(self, expected)) || + (!self.inverse && !fn.call(self, expected))) { + error = 'expected ' + angular.toJson(expected) + + ' but was ' + angular.toJson(self.actual); + } + done(error); + }); + }; +}; + +/** + * Initialization function for the scenario runner. + * + * @param {angular.scenario.Runner} $scenario The runner to setup + * @param {Object} config Config options + */ +function angularScenarioInit($scenario, config) { + var href = window.location.href; + var body = _jQuery(document.body); + var output = []; + + if (config.scenario_output) { + output = config.scenario_output.split(','); + } + + angular.foreach(angular.scenario.output, function(fn, name) { + if (!output.length || indexOf(output,name) != -1) { + var context = body.append('
').find('div:last'); + context.attr('id', name); + fn.call({}, context, $scenario); + } + }); + + if (!/^http/.test(href) && !/^https/.test(href)) { + body.append('

'); + body.find('#system-error').text( + 'Scenario runner must be run using http or https. The protocol ' + + href.split(':')[0] + ':// is not supported.' + ); + return; + } + + var appFrame = body.append('
').find('#application'); + var application = new angular.scenario.Application(appFrame); + + $scenario.on('RunnerEnd', function() { + appFrame.css('display', 'none'); + appFrame.find('iframe').attr('src', 'about:blank'); + }); + + $scenario.on('RunnerError', function(error) { + if (window.console) { + console.log(formatException(error)); + } else { + // Do something for IE + alert(error); + } + }); + + $scenario.run(application); +} + +/** + * Iterates through list with iterator function that must call the + * continueFunction to continute iterating. + * + * @param {Array} list list to iterate over + * @param {Function} iterator Callback function(value, continueFunction) + * @param {Function} done Callback function(error, result) called when + * iteration finishes or an error occurs. + */ +function asyncForEach(list, iterator, done) { + var i = 0; + function loop(error, index) { + if (index && index > i) { + i = index; + } + if (error || i >= list.length) { + done(error); + } else { + try { + iterator(list[i++], loop); + } catch (e) { + done(e); + } + } + } + loop(); +} + +/** + * Formats an exception into a string with the stack trace, but limits + * to a specific line length. + * + * @param {Object} error The exception to format, can be anything throwable + * @param {Number} maxStackLines Optional. max lines of the stack trace to include + * default is 5. + */ +function formatException(error, maxStackLines) { + maxStackLines = maxStackLines || 5; + var message = error.toString(); + if (error.stack) { + var stack = error.stack.split('\n'); + if (stack[0].indexOf(message) === -1) { + maxStackLines++; + stack.unshift(error.message); + } + message = stack.slice(0, maxStackLines).join('\n'); + } + return message; +} + +/** + * Returns a function that gets the file name and line number from a + * location in the stack if available based on the call site. + * + * Note: this returns another function because accessing .stack is very + * expensive in Chrome. + * + * @param {Number} offset Number of stack lines to skip + */ +function callerFile(offset) { + var error = new Error(); + + return function() { + var line = (error.stack || '').split('\n')[offset]; + + // Clean up the stack trace line + if (line) { + if (line.indexOf('@') !== -1) { + // Firefox + line = line.substring(line.indexOf('@')+1); + } else { + // Chrome + line = line.substring(line.indexOf('(')+1).replace(')', ''); + } + } + + return line || ''; + }; +} + +/** + * Triggers a browser event. Attempts to choose the right event if one is + * not specified. + * + * @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement + * @param {string} Optional event type. + */ +function browserTrigger(element, type) { + if (element && !element.nodeName) element = element[0]; + if (!element) return; + if (!type) { + type = { + 'text': 'change', + 'textarea': 'change', + 'hidden': 'change', + 'password': 'change', + 'button': 'click', + 'submit': 'click', + 'reset': 'click', + 'image': 'click', + 'checkbox': 'click', + 'radio': 'click', + 'select-one': 'change', + 'select-multiple': 'change' + }[element.type] || 'click'; + } + if (lowercase(nodeName(element)) == 'option') { + element.parentNode.value = element.value; + element = element.parentNode; + type = 'change'; + } + if (msie) { + switch(element.type) { + case 'radio': + case 'checkbox': + element.checked = !element.checked; + break; + } + element.fireEvent('on' + type); + if (lowercase(element.type) == 'submit') { + while(element) { + if (lowercase(element.nodeName) == 'form') { + element.fireEvent('onsubmit'); + break; + } + element = element.parentNode; + } + } + } else { + var evnt = document.createEvent('MouseEvents'); + evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element); + element.dispatchEvent(evnt); + } +} + +/** + * Don't use the jQuery trigger method since it works incorrectly. + * + * jQuery notifies listeners and then changes the state of a checkbox and + * does not create a real browser event. A real click changes the state of + * the checkbox and then notifies listeners. + * + * To work around this we instead use our own handler that fires a real event. + */ +(function(fn){ + var parentTrigger = fn.trigger; + fn.trigger = function(type) { + if (/(click|change|keyup)/.test(type)) { + return this.each(function(index, node) { + browserTrigger(node, type); + }); + } + return parentTrigger.apply(this, arguments); + }; +})(_jQuery.fn); + +/** + * Finds all bindings with the substring match of name and returns an + * array of their values. + * + * @param {string} name The name to match + * @return {Array.} String of binding values + */ +_jQuery.fn.bindings = function(name) { + function contains(text, value) { + return value instanceof RegExp ? + value.test(text) : + text && text.indexOf(value) >= 0; + } + var result = []; + this.find('.ng-binding:visible').each(function() { + var element = new _jQuery(this); + if (!angular.isDefined(name) || + contains(element.attr('ng:bind'), name) || + contains(element.attr('ng:bind-template'), name)) { + if (element.is('input, textarea')) { + result.push(element.val()); + } else { + result.push(element.html()); + } + } + }); + return result; +}; +/** + * Represents the application currently being tested and abstracts usage + * of iframes or separate windows. + * + * @param {Object} context jQuery wrapper around HTML context. + */ +angular.scenario.Application = function(context) { + this.context = context; + context.append( + '

Current URL: None

' + + '
' + ); +}; + +/** + * Gets the jQuery collection of frames. Don't use this directly because + * frames may go stale. + * + * @private + * @return {Object} jQuery collection + */ +angular.scenario.Application.prototype.getFrame_ = function() { + return this.context.find('#test-frames iframe:last'); +}; + +/** + * Gets the window of the test runner frame. Always favor executeAction() + * instead of this method since it prevents you from getting a stale window. + * + * @private + * @return {Object} the window of the frame + */ +angular.scenario.Application.prototype.getWindow_ = function() { + var contentWindow = this.getFrame_().attr('contentWindow'); + if (!contentWindow) + throw 'Frame window is not accessible.'; + return contentWindow; +}; + +/** + * Checks that a URL would return a 2xx success status code. Callback is called + * with no arguments on success, or with an error on failure. + * + * Warning: This requires the server to be able to respond to HEAD requests + * and not modify the state of your application. + * + * @param {string} url Url to check + * @param {Function} callback function(error) that is called with result. + */ +angular.scenario.Application.prototype.checkUrlStatus_ = function(url, callback) { + var self = this; + _jQuery.ajax({ + url: url, + type: 'HEAD', + complete: function(request) { + if (request.status < 200 || request.status >= 300) { + if (!request.status) { + callback.call(self, 'Sandbox Error: Cannot access ' + url); + } else { + callback.call(self, request.status + ' ' + request.statusText); + } + } else { + callback.call(self); + } + } + }); +}; + +/** + * Changes the location of the frame. + * + * @param {string} url The URL. If it begins with a # then only the + * hash of the page is changed. + * @param {Function} loadFn function($window, $document) Called when frame loads. + * @param {Function} errorFn function(error) Called if any error when loading. + */ +angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorFn) { + var self = this; + var frame = this.getFrame_(); + //TODO(esprehn): Refactor to use rethrow() + errorFn = errorFn || function(e) { throw e; }; + if (url === 'about:blank') { + errorFn('Sandbox Error: Navigating to about:blank is not allowed.'); + } else if (url.charAt(0) === '#') { + url = frame.attr('src').split('#')[0] + url; + frame.attr('src', url); + this.executeAction(loadFn); + } else { + frame.css('display', 'none').attr('src', 'about:blank'); + this.checkUrlStatus_(url, function(error) { + if (error) { + return errorFn(error); + } + self.context.find('#test-frames').append('