Compare commits
508 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45879a8c5a | |||
| fe600c03da | |||
| 02c04690da | |||
| c3d5cc50cc | |||
| 6f85b3b699 | |||
| 70b801f326 | |||
| 3cf4eed6b5 | |||
| 376804676e | |||
| e3cb64b9e4 | |||
| fb2d3fa15b | |||
| 0517799a05 | |||
| 627180fb71 | |||
| 841feb0a05 | |||
| b85c70a958 | |||
| b2ed68d723 | |||
| 5b7e4b4394 | |||
| 77f26bddf7 | |||
| c8c3de490f | |||
| 3a2aea78ad | |||
| b7bb797e5c | |||
| 059d4f6118 | |||
| 1e90d03077 | |||
| b7d396b8b6 | |||
| 71d2c147a8 | |||
| 4492db3077 | |||
| 09a638559a | |||
| a8dedf7a01 | |||
| b0815a702e | |||
| feee36c8c9 | |||
| 983e27b628 | |||
| c68b31cb2c | |||
| 4b2de4bcbf | |||
| 786cb30c61 | |||
| c0bc1df3f7 | |||
| 98e0e047b0 | |||
| a8bef95127 | |||
| 3365256502 | |||
| de94dd4079 | |||
| a2a8d7e791 | |||
| 73c6467f14 | |||
| 6e55b890de | |||
| 64c23e4d9c | |||
| 7e2e235cf2 | |||
| 4f4ad3cad4 | |||
| a37f89f864 | |||
| 02fb980de6 | |||
| 77917e34c9 | |||
| 290a5f2339 | |||
| 719e66b38b | |||
| c0adcc3a4b | |||
| 33b251d55e | |||
| ea0585773b | |||
| 8b399545a5 | |||
| b87c6a6d4d | |||
| c617d6dcee | |||
| 56b6ba8e0b | |||
| f1c164c92f | |||
| 3eabaab009 | |||
| 9645a08b61 | |||
| 04ee1e781b | |||
| a126b346ff | |||
| ba140dbff9 | |||
| fb479188f5 | |||
| 75bf199421 | |||
| 9196c80c33 | |||
| e1e2100e66 | |||
| 9f1793fd2d | |||
| a7a9688962 | |||
| 32b1a0c580 | |||
| 9a0f1abdda | |||
| c484213180 | |||
| 2789ccbcf9 | |||
| 3e7a87d7d1 | |||
| 3ecb00115a | |||
| e4e2024d1c | |||
| 02f4ca4887 | |||
| 8d6ac5f317 | |||
| 7d50b2e9ee | |||
| ea04dbb229 | |||
| fbe679dfbc | |||
| fb00991460 | |||
| d3bffc5476 | |||
| b969c3e354 | |||
| 16b82c6afe | |||
| e3ece2fad9 | |||
| 67f54b6600 | |||
| 3fc6843528 | |||
| 1e9eadcd72 | |||
| a8830d2be4 | |||
| 8d9984e530 | |||
| 1d804645f7 | |||
| 40c49902b6 | |||
| 7300a5311c | |||
| 4c97df5852 | |||
| 2ea1e49cd6 | |||
| c76b09beed | |||
| f9d29889c4 | |||
| 50ceb23277 | |||
| ab386cdef2 | |||
| 507185979f | |||
| a7e5e83240 | |||
| 3f95ae21ef | |||
| 00b482fd8b | |||
| 9ba3d0070d | |||
| ddbf197ad9 | |||
| 7a9b7ba913 | |||
| 62e2ec18e6 | |||
| b86876c4a2 | |||
| c7bea44107 | |||
| 8510143a0e | |||
| 6388a348e4 | |||
| 2da4917af4 | |||
| 4ff51bf18e | |||
| ba42cedce5 | |||
| ff2ce9ddde | |||
| 8da3aef91c | |||
| 8988157861 | |||
| d253c9d45c | |||
| 07d84dd85f | |||
| 88cb9af594 | |||
| e942e1e988 | |||
| 96dd35afb6 | |||
| 9a521cb3ad | |||
| 62743a54b7 | |||
| 22450e5b7c | |||
| 240a3ddbf1 | |||
| 7df29521d8 | |||
| 167766a5dd | |||
| 0cd3921782 | |||
| f6e60c14c0 | |||
| 223de59e98 | |||
| 5c38fb744e | |||
| 97d0224ae6 | |||
| e5fb92978f | |||
| 41d5c90f17 | |||
| 2c1e58984b | |||
| 6d5ef34fc6 | |||
| 00815db8ef | |||
| a61c5d382e | |||
| a7688100e3 | |||
| dde520e02a | |||
| 394b185416 | |||
| da72477701 | |||
| 74b04c9403 | |||
| 2e03aedc85 | |||
| 199d888b84 | |||
| 8b69d91fff | |||
| 0fa5a37838 | |||
| 07e475137e | |||
| 5838017f26 | |||
| f2f5ac7ce4 | |||
| 55ba44913e | |||
| aa3f951330 | |||
| 12cf994fcc | |||
| 873e26347f | |||
| f876ab7191 | |||
| 181ac0b7a1 | |||
| 0cbc505121 | |||
| 667db466f9 | |||
| 2c9c3a0784 | |||
| 817ac56719 | |||
| 0864f73458 | |||
| c15c8a1380 | |||
| 9871ada04b | |||
| 202f1809ad | |||
| 32fbb2e78f | |||
| 9ba07e974a | |||
| dfcb5ac1e4 | |||
| 10b48094d7 | |||
| 0f0a16ec40 | |||
| 84294ec1fc | |||
| 69e0968309 | |||
| bf758d0bef | |||
| 00936ad12b | |||
| 656c8fa8f2 | |||
| 5462373b5b | |||
| 5f76bc6097 | |||
| c8d3498708 | |||
| 2cb0318c1c | |||
| 2c8bfd8a08 | |||
| e5c6174839 | |||
| f05f548546 | |||
| 8166dd40e4 | |||
| 0822d34b10 | |||
| 87273022f2 | |||
| ddd78bd3e4 | |||
| 05fdf918d9 | |||
| 6eb15cb114 | |||
| 72a87ceda7 | |||
| 6d997f56a3 | |||
| 34237f9299 | |||
| 7e5a12e2c1 | |||
| da6ae8b4b6 | |||
| efaf59f988 | |||
| f18dd2957c | |||
| 97b00ca497 | |||
| 57280768e1 | |||
| bf60182df1 | |||
| 8f61cf631a | |||
| 21a2f4bb23 | |||
| 20590c0bd0 | |||
| 2f7fad5f9f | |||
| ecc09a45f0 | |||
| 3651e42e49 | |||
| 233a93f6e0 | |||
| 59a9c1319d | |||
| 66dff7cf5f | |||
| 3072938186 | |||
| 48f0957dde | |||
| 6d2ff0ac6d | |||
| 7c876285cb | |||
| a83d64605f | |||
| 919087f0ec | |||
| 5b31b379a6 | |||
| c9691a29f2 | |||
| 9b6c1540a5 | |||
| 56ac2a7b3c | |||
| 811af9ccfb | |||
| a784fab605 | |||
| 3650723501 | |||
| aee5d02cb7 | |||
| 0179c70d02 | |||
| 84061c2954 | |||
| 9936e570ea | |||
| 1b196332f5 | |||
| d91a6bdbc6 | |||
| 394dbcc277 | |||
| f1d01bbc74 | |||
| 01d6a47e91 | |||
| ac57a25cd9 | |||
| 15bbd3e18c | |||
| 87a586eb9a | |||
| e6d5fe7dea | |||
| 823c7edb84 | |||
| 631076a31b | |||
| 341f8dbe24 | |||
| 8de97949c5 | |||
| 2fb2d09971 | |||
| 2ee5033967 | |||
| de74034ddf | |||
| 529550d0da | |||
| 828a2757b2 | |||
| 7e97db93ff | |||
| 6a22c79a94 | |||
| 0616dde95e | |||
| 25bf1ef07c | |||
| 7f2accaa3a | |||
| c8793431b6 | |||
| 77b302ab00 | |||
| af83c1541c | |||
| e872f0ed36 | |||
| 122d89b240 | |||
| 1991e77e43 | |||
| 57a972d899 | |||
| cdaa6a951b | |||
| bf0af6dbb1 | |||
| 62c9d1da7a | |||
| 8deff89e5f | |||
| 65728383f3 | |||
| a772b94b0a | |||
| 423acc2c67 | |||
| a03b75c6a8 | |||
| e58bcfab99 | |||
| 4015e0fde5 | |||
| 030ae660bb | |||
| b85926b33f | |||
| b12a0b7652 | |||
| 464dde8bd1 | |||
| 42299e9f6d | |||
| 53fb909068 | |||
| 420ceb6e48 | |||
| 32f4645314 | |||
| e5c5b4a131 | |||
| 5d5fd6292a | |||
| 7fbbacc104 | |||
| 1df736e256 | |||
| 7673ca7d15 | |||
| e65928eecb | |||
| 3bce707421 | |||
| b4651e5278 | |||
| aef3ef7b03 | |||
| b5118ac6a9 | |||
| 33cd29b3a3 | |||
| 667d4734fc | |||
| 837acd14e0 | |||
| 63b6d64a60 | |||
| 762580ff34 | |||
| a86a3195b4 | |||
| 2acea11a17 | |||
| 9bcdfcfe17 | |||
| 66945e217e | |||
| cd5efa095e | |||
| e79c472fdd | |||
| e1dd00e4ef | |||
| 8a89586bed | |||
| d0d829899c | |||
| 30eb194341 | |||
| 2f3bd89a51 | |||
| 301fdda648 | |||
| 1069086299 | |||
| 2a8a29ac43 | |||
| d21acafa77 | |||
| 8f563e2ecf | |||
| c09e613aac | |||
| 913cf794c9 | |||
| 17fb15d7a4 | |||
| 9341671d78 | |||
| 4a12ae7b36 | |||
| b496daabe6 | |||
| 6fc0f02ad2 | |||
| 2fdfbe7296 | |||
| f90b48d7d8 | |||
| dcc57f1718 | |||
| 602fa698a5 | |||
| b277e3ead7 | |||
| e4c2fe6d42 | |||
| ff0e61166d | |||
| 4b06637f70 | |||
| 5878f07474 | |||
| 8d7c7f4a8e | |||
| 080357e906 | |||
| 69c3faf405 | |||
| 7efed00632 | |||
| 496138f12a | |||
| 37a2c20bb8 | |||
| 8eb925d7c5 | |||
| ad0bb83819 | |||
| e23782b8c2 | |||
| d0622d0649 | |||
| e812b9fa9e | |||
| 6ee7c29ca7 | |||
| 0fbb1187b8 | |||
| 136a42abc1 | |||
| 06516d7c27 | |||
| 617b36117a | |||
| 189461f9bf | |||
| 93879b3c72 | |||
| c2b8fab0a4 | |||
| f403925ee0 | |||
| 1e841a89c1 | |||
| 2b0c0505e2 | |||
| 9d74f0fdcb | |||
| f1d0f03863 | |||
| b0f6afcdac | |||
| 2931a6df03 | |||
| bf5c2eef34 | |||
| dcdd5def8f | |||
| f3aa1d183a | |||
| 80077f35d0 | |||
| 32f38a33b6 | |||
| 9de0842719 | |||
| b9d2b30808 | |||
| 3d0abffbc0 | |||
| c35786ae26 | |||
| 60644d5d05 | |||
| 233f47b98a | |||
| 421040588c | |||
| f132ce740a | |||
| 5377baf0fc | |||
| bf7685abbd | |||
| ba2e0d3d9b | |||
| 0d9d57d418 | |||
| d96e58fdc8 | |||
| 70dbb15846 | |||
| d94153939c | |||
| eacf06f5cf | |||
| bb2a247aa5 | |||
| 24c94b83fa | |||
| 0c3620bbf5 | |||
| 38f8c97af7 | |||
| c80fa1cfe1 | |||
| 11d9ad1eb2 | |||
| 5a13cacf9f | |||
| 28bad725b1 | |||
| 1daa4f2231 | |||
| 4d43ee3327 | |||
| a59f46b37d | |||
| 728ce72372 | |||
| b779091ffd | |||
| 6ccbfa65d6 | |||
| cc793a1364 | |||
| 3dc0096dc4 | |||
| 846fa1cdf6 | |||
| 60e294cbd8 | |||
| 550f309dba | |||
| 49aba51e6b | |||
| e269c14425 | |||
| 19bc52127f | |||
| 4f69d38f09 | |||
| ebe90051ed | |||
| a649758655 | |||
| c357b1aba6 | |||
| 603b66e1fa | |||
| 295043d32e | |||
| 892d236afc | |||
| b7ee5ee3c6 | |||
| b55637a8f7 | |||
| 9a2efb7b66 | |||
| db46d24491 | |||
| f779230f70 | |||
| 62494bae38 | |||
| f85656e934 | |||
| b9d3185e52 | |||
| 43cf54c3a2 | |||
| 15149c1da1 | |||
| 23e747331b | |||
| 71f437c15a | |||
| 2c7400e7d0 | |||
| 2af2607fba | |||
| a0641ea475 | |||
| 538f4606ff | |||
| 4385e1268a | |||
| ba8b924bf5 | |||
| 0ce9348bc0 | |||
| a29f66c465 | |||
| 79c4a712d7 | |||
| 2f667b177c | |||
| 07f34e7af3 | |||
| 493b4967de | |||
| 8c1b4c020d | |||
| edfb691deb | |||
| 8162362217 | |||
| 15581287a4 | |||
| fd76d872fc | |||
| 3708162bc8 | |||
| 813117da31 | |||
| dc8762eff1 | |||
| 1e3c7d2922 | |||
| 187fce7cdf | |||
| 2b00e8893e | |||
| d03357a320 | |||
| 03dbd94cb8 | |||
| 0ef193f1a3 | |||
| dea8ae9af0 | |||
| 39a90a9c20 | |||
| fd4f011118 | |||
| 7084deccaa | |||
| 7019d9f27a | |||
| 89f3e3b0af | |||
| 641e13acc1 | |||
| 028fa1abb2 | |||
| 3c259ce624 | |||
| ef7d18b1c4 | |||
| 03043839d5 | |||
| d24617bf32 | |||
| 2b7a359363 | |||
| e7d8eee46d | |||
| 565ca6a1f5 | |||
| 54a7caf667 | |||
| eba7c28261 | |||
| 83b67b6c54 | |||
| d85b3f56ac | |||
| 1cf728e209 | |||
| 675f99bc5a | |||
| 5b6763f575 | |||
| 22392ef949 | |||
| f63c46406f | |||
| 3983a66eda | |||
| 74e232dab7 | |||
| 2e27df690c | |||
| c522a43f41 | |||
| 9c722cfcd2 | |||
| 9412962696 | |||
| 752f411c8b | |||
| 2b360bf305 | |||
| c00903be7a | |||
| 3776b2ef30 | |||
| e193cda800 | |||
| 296e9f6cdf | |||
| 035f1cd90e | |||
| 0694af8fc4 | |||
| 090a839ac9 | |||
| 5294d208ae | |||
| 4082fb03b6 | |||
| a24777a2c4 | |||
| 99a3adbdaa | |||
| 922a83e94f | |||
| ee1458fdba | |||
| cfc16b17f7 | |||
| 8ceeeccdd7 | |||
| 316f60fd14 | |||
| f768da2488 | |||
| c898ae9e45 | |||
| 47b14bf3e3 | |||
| 4e143fcae3 | |||
| 15d07c0108 | |||
| d08e31984d | |||
| fbe0b73ab5 | |||
| 07987e26e9 | |||
| c6842b5a65 | |||
| baa30695f3 | |||
| d03cac4cd5 | |||
| e9a4de035d | |||
| ce8abac66d | |||
| fade1db253 | |||
| 29f3bf4bd6 | |||
| 7f8bc587ae | |||
| 1ecf6efa81 | |||
| b82097085d | |||
| 5fd5e131b6 | |||
| 7b2ed4ae41 | |||
| 7c80d8afa9 | |||
| d05f894aff | |||
| 1c0c2605d3 | |||
| 5b477614ff | |||
| 9595337f67 | |||
| a9e91461a8 | |||
| 4e7f652b5f |
@@ -1,5 +1,4 @@
|
||||
build/**
|
||||
docs/bower_components/**
|
||||
docs/app/assets/js/angular-bootstrap/**
|
||||
docs/config/templates/**
|
||||
node_modules/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- General PR submission guidelines https://github.com/angular/angular.js/CONTRIBUTING.md#submit-pr -->
|
||||
<!-- General PR submission guidelines https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#submit-pr -->
|
||||
**What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)**
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
|
||||
**Please check if the PR fulfills these requirements**
|
||||
- [ ] The commit message follows our [guidelines](../DEVELOPERS.md#commits)
|
||||
- [ ] Fix/Feature: [Docs](../DEVELOPERS.md#documentation) have been added/updated
|
||||
- [ ] The commit message follows our [guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits)
|
||||
- [ ] Fix/Feature: [Docs](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#documentation) have been added/updated
|
||||
- [ ] Fix/Feature: Tests have been added; existing tests pass
|
||||
|
||||
**Other information**:
|
||||
|
||||
@@ -32,7 +32,6 @@ before_install:
|
||||
before_script:
|
||||
- du -sh ./node_modules || true
|
||||
- "./scripts/travis/before_build.sh"
|
||||
|
||||
script:
|
||||
- "./scripts/travis/build.sh"
|
||||
|
||||
|
||||
+1696
-2
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -49,7 +49,6 @@ If you find a bug in the source code, you can help us by submitting an issue to
|
||||
**Special Note for Localization Issues:** AngularJS uses the [Google Closure I18N library] to
|
||||
generate its own I18N files (the ngLocale module). This means that any changes to these files
|
||||
would be lost the next time that we import the library.
|
||||
|
||||
Since the Closure library i18n data is itself auto-generated from the data of the
|
||||
[Common Locale Data Repository (CLDR)] project, errors in the data should
|
||||
be reported there. See also the [Closure guide to i18n changes].
|
||||
@@ -165,6 +164,7 @@ restarted.
|
||||
* Re-run the AngularJS test suite to ensure tests are still passing.
|
||||
* Commit your changes to your branch (e.g. `my-fix-branch`).
|
||||
* Push the changes to your GitHub repository (this will update your Pull Request).
|
||||
|
||||
You can also amend the initial commits and force push them to the branch.
|
||||
|
||||
```shell
|
||||
|
||||
@@ -185,7 +185,6 @@ module.exports = function(grunt) {
|
||||
'test/**/*.js',
|
||||
'i18n/**/*.js',
|
||||
'!docs/app/assets/js/angular-bootstrap/**',
|
||||
'!docs/bower_components/**',
|
||||
'!docs/config/templates/**',
|
||||
'!src/angular.bind.js',
|
||||
'!i18n/closure/**',
|
||||
@@ -195,16 +194,6 @@ module.exports = function(grunt) {
|
||||
},
|
||||
|
||||
build: {
|
||||
scenario: {
|
||||
dest: 'build/angular-scenario.js',
|
||||
src: [
|
||||
'bower_components/jquery/dist/jquery.js',
|
||||
util.wrap([files['angularSrc'], files['angularScenario']], 'ngScenario/angular')
|
||||
],
|
||||
styles: {
|
||||
css: ['css/angular.css', 'css/angular-scenario.css']
|
||||
}
|
||||
},
|
||||
angular: {
|
||||
dest: 'build/angular.js',
|
||||
src: util.wrap([files['angularSrc']], 'angular'),
|
||||
@@ -290,9 +279,7 @@ module.exports = function(grunt) {
|
||||
files: [
|
||||
'src/**/*.js',
|
||||
'test/**/*.js',
|
||||
'!test/ngScenario/DescribeSpec.js',
|
||||
'!src/ng/directive/attrs.js', // legitimate xit here
|
||||
'!src/ngScenario/**/*.js',
|
||||
'!test/helpers/privateMocks*.js'
|
||||
],
|
||||
options: {
|
||||
|
||||
Vendored
-29
@@ -109,7 +109,6 @@ var angularFiles = {
|
||||
],
|
||||
'ngCookies': [
|
||||
'src/ngCookies/cookies.js',
|
||||
'src/ngCookies/cookieStore.js',
|
||||
'src/ngCookies/cookieWriter.js'
|
||||
],
|
||||
'ngMessageFormat': [
|
||||
@@ -146,7 +145,6 @@ var angularFiles = {
|
||||
'ngTouch': [
|
||||
'src/ngTouch/touch.js',
|
||||
'src/ngTouch/swipe.js',
|
||||
'src/ngTouch/directive/ngClick.js',
|
||||
'src/ngTouch/directive/ngSwipe.js'
|
||||
],
|
||||
'ngAria': [
|
||||
@@ -154,26 +152,8 @@ var angularFiles = {
|
||||
]
|
||||
},
|
||||
|
||||
'angularScenario': [
|
||||
'src/ngScenario/Scenario.js',
|
||||
'src/ngScenario/Application.js',
|
||||
'src/ngScenario/Describe.js',
|
||||
'src/ngScenario/Future.js',
|
||||
'src/ngScenario/ObjectModel.js',
|
||||
'src/ngScenario/Runner.js',
|
||||
'src/ngScenario/SpecRunner.js',
|
||||
'src/ngScenario/dsl.js',
|
||||
'src/ngScenario/matchers.js',
|
||||
'src/ngScenario/output/Html.js',
|
||||
'src/ngScenario/output/Json.js',
|
||||
'src/ngScenario/output/Xml.js',
|
||||
'src/ngScenario/output/Object.js'
|
||||
],
|
||||
|
||||
'angularTest': [
|
||||
'test/helpers/*.js',
|
||||
'test/ngScenario/*.js',
|
||||
'test/ngScenario/output/*.js',
|
||||
'test/*.js',
|
||||
'test/auto/*.js',
|
||||
'test/ng/**/*.js',
|
||||
@@ -194,22 +174,15 @@ var angularFiles = {
|
||||
'test/jquery_remove.js',
|
||||
'@angularSrc',
|
||||
'@angularSrcModules',
|
||||
'@angularScenario',
|
||||
'@angularTest'
|
||||
],
|
||||
|
||||
'karmaExclude': [
|
||||
'test/jquery_alias.js',
|
||||
'src/angular-bootstrap.js',
|
||||
'src/ngScenario/angular-bootstrap.js',
|
||||
'src/angular.bind.js'
|
||||
],
|
||||
|
||||
'karmaScenario': [
|
||||
'build/angular-scenario.js',
|
||||
'build/docs/docs-scenario.js'
|
||||
],
|
||||
|
||||
'karmaModules': [
|
||||
'build/angular.js',
|
||||
'@angularSrcModules',
|
||||
@@ -232,13 +205,11 @@ var angularFiles = {
|
||||
'test/jquery_alias.js',
|
||||
'@angularSrc',
|
||||
'@angularSrcModules',
|
||||
'@angularScenario',
|
||||
'@angularTest'
|
||||
],
|
||||
|
||||
'karmaJqueryExclude': [
|
||||
'src/angular-bootstrap.js',
|
||||
'src/ngScenario/angular-bootstrap.js',
|
||||
'test/jquery_remove.js',
|
||||
'src/angular.bind.js'
|
||||
]
|
||||
|
||||
@@ -29,19 +29,16 @@ app.directive('bmPeWatch', function() {
|
||||
};
|
||||
});
|
||||
|
||||
//Executes the specified expression as a watcher
|
||||
//Adds a simple wrapper method to allow use of $watch instead of $watchCollection
|
||||
app.directive('bmPeWatchLiteral', function($parse) {
|
||||
function retZero() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Executes the specified expression as a collection watcher
|
||||
app.directive('bmPeWatchCollection', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function($element, $attrs) {
|
||||
$element.text($attrs.bmPeWatchLiteral);
|
||||
$element.text($attrs.bmPeWatchCollection);
|
||||
return function($scope, $element, $attrs) {
|
||||
$scope.$watch($parse($attrs.bmPeWatchLiteral, retZero));
|
||||
$scope.$watchCollection($attrs.bmPeWatchCollection, function(val) {
|
||||
$element.text(val);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -72,8 +69,7 @@ app.controller('DataController', function($scope, $rootScope) {
|
||||
date2: new Date(Math.random() * Date.now()),
|
||||
func: function() { return star; },
|
||||
obj: data[i - 1],
|
||||
keys: data[i - 1] && (data[i - 1].keys || Object.keys(data[i - 1])),
|
||||
constructor: data[i - 1]
|
||||
keys: data[i - 1] && (data[i - 1].keys || Object.keys(data[i - 1]))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
<label for="complexPath">Complex Paths</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="radio" ng-model="expressionType" value="constructorPath" id="constructorPath">
|
||||
<label for="constructorPath">Constructor Paths</label>
|
||||
($parse special cases "constructor" for security)
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="radio" ng-model="expressionType" value="fieldAccess" id="fieldAccess">
|
||||
<label for="fieldAccess">Field Accessors</label>
|
||||
@@ -66,6 +60,16 @@
|
||||
<input type="radio" ng-model="expressionType" value="arrayLiterals" id="arrayLiterals">
|
||||
<label for="arrayLiterals">Array Literals</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="radio" ng-model="expressionType" value="watchCollection" id="watchCollection">
|
||||
<label for="watchCollection">$watchCollection</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="radio" ng-model="expressionType" value="watchCollectionLiterals" id="watchCollectionLiterals">
|
||||
<label for="watchCollectionLiterals">$watchCollection Literals</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!--
|
||||
@@ -88,17 +92,6 @@
|
||||
<span bm-pe-watch="row.keys"></span>
|
||||
</li>
|
||||
|
||||
<li ng-switch-when="constructorPath" ng-repeat="(rowIdx, row) in ::data">
|
||||
<span bm-pe-watch="row.index"></span>
|
||||
<span bm-pe-watch="row.constructor.index"></span>
|
||||
<span bm-pe-watch="row.constructor.index"></span>
|
||||
<span bm-pe-watch="row.constructor.index"></span>
|
||||
<span bm-pe-watch="row.constructor.constructor.index"></span>
|
||||
<span bm-pe-watch="row.constructor.constructor.index"></span>
|
||||
<span bm-pe-watch="row.constructor.constructor.constructor.index"></span>
|
||||
<span bm-pe-watch="row.constructor.constructor.constructor.index"></span>
|
||||
</li>
|
||||
|
||||
<li ng-switch-when="complexPath" ng-repeat="(rowIdx, row) in ::data">
|
||||
<span bm-pe-watch="row.index"></span>
|
||||
<span bm-pe-watch="row.num0"></span>
|
||||
@@ -215,27 +208,44 @@
|
||||
</li>
|
||||
|
||||
<li ng-switch-when="objectLiterals" ng-repeat="(rowIdx, row) in ::data">
|
||||
<span bm-pe-watch-literal="{foo: rowIdx}"></span>
|
||||
<span bm-pe-watch-literal="{foo: row, bar: rowIdx}"></span>
|
||||
<span bm-pe-watch-literal="{0: row, 1: rowIdx, 2: 3}"></span>
|
||||
<span bm-pe-watch-literal="{str: 'foo', num: rowIdx, b: true}"></span>
|
||||
<span bm-pe-watch-literal="{a: {b: {c: {d: {e: {f: rowIdx}}}}}}"></span>
|
||||
<span bm-pe-watch-literal="{a: rowIdx, b: 1, c: 2, d: 3, e: 4, f: 5, g: rowIdx, h: 6, i: 7, j: 8, k: rowIdx}"></span>
|
||||
<span bm-pe-watch="{foo: rowIdx}"></span>
|
||||
<span bm-pe-watch="{foo: row, bar: rowIdx}"></span>
|
||||
<span bm-pe-watch="{0: row, 1: rowIdx, 2: 3}"></span>
|
||||
<span bm-pe-watch="{str: 'foo', num: rowIdx, b: true}"></span>
|
||||
<span bm-pe-watch="{a: {b: {c: {d: {e: {f: rowIdx}}}}}}"></span>
|
||||
<span bm-pe-watch="{a: rowIdx, b: 1, c: 2, d: 3, e: 4, f: 5, g: rowIdx, h: 6, i: 7, j: 8, k: rowIdx}"></span>
|
||||
</li>
|
||||
|
||||
<li ng-switch-when="arrayLiterals" ng-repeat="(rowIdx, row) in ::data">
|
||||
<span bm-pe-watch-literal="[rowIdx]"></span>
|
||||
<span bm-pe-watch-literal="[rowIdx, 0]"></span>
|
||||
<span bm-pe-watch-literal="[rowIdx, 0, 1]"></span>
|
||||
<span bm-pe-watch-literal="[rowIdx, 0, 1, 2]"></span>
|
||||
<span bm-pe-watch-literal="[rowIdx, 0, 1, 2, 3]"></span>
|
||||
<span bm-pe-watch-literal="[[], [rowIdx], [], [], [3], [[[]]]]"></span>
|
||||
<span bm-pe-watch-literal="[rowIdx, undefined, null, true, false]"></span>
|
||||
<span bm-pe-watch-literal="[[][0], [0][0], [][rowIdx]]"></span>
|
||||
<span bm-pe-watch-literal="[0, rowIdx]"></span>
|
||||
<span bm-pe-watch-literal="[0, 1, rowIdx]"></span>
|
||||
<span bm-pe-watch-literal="[0, 1, 2, rowIdx]"></span>
|
||||
<span bm-pe-watch-literal="[0, 1, 2, 3, rowIdx]"></span>
|
||||
<span bm-pe-watch="[rowIdx]"></span>
|
||||
<span bm-pe-watch="[rowIdx, 0]"></span>
|
||||
<span bm-pe-watch="[rowIdx, 0, 1]"></span>
|
||||
<span bm-pe-watch="[rowIdx, 0, 1, 2]"></span>
|
||||
<span bm-pe-watch="[rowIdx, 0, 1, 2, 3]"></span>
|
||||
<span bm-pe-watch="[[], [rowIdx], [], [], [3], [[[]]]]"></span>
|
||||
<span bm-pe-watch="[rowIdx, undefined, null, true, false]"></span>
|
||||
<span bm-pe-watch="[[][0], [0][0], [][rowIdx]]"></span>
|
||||
<span bm-pe-watch="[0, rowIdx]"></span>
|
||||
<span bm-pe-watch="[0, 1, rowIdx]"></span>
|
||||
<span bm-pe-watch="[0, 1, 2, rowIdx]"></span>
|
||||
<span bm-pe-watch="[0, 1, 2, 3, rowIdx]"></span>
|
||||
</li>
|
||||
|
||||
<li ng-switch-when="watchCollection" ng-repeat="(rowIdx, row) in data">
|
||||
<span bm-pe-watch-collection="data"></span>
|
||||
<span bm-pe-watch-collection="row.keys"></span>
|
||||
<span bm-pe-watch-collection="thisProbablyDoesntHaveAValue"></span>
|
||||
</li>
|
||||
|
||||
<li ng-switch-when="watchCollectionLiterals" ng-repeat="(rowIdx, row) in ::data">
|
||||
<span bm-pe-watch-collection="[rowIdx, row]"></span>
|
||||
<span bm-pe-watch-collection="[rowIdx, row, num0, str0, date0, obj, g, h, i, j, k, l, m, n, o, p]"></span>
|
||||
<span bm-pe-watch-collection="{a: rowIdx, b: row, c: num0, d: str0, e: date0, f: obj, g: g, h: h, i: i, j: j, k: k, l: l, m: m, n: n, o: o, p: p}"></span>
|
||||
|
||||
<!-- primitive/valueOf-compatible -->
|
||||
<span bm-pe-watch-collection="[rowIdx, row]"></span>
|
||||
<span bm-pe-watch-collection="[rowIdx, num0, str0, date0, date1, h, i, j, k, l, m, n, o, p]"></span>
|
||||
<span bm-pe-watch-collection="{a: rowIdx, c: num0, d: str0, e: date0, g: date1, h: h, i: i, j: j, k: k, l: l, m: m, n: n, o: o, p: p}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,8 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#json, #xml {
|
||||
#json,
|
||||
#xml {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -1,7 +1,11 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
|
||||
.ng-cloak, .x-ng-cloak,
|
||||
[ng\:cloak],
|
||||
[ng-cloak],
|
||||
[data-ng-cloak],
|
||||
[x-ng-cloak],
|
||||
.ng-cloak,
|
||||
.x-ng-cloak,
|
||||
.ng-hide:not(.ng-hide-animate) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
describe('table of contents', function() {
|
||||
|
||||
it('on provider pages', function() {
|
||||
browser.get('build/docs/index.html#!/api/ng/provider/$interpolateProvider');
|
||||
browser.get('build/docs/index.html#!/api/ng/provider/$controllerProvider');
|
||||
|
||||
var toc = element.all(by.css('toc-container > div > toc-tree'));
|
||||
toc.getText().then(function(text) {
|
||||
@@ -19,7 +19,7 @@ describe('table of contents', function() {
|
||||
var tocFirstLevel = element.all(by.css('toc-container > div > toc-tree > ul > li'));
|
||||
|
||||
tocFirstLevel.then(function(match) {
|
||||
expect(match.length).toBe(3);
|
||||
expect(match.length).toBe(2);
|
||||
|
||||
expect(match[1].all(by.css('li')).count()).toBe(2);
|
||||
});
|
||||
|
||||
@@ -91,16 +91,13 @@ directivesModule
|
||||
.component('tocTree', {
|
||||
template: '<ul>' +
|
||||
'<li ng-repeat="item in $ctrl.items">' +
|
||||
'<a ng-href="{{ $ctrl.path }}#{{item.fragment}}">{{item.title}}</a>' +
|
||||
'<a ng-href="#{{item.fragment}}">{{item.title}}</a>' +
|
||||
'<toc-tree ng-if="::item.children.length > 0" items="item.children"></toc-tree>' +
|
||||
'</li>' +
|
||||
'</ul>',
|
||||
bindings: {
|
||||
items: '<'
|
||||
},
|
||||
controller: ['$location', function TocTreeController($location) {
|
||||
this.path = $location.path().replace(/^\/?(.+?)(\/index)?\/?$/, '$1');
|
||||
}]
|
||||
}
|
||||
})
|
||||
.directive('tocContainer', function() {
|
||||
return {
|
||||
|
||||
@@ -5,18 +5,36 @@
|
||||
* @description
|
||||
* Process "error" docType docs and generate errorNamespace docs
|
||||
*/
|
||||
module.exports = function errorDocsProcessor(errorNamespaceMap, getMinerrInfo) {
|
||||
module.exports = function errorDocsProcessor(log, errorNamespaceMap, getMinerrInfo) {
|
||||
return {
|
||||
$runAfter: ['tags-extracted'],
|
||||
$runBefore: ['extra-docs-added'],
|
||||
$process: function(docs) {
|
||||
|
||||
// Get the extracted min errors to compare with the error docs, and report any mismatch
|
||||
var collectedErrors = require('../../../build/errors.json').errors;
|
||||
var flatErrors = [];
|
||||
|
||||
for (var namespace in collectedErrors) {
|
||||
for (var error in collectedErrors[namespace]) {
|
||||
flatErrors.push(namespace + ':' + error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create error namespace docs and attach error docs to each
|
||||
docs.forEach(function(doc) {
|
||||
var parts, namespaceDoc;
|
||||
|
||||
if (doc.docType === 'error') {
|
||||
|
||||
var matchingMinErr = flatErrors.indexOf(doc.name);
|
||||
|
||||
if (matchingMinErr === -1) {
|
||||
log.warn('Error doc: ' + doc.name + ' has no matching min error');
|
||||
} else {
|
||||
flatErrors.splice(matchingMinErr, 1);
|
||||
}
|
||||
|
||||
// Parse out the error info from the id
|
||||
parts = doc.name.split(':');
|
||||
doc.namespace = parts[0];
|
||||
@@ -41,6 +59,10 @@ module.exports = function errorDocsProcessor(errorNamespaceMap, getMinerrInfo) {
|
||||
}
|
||||
});
|
||||
|
||||
flatErrors.forEach(function(value) {
|
||||
log.warn('No error doc exists for min error: ' + value);
|
||||
});
|
||||
|
||||
errorNamespaceMap.forEach(function(errorNamespace) {
|
||||
docs.push(errorNamespace);
|
||||
});
|
||||
|
||||
@@ -222,11 +222,7 @@ Use the ngCookies module to handle cookie management within your application.
|
||||
{@link ngCookies#service Services / Factories}
|
||||
</td>
|
||||
<td>
|
||||
The following services are used for cookie management:
|
||||
<ul>
|
||||
<li>The {@link ngCookies.$cookies $cookie} service is a convenient wrapper to store simple data within browser cookies.</li>
|
||||
<li>{@link ngCookies.$cookieStore $cookieStore} is used to store more complex data using serialization.</li>
|
||||
</ul>
|
||||
The {@link ngCookies.$cookies $cookies} service is a convenient wrapper to store simple data within browser cookies.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -10,13 +10,15 @@ myModule.directive('directiveName', function factory() {
|
||||
return {
|
||||
...
|
||||
scope: {
|
||||
'attrName': '@', // OK
|
||||
'attrName2': '=localName', // OK
|
||||
'attrName3': '<?localName', // OK
|
||||
'attrName4': ' = name', // OK
|
||||
'attrName5': 'name', // ERROR: missing mode @&=
|
||||
'attrName6': 'name=', // ERROR: must be prefixed with @&=
|
||||
'attrName7': '=name?', // ERROR: ? must come directly after the mode
|
||||
'localName': '@', // OK
|
||||
'localName2': '&attr', // OK
|
||||
'localName3': '<?attr', // OK
|
||||
'localName4': ' = attr', // OK
|
||||
'localName5': ' =*attr', // OK
|
||||
'localName6': 'attr', // ERROR: missing mode @&=<
|
||||
'localName7': 'attr=', // ERROR: must be prefixed with @&=<
|
||||
'localName8': '=attr?', // ERROR: ? must come directly after the mode
|
||||
'localName9': '<*' // ERROR: * is only valid with =
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
@fullName Missing required attribute
|
||||
@description
|
||||
|
||||
This error may occur only when `$compileProvider.strictComponentBindingsEnabled` is set to `true`.
|
||||
Then all attributes mentioned in `bindings` without `?` must be set. If one or more aren't set,
|
||||
the first one will throw an error.
|
||||
This error may occur only when {@link $compileProvider#strictComponentBindingsEnabled `$compileProvider.strictComponentBindingsEnabled`} is set to `true`.
|
||||
|
||||
If that is the case, then all {@link $compileProvider#component component} controller bindings and
|
||||
{@link $compileProvider#directive directive} scope / controller bindings that are non-optional,
|
||||
must be provided when the directive is instantiated.
|
||||
|
||||
To make a binding optional, add '?' to the definition.
|
||||
|
||||
## Example:
|
||||
|
||||
```js
|
||||
|
||||
app.component('myTest', {
|
||||
bindings: {
|
||||
first: '=?', // optional
|
||||
second: '='
|
||||
},
|
||||
controller: function() {
|
||||
...
|
||||
},
|
||||
template: '...'
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
This component will throw `missingattr` for the `second` binding when used as follows:
|
||||
|
||||
```html
|
||||
<my-test></my-test>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
@ngdoc error
|
||||
@name $compile:srcset
|
||||
@fullName Invalid value passed to `attr.$set('srcset', value)`
|
||||
@description
|
||||
|
||||
This error occurs if you try to programmatically set the `srcset` attribute with a non-string value.
|
||||
|
||||
This can be the case if you tried to avoid the automatic sanitization of the `srcset` value by
|
||||
passing a "trusted" value provided by calls to `$sce.trustAsMediaUrl(value)`.
|
||||
|
||||
If you want to programmatically set explicitly trusted unsafe URLs, you should use `$sce.trustAsHtml`
|
||||
on the whole `img` tag and inject it into the DOM using the `ng-bind-html` directive.
|
||||
@@ -1,11 +0,0 @@
|
||||
@ngdoc error
|
||||
@name $compile:tpload
|
||||
@fullName Error Loading Template
|
||||
@description
|
||||
|
||||
This error occurs when {@link ng.$compile `$compile`} attempts to fetch a template from some URL, and the request fails.
|
||||
|
||||
To resolve this error, ensure that the URL of the template is spelled correctly and resolves to correct absolute URL.
|
||||
The [Chrome Developer Tools](https://developers.google.com/chrome-developer-tools/docs/network#network_panel_overview) might also be helpful in determining why the request failed.
|
||||
|
||||
If you are using {@link ng.$templateCache} to pre-load templates, ensure that the cache was populated with the template.
|
||||
@@ -9,7 +9,7 @@ value is `JSON_CALLBACK`.
|
||||
|
||||
`$http` JSONP requests need to attach a callback query parameter to the URL. The name of this
|
||||
parameter is specified in the configuration object (or in the defaults) via the `jsonpCallbackParam`
|
||||
property. You must not provide your own parameter with this name in the configuratio of the request.
|
||||
property. You must not provide your own parameter with this name in the configuration of the request.
|
||||
|
||||
In previous versions of AngularJS, you specified where to add the callback parameter value via the
|
||||
`JSON_CALLBACK` placeholder. This is no longer allowed.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
@ngdoc error
|
||||
@name $interval:badprom
|
||||
@fullName Non-$interval promise
|
||||
@description
|
||||
|
||||
This error occurs when calling {@link ng.$interval#cancel $interval.cancel()} with a promise that
|
||||
was not generated by the {@link ng.$interval $interval} service. This can, for example, happen when
|
||||
calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new
|
||||
promise, and pass that new promise to {@link ng.$interval#cancel $interval.cancel()}.
|
||||
|
||||
Example of incorrect usage that leads to this error:
|
||||
|
||||
```js
|
||||
var promise = $interval(doSomething, 1000, 5).then(doSomethingElse);
|
||||
$interval.cancel(promise);
|
||||
```
|
||||
|
||||
To fix the example above, keep a reference to the promise returned by
|
||||
{@link ng.$interval $interval()} and pass that to {@link ng.$interval#cancel $interval.cancel()}:
|
||||
|
||||
```js
|
||||
var promise = $interval(doSomething, 1000, 5);
|
||||
var newPromise = promise.then(doSomethingElse);
|
||||
$interval.cancel(promise);
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
@ngdoc error
|
||||
@name $templateRequest:tpload
|
||||
@fullName Error Loading Template
|
||||
@description
|
||||
|
||||
This error occurs when {@link $templateRequest} attempts to fetch a template from some URL, and
|
||||
the request fails.
|
||||
|
||||
The template URL might be defined in a directive/component definition, an instance of `ngInclude`,
|
||||
an instance of `ngMessagesInclude` or a templated route in a `$route` route definition.
|
||||
|
||||
To resolve this error, ensure that the URL of the template is spelled correctly and resolves to
|
||||
correct absolute URL.
|
||||
The [Chrome Developer Tools](https://developers.google.com/chrome-developer-tools/docs/network#network_panel_overview)
|
||||
might also be helpful in determining why the request failed.
|
||||
|
||||
If you are using {@link ng.$templateCache} to pre-load templates, ensure that the cache was
|
||||
populated with the template.
|
||||
@@ -0,0 +1,25 @@
|
||||
@ngdoc error
|
||||
@name $timeout:badprom
|
||||
@fullName Non-$timeout promise
|
||||
@description
|
||||
|
||||
This error occurs when calling {@link ng.$timeout#cancel $timeout.cancel()} with a promise that
|
||||
was not generated by the {@link ng.$timeout $timeout} service. This can, for example, happen when
|
||||
calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new
|
||||
promise, and pass that new promise to {@link ng.$timeout#cancel $timeout.cancel()}.
|
||||
|
||||
Example of incorrect usage that leads to this error:
|
||||
|
||||
```js
|
||||
var promise = $timeout(doSomething, 1000).then(doSomethingElse);
|
||||
$timeout.cancel(promise);
|
||||
```
|
||||
|
||||
To fix the example above, keep a reference to the promise returned by
|
||||
{@link ng.$timeout $timeout()} and pass that to {@link ng.$timeout#cancel $timeout.cancel()}:
|
||||
|
||||
```js
|
||||
var promise = $timeout(doSomething, 1000);
|
||||
var newPromise = promise.then(doSomethingElse);
|
||||
$timeout.cancel(promise);
|
||||
```
|
||||
@@ -3,12 +3,9 @@
|
||||
@fullName Unsupported Selector Lookup
|
||||
@description
|
||||
|
||||
In order to keep AngularJS small, AngularJS implements only a subset of the selectors in
|
||||
{@link angular.element#angularjs-s-jqlite jqLite}.
|
||||
In order to keep AngularJS small, AngularJS implements only a subset of the selectors in {@link angular.element#angularjs-s-jqlite jqLite}.
|
||||
This error occurs when a jqLite instance is invoked with a selector other than this subset.
|
||||
|
||||
In order to resolve this error, rewrite your code to only use tag name selectors and manually
|
||||
traverse the DOM using the APIs provided by jqLite.
|
||||
In order to resolve this error, rewrite your code to only use tag name selectors and manually traverse the DOM using the APIs provided by jqLite.
|
||||
|
||||
Alternatively, you can include a full version of jQuery, which AngularJS will automatically use
|
||||
and that will make all selectors available.
|
||||
Alternatively, you can include a full version of jQuery, which AngularJS will automatically use and that will make all selectors available.
|
||||
|
||||
@@ -468,7 +468,7 @@ You can also use one of the other
|
||||
strategies to disable animations}.
|
||||
|
||||
|
||||
### Enable animations for elements outside of the AngularJS application DOM tree: {@link ng.$animate#pin $animate.pin()}
|
||||
## Enable animations outside of the application DOM tree: {@link ng.$animate#pin $animate.pin()}
|
||||
|
||||
Before animating, `ngAnimate` checks if the animated element is inside the application DOM tree. If
|
||||
not, no animation is run. Usually, this is not a problem since most apps use the `html` or `body`
|
||||
|
||||
@@ -14,6 +14,7 @@ This guide describes the Component Router for AngularJS.
|
||||
|
||||
<div class="alert alert-info">
|
||||
If you are looking for information about the default router for AngularJS have a look at the {@link ngRoute} module.
|
||||
|
||||
If you are looking for information about the Component Router for the new Angular then
|
||||
check out the [Angular Router Guide](https://angular.io/docs/ts/latest/guide/router.html).
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ When not to use Components:
|
||||
|
||||
## Creating and configuring a Component
|
||||
|
||||
Components can be registered using the `.component()` method of an AngularJS module (returned by {@link module `angular.module()`}). The method takes two arguments:
|
||||
Components can be registered using the {@link ng.$compileProvider#component `.component()`} method of an AngularJS module (returned by {@link module `angular.module()`}). The method takes two arguments:
|
||||
|
||||
* The name of the Component (as string).
|
||||
* The Component config object. (Note that, unlike the `.directive()` method, this method does **not** take a factory function.)
|
||||
|
||||
@@ -116,7 +116,6 @@ objects (or primitives) assigned to the scope become model properties. Any metho
|
||||
the scope are available in the template/view, and can be invoked via AngularJS expressions
|
||||
and `ng` event handler directives (e.g. {@link ng.directive:ngClick ngClick}).
|
||||
|
||||
|
||||
## Simple Spicy Controller Example
|
||||
|
||||
To illustrate further how Controller components work in AngularJS, let's create a little app with the
|
||||
@@ -157,7 +156,7 @@ string "very". Depending on which button is clicked, the `spice` model is set to
|
||||
|
||||
Things to notice in the example above:
|
||||
|
||||
- The `ngController` directive is used to (implicitly) create a scope for our template, and the
|
||||
- The `ng-controller` directive is used to (implicitly) create a scope for our template, and the
|
||||
scope is augmented (managed) by the `SpicyController` Controller.
|
||||
- `SpicyController` is just a plain JavaScript function. As an (optional) naming convention the name
|
||||
starts with capital letter and ends with "Controller".
|
||||
|
||||
@@ -125,8 +125,8 @@ and comments (M).
|
||||
|
||||
The built-in AngularJS directives show in their documentation page which type of matching they support.
|
||||
|
||||
The following demonstrates the various ways a directive that matches all 4 types
|
||||
(`myDir` in this case) can be referenced from within a template.
|
||||
The following demonstrates the various ways a directive (`myDir` in this case) that matches all
|
||||
4 types can be referenced from within a template.
|
||||
|
||||
```html
|
||||
<my-dir></my-dir>
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
|
||||
# E2E Testing
|
||||
|
||||
<div class="alert alert-danger">
|
||||
**Note:** In the past, end-to-end testing could be done with a deprecated tool called
|
||||
[AngularJS Scenario Runner](http://code.angularjs.org/1.2.16/docs/guide/e2e-testing). That tool
|
||||
is now in maintenance mode, and will be removed in version 1.7.0.
|
||||
</div>
|
||||
|
||||
As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to
|
||||
verify the correctness of new features, catch bugs and notice regressions. Unit tests
|
||||
are the first line of defense for catching bugs, but sometimes issues come up with integration
|
||||
|
||||
@@ -37,8 +37,8 @@ AngularJS expressions are like JavaScript expressions with the following differe
|
||||
even inside `ng-init` directive.
|
||||
|
||||
* **No RegExp Creation With Literal Notation:** You cannot create regular expressions
|
||||
in an AngularJS expression. An exception to this rule is {@link ngPattern `ng-pattern`} which
|
||||
accepts valid RegExp.
|
||||
in an AngularJS expression. An exception to this rule is {@link ngPattern `ng-pattern`} which accepts valid
|
||||
RegExp.
|
||||
|
||||
* **No Object Creation With New Operator:** You cannot use `new` operator in an AngularJS expression.
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ Official announcements, news and releases are posted to our blog, G+ and Twitter
|
||||
|
||||
* [AngularJS Blog](http://blog.angularjs.org/)
|
||||
* [Google+](https://plus.google.com/u/0/+AngularJS)
|
||||
* [Twitter](https://twitter.com/angularjs)
|
||||
* [Twitter](https://twitter.com/angular)
|
||||
* [AngularJS on YouTube](http://youtube.com/angularjs)
|
||||
|
||||
## Contributing to AngularJS
|
||||
|
||||
@@ -15,6 +15,764 @@ which drives many of these changes.
|
||||
* Several new features, especially animations, would not be possible without a few changes.
|
||||
* Finally, some outstanding bugs were best fixed by changing an existing API.
|
||||
|
||||
|
||||
## Migrating from 1.6 to 1.7
|
||||
|
||||
AngularJS 1.7 contains bug fixes and features to AngularJS core and its external modules, some of
|
||||
which contain breaking changes. However, most of these address internal behavior and not APIs, and
|
||||
should not affect many applications.
|
||||
Additionally, we have removed some long-deprecated modules and APIs.
|
||||
|
||||
The most notable changes are:
|
||||
|
||||
- $resource has now support for request and requestError interceptors
|
||||
|
||||
- Several deprecated features have been removed:
|
||||
- the `$controllerProvider.allowGlobals()` flag
|
||||
- the `$compileProvider.preAssignBindingsEnabled()` flag
|
||||
- the `angular.lowercase` and `angular.uppercase` methods
|
||||
- the `$cookieStore` service from the `ngCookies` module
|
||||
- the `ngClick` override directive and corresponding services from the `ngTouch` module
|
||||
- the complete `ngScenario` module
|
||||
|
||||
Please note that feature development (without breaking changes) has happened in parallel on the
|
||||
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those features
|
||||
that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
|
||||
|
||||
|
||||
<br />
|
||||
Below is the full list of breaking changes:
|
||||
|
||||
|
||||
<br />
|
||||
<a name="migrate1.6to1.7-ng-directives"></a>
|
||||
### Core: _Directives_
|
||||
|
||||
<a name="migrate1.6to1.7-ng-directives-form"></a>
|
||||
|
||||
#### **form**
|
||||
**Due to [223de5](https://github.com/angular/angular.js/commit/223de59e988dc0cc8b4ec3a045b7c0735eba1c77)**,
|
||||
forms will now set $submitted on child forms when they are submitted.
|
||||
For example:
|
||||
```
|
||||
<form name="parentform" ng-submit="$ctrl.submit()">
|
||||
<ng-form name="childform">
|
||||
<input type="text" name="input" ng-model="my.model" />
|
||||
</ng-form>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
```
|
||||
|
||||
Submitting this form will set $submitted on "parentform" and "childform".
|
||||
Previously, it was only set on "parentform".
|
||||
|
||||
This change was introduced because mixing form and ngForm does not create
|
||||
logically separate forms, but rather something like input groups.
|
||||
Therefore, child forms should inherit the submission state from their parent form.
|
||||
|
||||
|
||||
#### **input[radio]** and **input[checkbox]**
|
||||
**Due to [656c8f](https://github.com/angular/angular.js/commit/656c8fa8f23b1277cc5c214c4d0237f3393afa1e)**,
|
||||
`input[radio]` and `input[checkbox]` now listen to the "change" event instead of the "click" event.
|
||||
Most apps should not be affected, as "change" is automatically fired by browsers after "click"
|
||||
happens.
|
||||
|
||||
Two scenarios might need migration:
|
||||
|
||||
- Custom click events:
|
||||
|
||||
Before this change, custom click event listeners on radio / checkbox would be called after the
|
||||
input element and `ngModel` had been updated, unless they were specifically registered before
|
||||
the built-in click handlers.
|
||||
After this change, they are called before the input is updated, and can call event.preventDefault()
|
||||
to prevent the input from updating.
|
||||
|
||||
If an app uses a click event listener that expects ngModel to be updated when it is called, it now
|
||||
needs to register a change event listener instead.
|
||||
|
||||
- Triggering click events:
|
||||
|
||||
Conventional trigger functions:
|
||||
|
||||
The change event might not be fired when the input element is not attached to the document. This
|
||||
can happen in **tests** that compile input elements and
|
||||
trigger click events on them. Depending on the browser (Chrome and Safari) and the trigger method,
|
||||
the change event will not be fired when the input isn't attached to the document.
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
it('should update the model', inject(function($compile, $rootScope) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
With this patch, `$rootScope.checkbox` might not be true, because the click event
|
||||
hasn't triggered the change event. To make the test, work append the inputElm to the app's
|
||||
`$rootElement`, and the `$rootElement` to the `$document`.
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
|
||||
$rootElement.append(inputElm);
|
||||
$document.append($rootElement);
|
||||
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
#### **input\[number\]**
|
||||
**Due to [aa3f95](https://github.com/angular/angular.js/commit/aa3f951330ec7b10b43ea884d9b5754e296770ec)**,
|
||||
`input[type=number]` with `ngModel` now validates the input for the `max`/`min` restriction against
|
||||
the `ngModelController.$viewValue` instead of against the `ngModelController.$modelValue`.
|
||||
|
||||
This affects apps that use `$parsers` or `$formatters` to transform the input / model value.
|
||||
|
||||
If you rely on the $modelValue validation, you can overwrite the `min`/`max` validator from a custom directive, as seen in the following example directive definition object:
|
||||
|
||||
```
|
||||
{
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
var maxValidator = ctrl.$validators.max;
|
||||
|
||||
ctrk.$validators.max = function(modelValue, viewValue) {
|
||||
return maxValidator(modelValue, modelValue);
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### **ngModel, input**
|
||||
**Due to [74b04c](https://github.com/angular/angular.js/commit/74b04c9403af4fc7df5b6420f22c9f45a3e84140)**,
|
||||
*Custom* parsers that fail to parse on input types "email", "url", "number", "date", "month",
|
||||
"time", "datetime-local", "week", no longer set `ngModelController.$error[inputType]`, and
|
||||
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" do no
|
||||
longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
|
||||
|
||||
Instead, any custom parsers on these inputs set `ngModelController.$error.parse` and
|
||||
`ng-invalid-parse`. This change was made to make distinguishing errors from built-in parsers
|
||||
and custom parsers easier.
|
||||
|
||||
|
||||
#### **ngModelOptions**
|
||||
**Due to [55ba44](https://github.com/angular/angular.js/commit/55ba44913e02650b56410aa9ab5eeea5d3492b68)**,
|
||||
the 'default' key in 'debounce' now only debounces the default event, i.e. the event that is added
|
||||
as an update trigger by the different input directives automatically.
|
||||
|
||||
Previously, it also applied to other update triggers defined in 'updateOn' that
|
||||
did not have a corresponding key in the 'debounce'.
|
||||
|
||||
This behavior is now supported via a special wildcard / catch-all key: '*'.
|
||||
|
||||
See the following example:
|
||||
|
||||
Pre-1.7:
|
||||
'mouseup' is also debounced by 500 milliseconds because 'default' is applied:
|
||||
```
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { 'default': 500, 'blur': 0 }
|
||||
}
|
||||
```
|
||||
|
||||
1.7:
|
||||
The pre-1.7 behavior can be re-created by setting '*' as a catch-all debounce value:
|
||||
```
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { '*': 500, 'blur': 0 }
|
||||
}
|
||||
```
|
||||
|
||||
In contrast, when only 'default' is used, 'blur' and 'mouseup' are not debounced:
|
||||
```
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { 'default': 500 }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### **ngStyle**
|
||||
|
||||
**Due to [15bbd3](https://github.com/angular/angular.js/commit/15bbd3e18cd89b91f7206a06c73d40e54a8a48a0)**,
|
||||
previously the use of deep watch by ng-style would trigger styles to be
|
||||
re-applied when nested state changed. Now only changes to direct
|
||||
properties of the watched object will trigger changes.
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ng-services"></a>
|
||||
### Core: _Services_
|
||||
|
||||
#### **$compile**
|
||||
|
||||
**Due to [38f8c9](https://github.com/angular/angular.js/commit/38f8c97af74649ce224b6dd45f433cc665acfbfb)**,
|
||||
directive bindings are no longer available in the constructor.
|
||||
|
||||
Previously, the `$compileProvider.preAssignBindingsEnabled` flag was supported.
|
||||
The flag controlled whether bindings were available inside the controller
|
||||
constructor or only in the `$onInit` hook. The bindings are now no longer
|
||||
available in the constructor.
|
||||
|
||||
To migrate your code:
|
||||
|
||||
1. If you haven't invoked `$compileProvider.preAssignBindingsEnabled()` you
|
||||
don't have to do anything to migrate.
|
||||
|
||||
2. If you specified `$compileProvider.preAssignBindingsEnabled(false)`, you
|
||||
can remove that statement - since AngularJS 1.6.0 this is the default so your
|
||||
app should still work even in AngularJS 1.6 after such removal. Afterwards,
|
||||
migrating to AngularJS 1.7.0 shouldn't require any further action.
|
||||
|
||||
3. If you specified `$compileProvider.preAssignBindingsEnabled(true)` you need
|
||||
to first migrate your code so that the flag can be flipped to `false`. The
|
||||
instructions on how to do that are available in the "Migrating from 1.5 to 1.6"
|
||||
guide:
|
||||
https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
|
||||
Afterwards, remove the `$compileProvider.preAssignBindingsEnabled(true)`
|
||||
statement.
|
||||
|
||||
<hr />
|
||||
**Due to [6ccbfa](https://github.com/angular/angular.js/commit/6ccbfa65d60a3dc396d0cf6da21b993ad74653fd)**,
|
||||
the `xlink:href` security context for SVG's `a` and `image` elements has been lowered.
|
||||
|
||||
In the unlikely case that an app relied on RESOURCE_URL whitelisting for the
|
||||
purpose of binding to the `xlink:href` property of SVG's `<a>` or `<image>`
|
||||
elements and if the values do not pass the regular URL sanitization, they will
|
||||
break.
|
||||
|
||||
To fix this you need to ensure that the values used for binding to the affected
|
||||
`xlink:href` contexts are considered safe URLs, e.g. by whitelisting them in
|
||||
`$compileProvider`'s `aHrefSanitizationWhitelist` (for `<a>` elements) or
|
||||
`imgSrcSanitizationWhitelist` (for `<image>` elements).
|
||||
|
||||
<hr />
|
||||
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
|
||||
deepWatch is no longer used in in literal one-way bindings.
|
||||
|
||||
Previously when a literal value was passed into a directive/component via
|
||||
one-way binding it would be watched with a deep watcher.
|
||||
|
||||
For example, for `<my-component input="[a]">`, a new instance of the array
|
||||
would be passed into the directive/component (and trigger $onChanges) not
|
||||
only if `a` changed but also if any sub property of `a` changed such as
|
||||
`a.b` or `a.b.c.d.e` etc.
|
||||
|
||||
This also means a new but equal value for `a` would NOT trigger such a
|
||||
change.
|
||||
|
||||
Now literal values use an input-based watch similar to other directive/component
|
||||
one-way bindings. In this context inputs are the non-constant parts of the
|
||||
literal. In the example above the input would be `a`. Changes are only
|
||||
triggered when the inputs to the literal change.
|
||||
|
||||
<hr />
|
||||
**Due to [1cf728](https://github.com/angular/angular.js/commit/1cf728e209a9e0016068fac2769827e8f747760e)**,
|
||||
`base[href]` was added to the list of RESOURCE_URL context attributes.
|
||||
|
||||
Previously, `<base href="{{ $ctrl.baseUrl }}" />` would not require `baseUrl` to
|
||||
be trusted as a RESOURCE_URL. Now, `baseUrl` will be sent to `$sce`'s
|
||||
RESOURCE_URL checks. By default, it will break unless `baseUrl` is of the same
|
||||
origin as the application document.
|
||||
|
||||
Refer to the
|
||||
[`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce)
|
||||
for more info on how to trust a value in a RESOURCE_URL context.
|
||||
|
||||
Also, concatenation in trusted contexts is not allowed, which means that the
|
||||
following won't work: `<base href="/something/{{ $ctrl.partialPath }}" />`.
|
||||
|
||||
Either construct complex values in a controller (recommended):
|
||||
|
||||
```js
|
||||
this.baseUrl = '/something/' + this.partialPath;
|
||||
```
|
||||
```html
|
||||
<base href="{{ $ctrl.baseUrl }}" />
|
||||
```
|
||||
|
||||
Or use string concatenation in the interpolation expression (not recommended
|
||||
except for the simplest of cases):
|
||||
|
||||
```html
|
||||
<base href="{{ '/something/' + $ctrl.partialPath }}" />
|
||||
```
|
||||
|
||||
|
||||
#### **$rootScope**
|
||||
|
||||
**Due to ([c2b8fa](https://github.com/angular/angular.js/commit/c2b8fab0a480204374d561d6b9b3d47347ac5570))**,
|
||||
the arguments of `$watchGroup` callbacks have changed.
|
||||
|
||||
Previously when using `$watchGroup` the entries in `newValues` and
|
||||
`oldValues` represented the *most recent change of each entry*.
|
||||
|
||||
Now the entries in `oldValues` will always equal the `newValues` of the previous
|
||||
call of the listener. This means comparing the entries in `newValues` and
|
||||
`oldValues` can be used to determine which individual expressions changed.
|
||||
|
||||
For example `$scope.$watchGroup(['a', 'b'], fn)` would previously:
|
||||
|
||||
| Action | newValue | oldValue |
|
||||
|----------|------------|------------|
|
||||
| (init) | [undefined, undefined] | [undefined, undefined] |
|
||||
| `a=1` | [1, undefined] | [undefined, undefined] |
|
||||
| `a=2` | [2, undefined] | [1, undefined] |
|
||||
| `b=3` | [2, 3] | [1, undefined] |
|
||||
|
||||
|
||||
Now the `oldValue` will always equal the previous `newValue`:
|
||||
|
||||
| Action | newValue | oldValue |
|
||||
|----------|------------|------------|
|
||||
| (init) | [undefined, undefined] | [undefined, undefined] |
|
||||
| `a=1` | [1, undefined] | [undefined, undefined] |
|
||||
| `a=2` | [2, undefined] | [1, undefined] |
|
||||
| `b=3` | [2, 3] | [2, undefined] |
|
||||
|
||||
Note the last call now shows `a === 2` in the `oldValues` array.
|
||||
|
||||
This also makes the `oldValue` of one-time watchers more clear. Previously
|
||||
the `oldValue` of a one-time watcher would remain `undefined` forever. For
|
||||
example `$scope.$watchGroup(['a', '::b'], fn)` would previously:
|
||||
|
||||
| Action | newValue | oldValue |
|
||||
|----------|------------|------------|
|
||||
| (init) | [undefined, undefined] | [undefined, undefined] |
|
||||
| `a=1` | [1, undefined] | [undefined, undefined] |
|
||||
| `b=2` | [1, 2] | [undefined, undefined] |
|
||||
| `a=b=3` | [3, 2] | [1, undefined] |
|
||||
|
||||
Where now the `oldValue` will always equal the previous `newValue`:
|
||||
|
||||
| Action | newValue | oldValue |
|
||||
|----------|------------|------------|
|
||||
| (init) | [undefined, undefined] | [undefined, undefined] |
|
||||
| `a=1` | [1, undefined] | [undefined, undefined] |
|
||||
| `b=2` | [1, 2] | [1, undefined] |
|
||||
| `a=b=3` | [3, 2] | [1, 2] |
|
||||
|
||||
|
||||
#### **$interval**
|
||||
|
||||
**Due to [a8bef9](https://github.com/angular/angular.js/commit/a8bef95127775d83d80daa4617c33227c4b443d4)**,
|
||||
`$interval.cancel() will throw an error if called with a promise that was not generated by
|
||||
`$interval()`. Previously, it would silently do nothing.
|
||||
|
||||
Before:
|
||||
```js
|
||||
var promise = $interval(doSomething, 1000, 5).then(doSomethingElse);
|
||||
$interval.cancel(promise); // No error; interval NOT canceled.
|
||||
```
|
||||
|
||||
After:
|
||||
```js
|
||||
var promise = $interval(doSomething, 1000, 5).then(doSomethingElse);
|
||||
$interval.cancel(promise); // Throws error.
|
||||
```
|
||||
|
||||
Correct usage:
|
||||
```js
|
||||
var promise = $interval(doSomething, 1000, 5);
|
||||
var newPromise = promise.then(doSomethingElse);
|
||||
$interval.cancel(promise); // Interval canceled.
|
||||
```
|
||||
|
||||
|
||||
#### **$timeout**
|
||||
|
||||
**Due to [336525](https://github.com/angular/angular.js/commit/3365256502344970f86355d3ace1cb4251ae9828)**,
|
||||
`$timeout.cancel() will throw an error if called with a promise that was not generated by
|
||||
`$timeout()`. Previously, it would silently do nothing.
|
||||
|
||||
Before:
|
||||
```js
|
||||
var promise = $timeout(doSomething, 1000).then(doSomethingElse);
|
||||
$timeout.cancel(promise); // No error; timeout NOT canceled.
|
||||
```
|
||||
|
||||
After:
|
||||
```js
|
||||
var promise = $timeout(doSomething, 1000).then(doSomethingElse);
|
||||
$timeout.cancel(promise); // Throws error.
|
||||
```
|
||||
|
||||
Correct usage:
|
||||
```js
|
||||
var promise = $timeout(doSomething, 1000);
|
||||
var newPromise = promise.then(doSomethingElse);
|
||||
$timeout.cancel(promise); // Timeout canceled.
|
||||
```
|
||||
|
||||
|
||||
#### **$cookies**
|
||||
**Due to [73c646](https://github.com/angular/angular.js/commit/73c6467f1468353215dc689c019ed83aa4993c77)**,
|
||||
the `$cookieStore`service has been removed. Migrate to the $cookies service. Note that
|
||||
for object values you need to use the `putObject` & `getObject` methods as
|
||||
`get`/`put` will not correctly save/retrieve them.
|
||||
|
||||
Before:
|
||||
```js
|
||||
$cookieStore.put('name', {key: 'value'});
|
||||
$cookieStore.get('name'); // {key: 'value'}
|
||||
$cookieStore.remove('name');
|
||||
```
|
||||
|
||||
|
||||
#### **$templateRequest**
|
||||
|
||||
**Due to [c617d6](https://github.com/angular/angular.js/commit/c617d6dceee5b000bfceda44ced22fc16b48b18b)**,
|
||||
give tpload error namespace has changed. Previously the `tpload` error was namespaced to `$compile`.
|
||||
If you have code that matches errors of the form `[$compile:tpload]` it will no
|
||||
longer run. You should change the code to match
|
||||
`[$templateRequest:tpload]`.
|
||||
|
||||
<hr />
|
||||
**Due to ([fb0099](https://github.com/angular/angular.js/commit/fb00991460cf69ae8bc7f1f826363d09c73c0d5e)**,
|
||||
the service now returns the result of `$templateCache.put()` when making a server request to the
|
||||
template. Previously it would return the content of the response directly.
|
||||
This now means if you are decorating `$templateCache.put()` to manipulate the template, you will
|
||||
now get this manipulated result also on the first `$templateRequest` rather than only on subsequent
|
||||
calls (when the template is retrived from the cache).
|
||||
In practice this should not affect any apps, as it is unlikely that they rely on the template being
|
||||
different in the first and subsequent calls.
|
||||
|
||||
#### **$animate**
|
||||
**Due to [16b82c](https://github.com/angular/angular.js/commit/16b82c6afe0ab916fef1d6ca78053b00bf5ada83)**,
|
||||
$animate.cancel(runner) now rejects the underlying
|
||||
promise and calls the catch() handler on the runner
|
||||
returned by $animate functions (enter, leave, move,
|
||||
addClass, removeClass, setClass, animate).
|
||||
Previously it would resolve the promise as if the animation
|
||||
had ended successfully.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
var runner = $animate.addClass('red');
|
||||
runner.then(function() { console.log('success')});
|
||||
runner.catch(function() { console.log('cancelled')});
|
||||
|
||||
runner.cancel();
|
||||
```
|
||||
|
||||
Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'.
|
||||
To migrate, add a catch() handler to your animation runners.
|
||||
|
||||
|
||||
#### **$controller**
|
||||
|
||||
**Due to [e269c1](https://github.com/angular/angular.js/commit/e269c14425a3209040f65c022658770e00a36f16)**,
|
||||
the option to instantiate controllers from constructors on the global `window` object
|
||||
has been removed. Likewise, the deprecated `$controllerProvider.allowGlobals()`
|
||||
method that could enable this behavior, has been removed.
|
||||
|
||||
This behavior had been deprecated since AngularJS v1.3.0, because polluting the global scope
|
||||
is bad. To migrate, remove the call to $controllerProvider.allowGlobals() in the config, and
|
||||
register your controller via the Module API or the $controllerProvider, e.g.
|
||||
|
||||
```
|
||||
angular.module('myModule', []).controller('myController', function() {...});
|
||||
|
||||
angular.module('myModule', []).config(function($controllerProvider) {
|
||||
$controllerProvider.register('myController', function() {...});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
#### **$sce**
|
||||
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
|
||||
if you use `attrs.$set` for URL attributes (a[href] and img[src]) there will no
|
||||
longer be any automated sanitization of the value. This is in line with other
|
||||
programmatic operations, such as writing to the innerHTML of an element.
|
||||
|
||||
If you are programmatically writing URL values to attributes from untrusted
|
||||
input then you must sanitize it yourself. You could write your own sanitizer or copy
|
||||
the private `$$sanitizeUri` service.
|
||||
|
||||
Note that values that have been passed through the `$interpolate` service within the
|
||||
`URL` or `MEDIA_URL` will have already been sanitized, so you would not need to sanitize
|
||||
these values again.
|
||||
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ng-filters"></a>
|
||||
### Core: _Filters_
|
||||
|
||||
#### **orderBy**
|
||||
**Due to [1d8046](https://github.com/angular/angular.js/commit/1d804645f7656d592c90216a0355b4948807f6b8)**,
|
||||
when using `orderBy` to sort arrays containing `null` values, the `null` values
|
||||
will be considered "greater than" all other values, except for `undefined`.
|
||||
Previously, they were sorted as strings. This will result in different (but more
|
||||
intuitive) sorting order.
|
||||
|
||||
Before:
|
||||
```js
|
||||
orderByFilter(['a', undefined, 'o', null, 'z']);
|
||||
//--> 'a', null, 'o', 'z', undefined
|
||||
```
|
||||
|
||||
After:
|
||||
```js
|
||||
orderByFilter(['a', undefined, 'o', null, 'z']);
|
||||
//--> 'a', 'o', 'z', null, undefined
|
||||
```
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ng-misc"></a>
|
||||
### Core: _Misceallenous_
|
||||
|
||||
|
||||
#### **jqLite**
|
||||
**Due to [b7d396](https://github.com/angular/angular.js/commit/b7d396b8b6e8f27a1f4556d58fc903321e8d532a)**,
|
||||
removeData() no longer removes event handlers.
|
||||
|
||||
Before this commit `removeData()` invoked on an element removed its event
|
||||
handlers as well. If you want to trigger a full cleanup of an element, change:
|
||||
|
||||
```js
|
||||
elem.removeData();
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```js
|
||||
angular.element.cleanData(elem);
|
||||
```
|
||||
|
||||
In most cases, though, cleaning up after an element is supposed to be done
|
||||
only when it's removed from the DOM as well; in such cases the following:
|
||||
|
||||
```js
|
||||
elem.remove();
|
||||
```
|
||||
|
||||
will remove event handlers as well.
|
||||
|
||||
|
||||
|
||||
#### **Angular**
|
||||
|
||||
**Due to [1daa4f](https://github.com/angular/angular.js/commit/1daa4f2231a89ee88345689f001805ffffa9e7de)**,
|
||||
the helper functions `angular.lowercase` `and angular.uppercase` have been removed.
|
||||
|
||||
These functions have been deprecated since 1.5.0. They are internally
|
||||
used, but should not be exposed as they contain special locale handling
|
||||
(for Turkish) to maintain internal consistency regardless of user-set locale.
|
||||
|
||||
Developers should generally use the built-ins `toLowerCase` and `toUpperCase`
|
||||
or `toLocaleLowerCase` and `toLocaleUpperCase` for special cases.
|
||||
|
||||
Further, we generally discourage using the angular.x helpers in application code.
|
||||
|
||||
<hr />
|
||||
**Due to [e3ece2](https://github.com/angular/angular.js/commit/e3ece2fad9e1e6d47b5f06815ff186d7e6f44948)**,
|
||||
`angular.isArray()` now supports Array subclasses.
|
||||
|
||||
Previously, `angular.isArray()` was an alias for `Array.isArray()`.
|
||||
Therefore, objects that prototypally inherit from `Array` where not
|
||||
considered arrays. Now such objects are considered arrays too.
|
||||
|
||||
This change affects several other methods that use `angular.isArray()`
|
||||
under the hood, such as `angular.copy()`, `angular.equals()`,
|
||||
`angular.forEach()`, and `angular.merge()`.
|
||||
|
||||
This in turn affects how dirty checking treats objects that prototypally
|
||||
inherit from `Array` (e.g. MobX observable arrays). AngularJS will now
|
||||
be able to handle these objects better when copying or watching.
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngAria"></a>
|
||||
### ngAria
|
||||
|
||||
**Due to [6d5ef3](https://github.com/angular/angular.js/commit/6d5ef34fc6a974cde73157ba94f9706723dd8f5b)**,
|
||||
the ngAria directive no longer sets aria-* attributes on input[type="hidden"] with ngModel.
|
||||
This can affect apps that test for the presence of aria attributes on hidden inputs.
|
||||
To migrate, remove these assertions.
|
||||
In actual apps, this should not have a user-facing effect, as the previous behavior
|
||||
was incorrect, and the new behavior is correct for accessibility.
|
||||
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngResource"></a>
|
||||
### ngResource
|
||||
|
||||
#### **$resource**
|
||||
**Due to [ea0585](https://github.com/angular/angular.js/commit/ea0585773bb93fd891576e2271254a17e15f1ddd)**,
|
||||
the behavior of interceptors and success/error callbacks has changed.
|
||||
|
||||
If you are not using `success` or `error` callbacks with `$resource`,
|
||||
your app should not be affected by this change.
|
||||
|
||||
If you are using `success` or `error` callbacks (with or without
|
||||
response interceptors), one (subtle) difference is that throwing an
|
||||
error inside the callbacks will not propagate to the returned
|
||||
`$promise`. Therefore, you should try to use the promises whenever
|
||||
possible. E.g.:
|
||||
|
||||
```js
|
||||
// Avoid
|
||||
User.query(function onSuccess(users) { throw new Error(); }).
|
||||
$promise.
|
||||
catch(function onError() { /* Will not be called. */ });
|
||||
|
||||
// Prefer
|
||||
User.query().
|
||||
$promise.
|
||||
then(function onSuccess(users) { throw new Error(); }).
|
||||
catch(function onError() { /* Will be called. */ });
|
||||
```
|
||||
|
||||
Finally, if you are using `success` or `error` callbacks with response
|
||||
interceptors, the callbacks will now always run _after_ the interceptors
|
||||
(and wait for them to resolve in case they return a promise).
|
||||
Previously, the `error` callback was called before the `responseError`
|
||||
interceptor and the `success` callback was synchronously called after
|
||||
the `response` interceptor. E.g.:
|
||||
|
||||
```js
|
||||
var User = $resource('/api/users/:id', {id: '@id'}, {
|
||||
get: {
|
||||
method: 'get',
|
||||
interceptor: {
|
||||
response: function(response) {
|
||||
console.log('responseInterceptor-1');
|
||||
return $timeout(1000).then(function() {
|
||||
console.log('responseInterceptor-2');
|
||||
return response.resource;
|
||||
});
|
||||
},
|
||||
responseError: function(response) {
|
||||
console.log('responseErrorInterceptor-1');
|
||||
return $timeout(1000).then(function() {
|
||||
console.log('responseErrorInterceptor-2');
|
||||
return $q.reject('Ooops!');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var onSuccess = function(value) { console.log('successCallback', value); };
|
||||
var onError = function(error) { console.log('errorCallback', error); };
|
||||
|
||||
// Assuming the following call is successful...
|
||||
User.get({id: 1}, onSuccess, onError);
|
||||
// Old behavior:
|
||||
// responseInterceptor-1
|
||||
// successCallback, {/* Promise object */}
|
||||
// responseInterceptor-2
|
||||
// New behavior:
|
||||
// responseInterceptor-1
|
||||
// responseInterceptor-2
|
||||
// successCallback, {/* User object */}
|
||||
|
||||
// Assuming the following call returns an error...
|
||||
User.get({id: 2}, onSuccess, onError);
|
||||
// Old behavior:
|
||||
// errorCallback, {/* Response object */}
|
||||
// responseErrorInterceptor-1
|
||||
// responseErrorInterceptor-2
|
||||
// New behavior:
|
||||
// responseErrorInterceptor-1
|
||||
// responseErrorInterceptor-2
|
||||
// errorCallback, Ooops!
|
||||
```
|
||||
|
||||
<hr />
|
||||
**Due to [240a3d](https://github.com/angular/angular.js/commit/240a3ddbf12a9bb79754031be95dae4b6bd2dded)**,
|
||||
`$http` will be called asynchronously from `$resource` methods
|
||||
(regardless if a `request`/`requestError` interceptor has been defined).
|
||||
|
||||
Previously, calling a `$resource` method would synchronously call
|
||||
`$http`.
|
||||
|
||||
This is not expected to affect applications at runtime, since the
|
||||
overall operation is asynchronous already, but may affect assertions in
|
||||
tests. For example, if you want to assert that `$http` has been called
|
||||
with specific arguments as a result of a `$resource` call, you now need
|
||||
to run a `$digest` first, to ensure the (possibly empty) request
|
||||
interceptor promise has been resolved.
|
||||
|
||||
Before:
|
||||
```js
|
||||
it('...', function() {
|
||||
$httpBackend.expectGET('/api/things').respond(...);
|
||||
var Things = $resource('/api/things');
|
||||
Things.query();
|
||||
|
||||
expect($http).toHaveBeenCalledWith(...);
|
||||
});
|
||||
```
|
||||
|
||||
After:
|
||||
```js
|
||||
it('...', function() {
|
||||
$httpBackend.expectGET('/api/things').respond(...);
|
||||
var Things = $resource('/api/things');
|
||||
Things.query();
|
||||
$rootScope.$digest();
|
||||
|
||||
expect($http).toHaveBeenCalledWith(...);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngScenario"></a>
|
||||
### ngScenario
|
||||
|
||||
**Due to[0cd392](https://github.com/angular/angular.js/commit/0cd39217828b0ad53eaf731576af17d66c18ff60)**,
|
||||
the angular scenario runner end-to-end test framework has been
|
||||
removed from the project and will no longer be available on npm
|
||||
or bower starting with 1.7.0.
|
||||
It was deprecated and removed from the documentation in 2014.
|
||||
Applications that still use it should migrate to
|
||||
[Protractor](http://www.protractortest.org).
|
||||
Technically, it should also be possible to continue using an
|
||||
older version of the scenario runner, as the underlying APIs have
|
||||
not changed. However, we do not guarantee future compatibility.
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngTouch"></a>
|
||||
### ngTouch
|
||||
|
||||
**Due to [11d9ad](https://github.com/angular/angular.js/commit/11d9ad1eb25eaf5967195e424108207427835d50)**,
|
||||
the `ngClick` directive from the ngTouch module has been removed, and with it the
|
||||
corresponding `$touchProvider` and `$touch` service.
|
||||
|
||||
If you have included ngTouch v1.5.0 or higher in your application, and have not
|
||||
changed the value of `$touchProvider.ngClickOverrideEnabled()`, or injected and used the `$touch`
|
||||
service, then there are no migration steps for your code. Otherwise you must remove references to
|
||||
the provider and service.
|
||||
|
||||
The `ngClick` override directive had been deprecated and by default disabled since v1.5.0,
|
||||
because of buggy behavior in edge cases, and a general trend to avoid special touch based
|
||||
overrides of click events. In modern browsers, it should not be necessary to use a touch override
|
||||
library:
|
||||
|
||||
- Chrome, Firefox, Edge, and Safari remove the 300ms delay when
|
||||
`<meta name="viewport" content="width=device-width">` is set.
|
||||
- Internet Explorer 10+, Edge, Safari, and Chrome remove the delay on elements that have the
|
||||
`touch-action` css property is set to `manipulation`.
|
||||
|
||||
You can find out more in these articles:
|
||||
https://developers.google.com/web/updates/2013/12/300ms-tap-delay-gone-away
|
||||
https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_9_1.html#//apple_ref/doc/uid/TP40014305-CH10-SW8
|
||||
https://blogs.msdn.microsoft.com/ie/2015/02/24/pointer-events-w3c-recommendation-interoperable-touch-and-removing-the-dreaded-300ms-tap-delay/
|
||||
|
||||
|
||||
|
||||
## Migrating from 1.5 to 1.6
|
||||
|
||||
AngularJS 1.6 fixes numerous bugs and adds new features, both in core and in external modules.
|
||||
@@ -1272,7 +2030,7 @@ Due to [b71d7c3f](https://github.com/angular/angular.js/commit/b71d7c3f3c04e65b0
|
||||
falsy values (`''`, `0`, `false` and `null`) are properly recognized as option group identifiers for
|
||||
options passed to `ngOptions`. Previously, all of these values were ignored and the option was not
|
||||
assigned to any group. `undefined` is still interpreted as "no group".
|
||||
If you have options with falsy group indentifiers that should still not be assigned to any group,
|
||||
If you have options with falsy group identifiers that should still not be assigned to any group,
|
||||
then you must filter the values before passing them to `ngOptions`, converting falsy values to
|
||||
`undefined`.
|
||||
|
||||
@@ -1431,8 +2189,8 @@ For more info on the topic, you can take a look at this
|
||||
## Migrating from 1.3 to 1.4
|
||||
|
||||
AngularJS 1.4 fixes major animation issues and introduces a new API for `ngCookies`. Further, there
|
||||
are changes to `ngMessages`, `$compile`, `ngRepeat`, `ngOptions`, `ngPattern`, `pattern`, and
|
||||
some fixes to core filters: `limitTo` and `filter`.
|
||||
are changes to `ngMessages`, `$compile`, `ngRepeat`, `ngOptions`, `ngPattern`, `pattern` and some fixes to core filters:
|
||||
`limitTo` and `filter`.
|
||||
|
||||
The reason for the ngAnimate refactor was to fix timing issues and to expose new APIs to allow
|
||||
for developers to construct more versatile animations. We now have access to `$animateCss`
|
||||
|
||||
@@ -188,11 +188,6 @@ myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]);
|
||||
|
||||
Much simpler!
|
||||
|
||||
Note: Yes, we have called one of our service recipes 'Service'. We regret this and know that we'll
|
||||
be somehow punished for our misdeed. It's like we named one of our offspring 'Child'. Boy,
|
||||
that would mess with the teachers.
|
||||
|
||||
|
||||
## Provider Recipe
|
||||
|
||||
As already mentioned in the intro, the Provider recipe is the core recipe type and
|
||||
|
||||
@@ -109,12 +109,14 @@ to test the behavior without being distracted by the rendering details.
|
||||
## Scope Hierarchies
|
||||
|
||||
Each AngularJS application has exactly one {@link ng.$rootScope root scope}, but
|
||||
may have several child scopes.
|
||||
may have any number of child scopes.
|
||||
|
||||
The application can have multiple scopes, because some {@link guide/directive directives} create
|
||||
new child scopes (refer to directive documentation to see which directives create new scopes).
|
||||
When new scopes are created, they are added as children of their parent scope. This creates a tree
|
||||
structure which parallels the DOM where they're attached.
|
||||
The application can have multiple scopes, because {@link guide/directive directives} can create new
|
||||
child scopes. When new scopes are created, they are added as children of their parent scope. This
|
||||
creates a tree structure which parallels the DOM where they're attached.
|
||||
|
||||
The section {@link guide/scope#directives-that-create-scopes Directives that Create Scopes} has more
|
||||
info about which directives create scopes.
|
||||
|
||||
When AngularJS evaluates `{{name}}`, it first looks at the scope associated with the given
|
||||
element for the `name` property. If no such property is found, it searches the parent scope
|
||||
@@ -183,8 +185,11 @@ To examine the scope in the debugger:
|
||||
2. The debugger allows you to access the currently selected element in the console as `$0`
|
||||
variable.
|
||||
|
||||
3. To retrieve the associated scope in console execute: `angular.element($0).scope()` or just type $scope
|
||||
3. To retrieve the associated scope in console execute: `angular.element($0).scope()`
|
||||
|
||||
<div class="alert alert-warning">
|
||||
The `scope()` function is only available when {@link ng.$compileProvider#debugInfoEnabled `$compileProvider.debugInfoEnabled()`} is true (which is the default).
|
||||
</div>
|
||||
|
||||
## Scope Events Propagation
|
||||
|
||||
@@ -309,12 +314,16 @@ correctly.
|
||||
|
||||
In most cases, {@link ng.$compileProvider#directive directives} and scopes interact
|
||||
but do not create new instances of scope. However, some directives, such as {@link
|
||||
ng.directive:ngController ng-controller} and {@link
|
||||
ng.directive:ngRepeat ng-repeat}, create new child scopes
|
||||
and attach the child scope to the corresponding DOM element. You can retrieve a scope for any DOM
|
||||
element by using an `angular.element(aDomElement).scope()` method call.
|
||||
See the {@link guide/directive#isolating-the-scope-of-a-directive
|
||||
directives guide} for more information about isolate scopes.
|
||||
ng.directive:ngController ng-controller} and {@link ng.directive:ngRepeat ng-repeat},
|
||||
create new child scopes and attach the child scope to the corresponding DOM element.
|
||||
|
||||
A special type of scope is the `isolate` scope, which does not inherit prototypically from the parent scope.
|
||||
This type of scope is useful for component directives that should be isolated from their parent scope.
|
||||
See the {@link guide/directive#isolating-the-scope-of-a-directive directives guide} for
|
||||
more information about isolate scopes in custom directives.
|
||||
|
||||
Note also that component directives, which are created with the
|
||||
{@link api/ng/type/angular.Module#component .component()} helper always create an isolate scope.
|
||||
|
||||
### Controllers and Scopes
|
||||
|
||||
|
||||
@@ -149,16 +149,17 @@ for instantiating controllers.
|
||||
describe('PasswordController', function() {
|
||||
beforeEach(module('app'));
|
||||
|
||||
var $controller;
|
||||
var $controller, $rootScope;
|
||||
|
||||
beforeEach(inject(function(_$controller_){
|
||||
beforeEach(inject(function(_$controller_, _$rootScope_){
|
||||
// The injector unwraps the underscores (_) from around the parameter names when matching
|
||||
$controller = _$controller_;
|
||||
$rootScope = _$rootScope_;
|
||||
}));
|
||||
|
||||
describe('$scope.grade', function() {
|
||||
it('sets the strength to "strong" if the password length is >8 chars', function() {
|
||||
var $scope = {};
|
||||
var $scope = $rootScope.$new();
|
||||
var controller = $controller('PasswordController', { $scope: $scope });
|
||||
$scope.password = 'longerthaneightchars';
|
||||
$scope.grade();
|
||||
|
||||
@@ -111,7 +111,7 @@ The set of files included in each version directory are:
|
||||
* __`angular-route.js`__ — Routing and deep-linking services and directives for AngularJS apps.
|
||||
({@link module:ngRoute API docs})
|
||||
* __`angular-sanitize.js`__ — Functionality to sanitize HTML. ({@link module:ngSanitize API docs})
|
||||
* __`angular-touch.js`__ — Touch events and other helpers for touch-enabled devices.
|
||||
* __`angular-touch.js`__ — Touch events for touch-enabled devices.
|
||||
({@link module:ngTouch API docs})
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
### Why is this project called "AngularJS"? Why is the namespace called "ng"?
|
||||
|
||||
Because HTML has AngularJS brackets and "ng" sounds like "AngularJS".
|
||||
Because HTML has angular brackets and "ng" sounds like "AngularJS".
|
||||
|
||||
|
||||
### Is AngularJS a library, framework, plugin or a browser extension?
|
||||
@@ -39,7 +39,7 @@ When we are making a release we generate updates to the changelog directly from
|
||||
generated update contains a highlighted section that contains all the breaking changes that have been
|
||||
extracted from the commits. We can quickly see in the new changelog exactly what commits contain breaking
|
||||
changes and so can application developers when they are deciding whether to update to a new version of
|
||||
Angular.
|
||||
AngularJS.
|
||||
|
||||
Features with non-breaking changes can also appear in the "patch" version, e.g. in version 1.6.3 there might
|
||||
be a feature that is not available in 1.6.2.
|
||||
@@ -94,8 +94,7 @@ When adding new code to AngularJS, we have a very stringent commit policy:
|
||||
- Commit messages must be written in a specific manner that allows us to parse them and extract the changes
|
||||
for release notes ([see the contributing guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md))
|
||||
|
||||
The AngularJS code base has a very large set of unit tests and end-to-end tests. This means that
|
||||
a breaking change will require one or more tests to be changed to allow the
|
||||
The AngularJS code base has a very large set of unit tests and end-to-end tests. This means that a breaking change will require one or more tests to be changed to allow the
|
||||
tests to pass. So when a commit includes tests that are being removed or modified, this is a flag that the
|
||||
code might include a breaking change. When reviewing the commit we can then decide whether there really is
|
||||
a breaking change and if it is appropriate for the branch to which it is being merged. If so, then we
|
||||
|
||||
@@ -151,38 +151,20 @@ To learn more about AngularJS scopes, see the {@link ng.$rootScope.Scope Angular
|
||||
|
||||
### Testing Controllers
|
||||
|
||||
The "AngularJS way" of separating controller from the view, makes it easy to test code as it is being
|
||||
developed. If our controller were available on the global namespace, we could simply instantiate it
|
||||
with a mock scope object:
|
||||
The "AngularJS way" of separating the controller from the view makes it easy to test code as it is being
|
||||
developed. In the section "Model and Controller" we have registered our controller via a constructor
|
||||
function on the `phonecatApp` module.
|
||||
|
||||
<br />
|
||||
```js
|
||||
describe('PhoneListController', function() {
|
||||
In tests, we use an AngularJS service, `$controller`, which will retrieve a controller by name. It
|
||||
also takes a second argument - a map of dependencies that should be injected.
|
||||
|
||||
it('should create a `phones` model with 3 phones', function() {
|
||||
var scope = {};
|
||||
var ctrl = new PhoneListController(scope);
|
||||
The following test instantiates `PhoneListController` with a mock scope object,
|
||||
and verifies that the phones array property on the scope contains three records.
|
||||
|
||||
expect(scope.phones.length).toBe(3);
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
The test instantiates `PhoneListController` and verifies that the phones array property on the
|
||||
scope contains three records. This example demonstrates how easy it is to create a unit test for
|
||||
This example demonstrates how easy it is to create a unit test for
|
||||
code in AngularJS. Since testing is such a critical part of software development, we make it easy to
|
||||
create tests in AngularJS so that developers are encouraged to write them.
|
||||
|
||||
|
||||
### Testing non-global Controllers
|
||||
|
||||
In practice, you will not want to have your controller functions in the global namespace. Instead,
|
||||
you can see that we have registered it via a constructor function on the `phonecatApp` module.
|
||||
|
||||
In this case AngularJS provides a service, `$controller`, which will retrieve your controller by name.
|
||||
Here is the same test using `$controller`:
|
||||
|
||||
<br />
|
||||
**`app/app.spec.js`:**
|
||||
|
||||
|
||||
@@ -119,19 +119,19 @@ module.exports = function(config, specificOptions) {
|
||||
base: 'BrowserStack',
|
||||
browser: 'chrome',
|
||||
os: 'OS X',
|
||||
os_version: 'Yosemite'
|
||||
os_version: 'Sierra'
|
||||
},
|
||||
'BS_Safari': {
|
||||
base: 'BrowserStack',
|
||||
browser: 'safari',
|
||||
os: 'OS X',
|
||||
os_version: 'Yosemite'
|
||||
os_version: 'Sierra'
|
||||
},
|
||||
'BS_Firefox': {
|
||||
base: 'BrowserStack',
|
||||
browser: 'firefox',
|
||||
os: 'Windows',
|
||||
os_version: '8'
|
||||
os_version: '10'
|
||||
},
|
||||
'BS_IE_9': {
|
||||
base: 'BrowserStack',
|
||||
|
||||
+4
-5
@@ -2,8 +2,8 @@
|
||||
"name": "angularjs",
|
||||
"license": "MIT",
|
||||
"branchVersion": "^1.6.0",
|
||||
"branchPattern": "1.6.*",
|
||||
"distTag": "previous_1_6",
|
||||
"branchPattern": "1.7.*",
|
||||
"distTag": "latest",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/angular.js.git"
|
||||
@@ -71,7 +71,6 @@
|
||||
"karma-ie-launcher": "^1.0.0",
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-junit-reporter": "^1.2.0",
|
||||
"karma-ng-scenario": "^1.0.0",
|
||||
"karma-safari-launcher": "^1.0.0",
|
||||
"karma-sauce-launcher": "^1.2.0",
|
||||
"karma-script-launcher": "^1.0.0",
|
||||
@@ -85,13 +84,13 @@
|
||||
"npm-run": "^4.1.0",
|
||||
"open-sans-fontface": "^1.4.0",
|
||||
"promises-aplus-tests": "~2.1.0",
|
||||
"protractor": "^5.4.1",
|
||||
"protractor": "^5.1.2",
|
||||
"q": "~1.0.0",
|
||||
"q-io": "^1.10.9",
|
||||
"qq": "^0.3.5",
|
||||
"rewire": "~2.1.0",
|
||||
"sax": "^1.1.1",
|
||||
"selenium-webdriver": "^4.0.0-alpha.1",
|
||||
"selenium-webdriver": "^2.53.1",
|
||||
"semver": "^5.4.1",
|
||||
"serve-favicon": "^2.3.0",
|
||||
"serve-index": "^1.8.0",
|
||||
|
||||
@@ -12,7 +12,7 @@ set -xe
|
||||
# This is the default set of browsers to use on the CI server unless overridden via env variable
|
||||
if [[ -z "$BROWSERS" ]]
|
||||
then
|
||||
BROWSERS="Chrome,Firefox,/Users/jenkins/bin/safari.sh"
|
||||
BROWSERS="Chrome"
|
||||
fi
|
||||
|
||||
# CLEAN #
|
||||
|
||||
+1
-2
@@ -28,8 +28,6 @@
|
||||
"REGEX_STRING_REGEXP" : false,
|
||||
"lowercase": false,
|
||||
"uppercase": false,
|
||||
"manualLowercase": false,
|
||||
"manualUppercase": false,
|
||||
"isArrayLike": false,
|
||||
"forEach": false,
|
||||
"forEachSorted": false,
|
||||
@@ -161,6 +159,7 @@
|
||||
/* urlUtils.js */
|
||||
"urlResolve": false,
|
||||
"urlIsSameOrigin": false,
|
||||
"urlIsSameOriginAsBaseUrl": false,
|
||||
"urlIsAllowedOriginFactory": false,
|
||||
|
||||
/* ng/controller.js */
|
||||
|
||||
+24
-68
@@ -21,8 +21,6 @@
|
||||
|
||||
lowercase,
|
||||
uppercase,
|
||||
manualLowercase,
|
||||
manualUppercase,
|
||||
nodeName_,
|
||||
isArrayLike,
|
||||
forEach,
|
||||
@@ -130,15 +128,7 @@ var VALIDITY_STATE_PROPERTY = 'validity';
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name angular.lowercase
|
||||
* @module ng
|
||||
* @kind function
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.5.0"
|
||||
* removeVersion="1.7.0"
|
||||
* Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead.
|
||||
* @private
|
||||
*
|
||||
* @description Converts the specified string to lowercase.
|
||||
* @param {string} string String to be converted to lowercase.
|
||||
@@ -147,15 +137,7 @@ var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name angular.uppercase
|
||||
* @module ng
|
||||
* @kind function
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.5.0"
|
||||
* removeVersion="1.7.0"
|
||||
* Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead.
|
||||
* @private
|
||||
*
|
||||
* @description Converts the specified string to uppercase.
|
||||
* @param {string} string String to be converted to uppercase.
|
||||
@@ -164,31 +146,6 @@ var lowercase = function(string) {return isString(string) ? string.toLowerCase()
|
||||
var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
|
||||
|
||||
|
||||
var manualLowercase = function(s) {
|
||||
/* eslint-disable no-bitwise */
|
||||
return isString(s)
|
||||
? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);})
|
||||
: s;
|
||||
/* eslint-enable */
|
||||
};
|
||||
var manualUppercase = function(s) {
|
||||
/* eslint-disable no-bitwise */
|
||||
return isString(s)
|
||||
? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);})
|
||||
: s;
|
||||
/* eslint-enable */
|
||||
};
|
||||
|
||||
|
||||
// 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. See https://github.com/angular/angular.js/issues/11387
|
||||
if ('i' !== 'I'.toLowerCase()) {
|
||||
lowercase = manualLowercase;
|
||||
uppercase = manualUppercase;
|
||||
}
|
||||
|
||||
|
||||
var
|
||||
msie, // holds major version number for IE, or NaN if UA is not IE.
|
||||
jqLite, // delay binding since jQuery could be loaded after us.
|
||||
@@ -236,8 +193,7 @@ function isArrayLike(obj) {
|
||||
|
||||
// NodeList objects (with `item` method) and
|
||||
// other objects with suitable length characteristics are array-like
|
||||
return isNumber(length) &&
|
||||
(length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function');
|
||||
return isNumber(length) && (length >= 0 && (length - 1) in obj || typeof obj.item === 'function');
|
||||
|
||||
}
|
||||
|
||||
@@ -386,10 +342,8 @@ function baseExtend(dst, objs, deep) {
|
||||
} else if (isElement(src)) {
|
||||
dst[key] = src.clone();
|
||||
} else {
|
||||
if (key !== '__proto__') {
|
||||
if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
|
||||
baseExtend(dst[key], [src], true);
|
||||
}
|
||||
if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
|
||||
baseExtend(dst[key], [src], true);
|
||||
}
|
||||
} else {
|
||||
dst[key] = src;
|
||||
@@ -654,12 +608,14 @@ function isDate(value) {
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Determines if a reference is an `Array`. Alias of Array.isArray.
|
||||
* Determines if a reference is an `Array`.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is an `Array`.
|
||||
*/
|
||||
var isArray = Array.isArray;
|
||||
function isArray(arr) {
|
||||
return Array.isArray(arr) || arr instanceof Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
@@ -1942,25 +1898,25 @@ function bindJQuery() {
|
||||
injector: JQLitePrototype.injector,
|
||||
inheritedData: JQLitePrototype.inheritedData
|
||||
});
|
||||
|
||||
// All nodes removed from the DOM via various jQuery APIs like .remove()
|
||||
// are passed through jQuery.cleanData. Monkey-patch this method to fire
|
||||
// the $destroy event on all removed nodes.
|
||||
originalCleanData = jQuery.cleanData;
|
||||
jQuery.cleanData = function(elems) {
|
||||
var events;
|
||||
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
|
||||
events = jQuery._data(elem, 'events');
|
||||
if (events && events.$destroy) {
|
||||
jQuery(elem).triggerHandler('$destroy');
|
||||
}
|
||||
}
|
||||
originalCleanData(elems);
|
||||
};
|
||||
} else {
|
||||
jqLite = JQLite;
|
||||
}
|
||||
|
||||
// All nodes removed from the DOM via various jqLite/jQuery APIs like .remove()
|
||||
// are passed through jqLite/jQuery.cleanData. Monkey-patch this method to fire
|
||||
// the $destroy event on all removed nodes.
|
||||
originalCleanData = jqLite.cleanData;
|
||||
jqLite.cleanData = function(elems) {
|
||||
var events;
|
||||
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
|
||||
events = jqLite._data(elem).events;
|
||||
if (events && events.$destroy) {
|
||||
jqLite(elem).triggerHandler('$destroy');
|
||||
}
|
||||
}
|
||||
originalCleanData(elems);
|
||||
};
|
||||
|
||||
angular.element = jqLite;
|
||||
|
||||
// Prevent double-proxying.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
htmlAnchorDirective,
|
||||
inputDirective,
|
||||
hiddenInputBrowserCacheDirective,
|
||||
inputDirective,
|
||||
formDirective,
|
||||
scriptDirective,
|
||||
selectDirective,
|
||||
@@ -150,8 +150,6 @@ function publishExternalAPI(angular) {
|
||||
'isArray': isArray,
|
||||
'version': version,
|
||||
'isDate': isDate,
|
||||
'lowercase': lowercase,
|
||||
'uppercase': uppercase,
|
||||
'callbacks': {$$counter: 0},
|
||||
'getTestability': getTestability,
|
||||
'reloadWithDebugInfo': reloadWithDebugInfo,
|
||||
@@ -159,7 +157,9 @@ function publishExternalAPI(angular) {
|
||||
'$$csp': csp,
|
||||
'$$encodeUriSegment': encodeUriSegment,
|
||||
'$$encodeUriQuery': encodeUriQuery,
|
||||
'$$stringify': stringify
|
||||
'$$lowercase': lowercase,
|
||||
'$$stringify': stringify,
|
||||
'$$uppercase': uppercase
|
||||
});
|
||||
|
||||
angularModule = setupModuleLoader(window);
|
||||
@@ -217,8 +217,7 @@ function publishExternalAPI(angular) {
|
||||
ngModelOptions: ngModelOptionsDirective
|
||||
}).
|
||||
directive({
|
||||
ngInclude: ngIncludeFillContentDirective,
|
||||
input: hiddenInputBrowserCacheDirective
|
||||
ngInclude: ngIncludeFillContentDirective
|
||||
}).
|
||||
directive(ngAttributeAliasDirectives).
|
||||
directive(ngEventDirectives);
|
||||
|
||||
@@ -922,9 +922,7 @@ function createInjector(modulesToLoad, strictDi) {
|
||||
}
|
||||
var result = func.$$ngIsClass;
|
||||
if (!isBoolean(result)) {
|
||||
// Support: Edge 12-13 only
|
||||
// See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/
|
||||
result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func));
|
||||
result = func.$$ngIsClass = /^class\b/.test(stringifyFn(func));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+28
-9
@@ -311,6 +311,28 @@ function jqLiteDealoc(element, onlyDescendants) {
|
||||
}
|
||||
}
|
||||
|
||||
function isEmptyObject(obj) {
|
||||
var name;
|
||||
|
||||
for (name in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeIfEmptyData(element) {
|
||||
var expandoId = element.ng339;
|
||||
var expandoStore = expandoId && jqCache[expandoId];
|
||||
|
||||
var events = expandoStore && expandoStore.events;
|
||||
var data = expandoStore && expandoStore.data;
|
||||
|
||||
if ((!data || isEmptyObject(data)) && (!events || isEmptyObject(events))) {
|
||||
delete jqCache[expandoId];
|
||||
element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it
|
||||
}
|
||||
}
|
||||
|
||||
function jqLiteOff(element, type, fn, unsupported) {
|
||||
if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument');
|
||||
|
||||
@@ -347,6 +369,8 @@ function jqLiteOff(element, type, fn, unsupported) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeIfEmptyData(element);
|
||||
}
|
||||
|
||||
function jqLiteRemoveData(element, name) {
|
||||
@@ -356,17 +380,11 @@ function jqLiteRemoveData(element, name) {
|
||||
if (expandoStore) {
|
||||
if (name) {
|
||||
delete expandoStore.data[name];
|
||||
return;
|
||||
} else {
|
||||
expandoStore.data = {};
|
||||
}
|
||||
|
||||
if (expandoStore.handle) {
|
||||
if (expandoStore.events.$destroy) {
|
||||
expandoStore.handle({}, '$destroy');
|
||||
}
|
||||
jqLiteOff(element);
|
||||
}
|
||||
delete jqCache[expandoId];
|
||||
element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it
|
||||
removeIfEmptyData(element);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,6 +634,7 @@ forEach({
|
||||
cleanData: function jqLiteCleanData(nodes) {
|
||||
for (var i = 0, ii = nodes.length; i < ii; i++) {
|
||||
jqLiteRemoveData(nodes[i]);
|
||||
jqLiteOff(nodes[i]);
|
||||
}
|
||||
}
|
||||
}, function(fn, name) {
|
||||
|
||||
+2
-1
@@ -328,7 +328,8 @@ function setupModuleLoader(window) {
|
||||
* @ngdoc method
|
||||
* @name angular.Module#component
|
||||
* @module ng
|
||||
* @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
|
||||
* @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`),
|
||||
* or an object map of components where the keys are the names and the values are the component definition objects.
|
||||
* @param {Object} options Component definition object (a simplified
|
||||
* {@link ng.$compile#directive-definition-object directive definition object})
|
||||
*
|
||||
|
||||
+75
-11
@@ -464,13 +464,77 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* @ngdoc method
|
||||
* @name $animate#cancel
|
||||
* @kind function
|
||||
* @description Cancels the provided animation.
|
||||
* @description Cancels the provided animation and applies the end state of the animation.
|
||||
* Note that this does not cancel the underlying operation, e.g. the setting of classes or
|
||||
* adding the element to the DOM.
|
||||
*
|
||||
* @param {Promise} animationPromise The animation promise that is returned when an animation is started.
|
||||
* @param {animationRunner} animationRunner An animation runner returned by an $animate function.
|
||||
*
|
||||
* @example
|
||||
<example module="animationExample" deps="angular-animate.js" animations="true" name="animate-cancel">
|
||||
<file name="app.js">
|
||||
angular.module('animationExample', ['ngAnimate']).component('cancelExample', {
|
||||
templateUrl: 'template.html',
|
||||
controller: function($element, $animate) {
|
||||
this.runner = null;
|
||||
|
||||
this.addClass = function() {
|
||||
this.runner = $animate.addClass($element.find('div'), 'red');
|
||||
var ctrl = this;
|
||||
this.runner.finally(function() {
|
||||
ctrl.runner = null;
|
||||
});
|
||||
};
|
||||
|
||||
this.removeClass = function() {
|
||||
this.runner = $animate.removeClass($element.find('div'), 'red');
|
||||
var ctrl = this;
|
||||
this.runner.finally(function() {
|
||||
ctrl.runner = null;
|
||||
});
|
||||
};
|
||||
|
||||
this.cancel = function() {
|
||||
$animate.cancel(this.runner);
|
||||
};
|
||||
}
|
||||
});
|
||||
</file>
|
||||
<file name="template.html">
|
||||
<p>
|
||||
<button id="add" ng-click="$ctrl.addClass()">Add</button>
|
||||
<button ng-click="$ctrl.removeClass()">Remove</button>
|
||||
<br>
|
||||
<button id="cancel" ng-click="$ctrl.cancel()" ng-disabled="!$ctrl.runner">Cancel</button>
|
||||
<br>
|
||||
<div id="target">CSS-Animated Text</div>
|
||||
</p>
|
||||
</file>
|
||||
<file name="index.html">
|
||||
<cancel-example></cancel-example>
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.red-add, .red-remove {
|
||||
transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
|
||||
}
|
||||
|
||||
.red,
|
||||
.red-add.red-add-active {
|
||||
color: #FF0000;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.red-remove.red-remove-active {
|
||||
font-size: 10px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
cancel: function(runner) {
|
||||
if (runner.end) {
|
||||
runner.end();
|
||||
if (runner.cancel) {
|
||||
runner.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -496,7 +560,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Promise} the animation callback promise
|
||||
* @return {Runner} the animation runner
|
||||
*/
|
||||
enter: function(element, parent, after, options) {
|
||||
parent = parent && jqLite(parent);
|
||||
@@ -528,7 +592,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Promise} the animation callback promise
|
||||
* @return {Runner} the animation runner
|
||||
*/
|
||||
move: function(element, parent, after, options) {
|
||||
parent = parent && jqLite(parent);
|
||||
@@ -555,7 +619,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Promise} the animation callback promise
|
||||
* @return {Runner} the animation runner
|
||||
*/
|
||||
leave: function(element, options) {
|
||||
return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
|
||||
@@ -585,7 +649,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Promise} the animation callback promise
|
||||
* @return {Runner} animationRunner the animation runner
|
||||
*/
|
||||
addClass: function(element, className, options) {
|
||||
options = prepareAnimateOptions(options);
|
||||
@@ -615,7 +679,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Promise} the animation callback promise
|
||||
* @return {Runner} the animation runner
|
||||
*/
|
||||
removeClass: function(element, className, options) {
|
||||
options = prepareAnimateOptions(options);
|
||||
@@ -646,7 +710,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Promise} the animation callback promise
|
||||
* @return {Runner} the animation runner
|
||||
*/
|
||||
setClass: function(element, add, remove, options) {
|
||||
options = prepareAnimateOptions(options);
|
||||
@@ -693,7 +757,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Promise} the animation callback promise
|
||||
* @return {Runner} the animation runner
|
||||
*/
|
||||
animate: function(element, from, to, className, options) {
|
||||
options = prepareAnimateOptions(options);
|
||||
|
||||
+1
-2
@@ -172,8 +172,7 @@ function Browser(window, document, $log, $sniffer) {
|
||||
// - pendingLocation is needed as browsers don't allow to read out
|
||||
// the new location.href if a reload happened or if there is a bug like in iOS 9 (see
|
||||
// https://openradar.appspot.com/22186109).
|
||||
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
|
||||
return pendingLocation || location.href.replace(/%27/g,'\'');
|
||||
return pendingLocation || location.href;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+90
-111
@@ -303,21 +303,22 @@
|
||||
* name. Given `<my-component my-attr="parentModel">` and the isolate scope definition `scope: {
|
||||
* localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the
|
||||
* value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in
|
||||
* `localModel` and vice versa. Optional attributes should be marked as such with a question mark:
|
||||
* `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't
|
||||
* optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`})
|
||||
* will be thrown upon discovering changes to the local value, since it will be impossible to sync
|
||||
* them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`}
|
||||
* `localModel` and vice versa. If the binding expression is non-assignable, or if the attribute
|
||||
* isn't optional and doesn't exist, an exception
|
||||
* ({@link error/$compile/nonassign `$compile:nonassign`}) will be thrown upon discovering changes
|
||||
* to the local value, since it will be impossible to sync them back to the parent scope.
|
||||
*
|
||||
* By default, the {@link ng.$rootScope.Scope#$watch `$watch`}
|
||||
* method is used for tracking changes, and the equality check is based on object identity.
|
||||
* However, if an object literal or an array literal is passed as the binding expression, the
|
||||
* equality check is done by value (using the {@link angular.equals} function). It's also possible
|
||||
* to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection
|
||||
* `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional).
|
||||
* `$watchCollection`}: use `=*` or `=*attr`
|
||||
*
|
||||
* * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an
|
||||
* expression passed via the attribute `attr`. The expression is evaluated in the context of the
|
||||
* parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the
|
||||
* local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`.
|
||||
* local name.
|
||||
*
|
||||
* For example, given `<my-component my-attr="parentModel">` and directive definition of
|
||||
* `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the
|
||||
@@ -347,6 +348,36 @@
|
||||
* and values into the expression wrapper fn. For example, if the expression is `increment(amount)`
|
||||
* then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`.
|
||||
*
|
||||
* All 4 kinds of bindings (`@`, `=`, `<`, and `&`) can be made optional by adding `?` to the expression.
|
||||
* The marker must come after the mode and before the attribute name.
|
||||
* See the {@link error/$compile/iscp Invalid Isolate Scope Definition error} for definition examples.
|
||||
* This is useful to refine the interface directives provide.
|
||||
* One subtle difference between optional and non-optional happens **when the binding attribute is not
|
||||
* set**:
|
||||
* - the binding is optional: the property will not be defined
|
||||
* - the binding is not optional: the property is defined
|
||||
*
|
||||
* ```js
|
||||
*app.directive('testDir', function() {
|
||||
return {
|
||||
scope: {
|
||||
notoptional: '=',
|
||||
optional: '=?',
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function() {
|
||||
this.$onInit = function() {
|
||||
console.log(this.hasOwnProperty('notoptional')) // true
|
||||
console.log(this.hasOwnProperty('optional')) // false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
*```
|
||||
*
|
||||
*
|
||||
* ##### Combining directives with different scope defintions
|
||||
*
|
||||
* In general it's possible to apply more than one directive to one element, but there might be limitations
|
||||
* depending on the type of scope required by the directives. The following points will help explain these limitations.
|
||||
* For simplicity only two directives are taken into account, but it is also applicable for several directives:
|
||||
@@ -374,12 +405,6 @@
|
||||
* `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings
|
||||
* initialized.
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* **Deprecation warning:** if `$compileProcvider.preAssignBindingsEnabled(true)` was called, bindings for non-ES6 class
|
||||
* controllers are bound to `this` before the controller constructor is called but this use is now deprecated. Please
|
||||
* place initialization code that relies upon bindings inside a `$onInit` method on the controller, instead.
|
||||
* </div>
|
||||
*
|
||||
* It is also possible to set `bindToController` to an object hash with the same format as the `scope` property.
|
||||
* This will set up the scope bindings to the controller directly. Note that `scope` can still be used
|
||||
* to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate
|
||||
@@ -1442,56 +1467,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
return debugInfoEnabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $compileProvider#preAssignBindingsEnabled
|
||||
*
|
||||
* @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the
|
||||
* current preAssignBindingsEnabled state
|
||||
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
||||
*
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Call this method to enable/disable whether directive controllers are assigned bindings before
|
||||
* calling the controller's constructor.
|
||||
* If enabled (true), the compiler assigns the value of each of the bindings to the
|
||||
* properties of the controller object before the constructor of this object is called.
|
||||
*
|
||||
* If disabled (false), the compiler calls the constructor first before assigning bindings.
|
||||
*
|
||||
* The default value is false.
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.6.0"
|
||||
* removeVersion="1.7.0"
|
||||
*
|
||||
* This method and the option to assign the bindings before calling the controller's constructor
|
||||
* will be removed in v1.7.0.
|
||||
*/
|
||||
var preAssignBindingsEnabled = false;
|
||||
this.preAssignBindingsEnabled = function(enabled) {
|
||||
if (isDefined(enabled)) {
|
||||
preAssignBindingsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
return preAssignBindingsEnabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $compileProvider#strictComponentBindingsEnabled
|
||||
*
|
||||
* @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, otherwise just return the
|
||||
* current strictComponentBindingsEnabled state
|
||||
* @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided,
|
||||
* otherwise return the current strictComponentBindingsEnabled state.
|
||||
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
||||
*
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Call this method to enable/disable strict component bindings check. If enabled, the compiler will enforce that
|
||||
* for all bindings of a component that are not set as optional with `?`, an attribute needs to be provided
|
||||
* on the component's HTML tag.
|
||||
* Call this method to enable / disable the strict component bindings check. If enabled, the
|
||||
* compiler will enforce that all scope / controller bindings of a
|
||||
* {@link $compileProvider#directive directive} / {@link $compileProvider#component component}
|
||||
* that are not set as optional with `?`, must be provided when the directive is instantiated.
|
||||
* If not provided, the compiler will throw the
|
||||
* {@link error/$compile/missingattr $compile:missingattr error}.
|
||||
*
|
||||
* The default value is false.
|
||||
*/
|
||||
@@ -1592,9 +1584,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
|
||||
this.$get = [
|
||||
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
|
||||
'$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
|
||||
'$controller', '$rootScope', '$sce', '$animate',
|
||||
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
|
||||
$controller, $rootScope, $sce, $animate, $$sanitizeUri) {
|
||||
$controller, $rootScope, $sce, $animate) {
|
||||
|
||||
var SIMPLE_ATTR_NAME = /^\w/;
|
||||
var specialAttrHolder = window.document.createElement('div');
|
||||
@@ -1739,8 +1731,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
*/
|
||||
$set: function(key, value, writeAttr, attrName) {
|
||||
// TODO: decide whether or not to throw an error if "class"
|
||||
//is set through this function since it may cause $updateClass to
|
||||
//become unstable.
|
||||
// is set through this function since it may cause $updateClass to
|
||||
// become unstable.
|
||||
|
||||
var node = this.$$element[0],
|
||||
booleanKey = getBooleanAttrName(node, key),
|
||||
@@ -1770,12 +1762,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
|
||||
nodeName = nodeName_(this.$$element);
|
||||
|
||||
if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
|
||||
(nodeName === 'img' && key === 'src')) {
|
||||
// sanitize a[href] and img[src] values
|
||||
this[key] = value = (value == null) ? value : $$sanitizeUri(value, key === 'src');
|
||||
} else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
|
||||
// sanitize img[srcset] values
|
||||
// Sanitize img[srcset] values.
|
||||
if (nodeName === 'img' && key === 'srcset' && value) {
|
||||
if (!isString(value)) {
|
||||
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
|
||||
}
|
||||
|
||||
// Such values are a bit too complex to handle automatically inside $sce.
|
||||
// Instead, we sanitize each of the URIs individually, which works, even dynamically.
|
||||
|
||||
// It's not possible to work around this using `$sce.trustAsMediaUrl`.
|
||||
// If you want to programmatically set explicitly trusted unsafe URLs, you should use
|
||||
// `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
|
||||
// `ng-bind-html` directive.
|
||||
|
||||
var result = '';
|
||||
|
||||
// first check if there are spaces because it's not the same pattern
|
||||
@@ -1792,16 +1792,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
for (var i = 0; i < nbrUrisWith2parts; i++) {
|
||||
var innerIdx = i * 2;
|
||||
// sanitize the uri
|
||||
result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
|
||||
result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
|
||||
// add the descriptor
|
||||
result += (' ' + trim(rawUris[innerIdx + 1]));
|
||||
result += ' ' + trim(rawUris[innerIdx + 1]);
|
||||
}
|
||||
|
||||
// split the last item into uri and descriptor
|
||||
var lastTuple = trim(rawUris[i * 2]).split(/\s/);
|
||||
|
||||
// sanitize the last uri
|
||||
result += $$sanitizeUri(trim(lastTuple[0]), true);
|
||||
result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
|
||||
|
||||
// and add the last descriptor if any
|
||||
if (lastTuple.length === 2) {
|
||||
@@ -1811,7 +1811,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
|
||||
if (writeAttr !== false) {
|
||||
if (value == null) {
|
||||
if (value === null || isUndefined(value)) {
|
||||
this.$$element.removeAttr(attrName);
|
||||
} else {
|
||||
if (SIMPLE_ATTR_NAME.test(attrName)) {
|
||||
@@ -2549,17 +2549,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
compileNode = $compileNode[0];
|
||||
replaceWith(jqCollection, sliceArgs($template), compileNode);
|
||||
|
||||
// Support: Chrome < 50
|
||||
// https://github.com/angular/angular.js/issues/14041
|
||||
|
||||
// In the versions of V8 prior to Chrome 50, the document fragment that is created
|
||||
// in the `replaceWith` function is improperly garbage collected despite still
|
||||
// being referenced by the `parentNode` property of all of the child nodes. By adding
|
||||
// a reference to the fragment via a different property, we can avoid that incorrect
|
||||
// behavior.
|
||||
// TODO: remove this line after Chrome 50 has been released
|
||||
$template[0].$$parentNode = $template[0].parentNode;
|
||||
|
||||
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
|
||||
replaceDirective && replaceDirective.name, {
|
||||
// Don't pass in:
|
||||
@@ -2819,33 +2808,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
var controller = elementControllers[name];
|
||||
var bindings = controllerDirective.$$bindings.bindToController;
|
||||
|
||||
if (preAssignBindingsEnabled) {
|
||||
if (bindings) {
|
||||
controller.bindingInfo =
|
||||
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
|
||||
} else {
|
||||
controller.bindingInfo = {};
|
||||
}
|
||||
|
||||
var controllerResult = controller();
|
||||
if (controllerResult !== controller.instance) {
|
||||
// If the controller constructor has a return value, overwrite the instance
|
||||
// from setupControllers
|
||||
controller.instance = controllerResult;
|
||||
$element.data('$' + controllerDirective.name + 'Controller', controllerResult);
|
||||
if (controller.bindingInfo.removeWatches) {
|
||||
controller.bindingInfo.removeWatches();
|
||||
}
|
||||
controller.bindingInfo =
|
||||
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
|
||||
}
|
||||
} else {
|
||||
controller.instance = controller();
|
||||
$element.data('$' + controllerDirective.name + 'Controller', controller.instance);
|
||||
controller.bindingInfo =
|
||||
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
|
||||
controller.instance = controller();
|
||||
$element.data('$' + controllerDirective.name + 'Controller', controller.instance);
|
||||
controller.bindingInfo =
|
||||
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy
|
||||
forEach(controllerDirectives, function(controllerDirective, name) {
|
||||
@@ -3349,18 +3316,30 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
var tag = nodeName_(node);
|
||||
// All tags with src attributes require a RESOURCE_URL value, except for
|
||||
// img and various html5 media tags.
|
||||
// img and various html5 media tags, which require the MEDIA_URL context.
|
||||
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
|
||||
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
|
||||
return $sce.RESOURCE_URL;
|
||||
}
|
||||
// maction[xlink:href] can source SVG. It's not limited to <maction>.
|
||||
} else if (attrNormalizedName === 'xlinkHref' ||
|
||||
return $sce.MEDIA_URL;
|
||||
} else if (attrNormalizedName === 'xlinkHref') {
|
||||
// Some xlink:href are okay, most aren't
|
||||
if (tag === 'image') return $sce.MEDIA_URL;
|
||||
if (tag === 'a') return $sce.URL;
|
||||
return $sce.RESOURCE_URL;
|
||||
} else if (
|
||||
// Formaction
|
||||
(tag === 'form' && attrNormalizedName === 'action') ||
|
||||
// If relative URLs can go where they are not expected to, then
|
||||
// all sorts of trust issues can arise.
|
||||
(tag === 'base' && attrNormalizedName === 'href') ||
|
||||
// links can be stylesheets or imports, which can run script in the current origin
|
||||
(tag === 'link' && attrNormalizedName === 'href')
|
||||
) {
|
||||
return $sce.RESOURCE_URL;
|
||||
} else if (tag === 'a' && (attrNormalizedName === 'href' ||
|
||||
attrNormalizedName === 'ngHref')) {
|
||||
return $sce.URL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3631,21 +3610,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
if (optional && !attrs[attrName]) break;
|
||||
|
||||
parentGet = $parse(attrs[attrName]);
|
||||
var deepWatch = parentGet.literal;
|
||||
var isLiteral = parentGet.literal;
|
||||
|
||||
var initialValue = destination[scopeName] = parentGet(scope);
|
||||
initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
|
||||
|
||||
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
|
||||
if (oldValue === newValue) {
|
||||
if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) {
|
||||
if (oldValue === initialValue || (isLiteral && equals(oldValue, initialValue))) {
|
||||
return;
|
||||
}
|
||||
oldValue = initialValue;
|
||||
}
|
||||
recordChanges(scopeName, newValue, oldValue);
|
||||
destination[scopeName] = newValue;
|
||||
}, deepWatch);
|
||||
});
|
||||
|
||||
removeWatchCollection.push(removeWatch);
|
||||
break;
|
||||
|
||||
+3
-22
@@ -26,8 +26,7 @@ function identifierForController(controller, ident) {
|
||||
* {@link ng.$controllerProvider#register register} method.
|
||||
*/
|
||||
function $ControllerProvider() {
|
||||
var controllers = {},
|
||||
globals = false;
|
||||
var controllers = {};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
@@ -55,22 +54,7 @@ function $ControllerProvider() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $controllerProvider#allowGlobals
|
||||
* @description If called, allows `$controller` to find controller constructors on `window`
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="v1.3.0"
|
||||
* removeVersion="v1.7.0"
|
||||
* This method of finding controllers has been deprecated.
|
||||
*/
|
||||
this.allowGlobals = function() {
|
||||
globals = true;
|
||||
};
|
||||
|
||||
|
||||
this.$get = ['$injector', '$window', function($injector, $window) {
|
||||
this.$get = ['$injector', function($injector) {
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
@@ -83,8 +67,6 @@ function $ControllerProvider() {
|
||||
*
|
||||
* * check if a controller with given name is registered via `$controllerProvider`
|
||||
* * check if evaluating the string on the current scope returns a constructor
|
||||
* * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
|
||||
* `window` object (deprecated, not recommended)
|
||||
*
|
||||
* The string can use the `controller as property` syntax, where the controller instance is published
|
||||
* as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
|
||||
@@ -124,8 +106,7 @@ function $ControllerProvider() {
|
||||
identifier = identifier || match[3];
|
||||
expression = controllers.hasOwnProperty(constructor)
|
||||
? controllers[constructor]
|
||||
: getter(locals.$scope, constructor, true) ||
|
||||
(globals ? getter($window, constructor, true) : undefined);
|
||||
: getter(locals.$scope, constructor, true);
|
||||
|
||||
if (!expression) {
|
||||
throw $controllerMinErr('ctrlreg',
|
||||
|
||||
@@ -436,7 +436,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
|
||||
// On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
|
||||
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
|
||||
// to set the property as well to achieve the desired effect.
|
||||
// We use attr[attrName] value since $set can sanitize the url.
|
||||
// We use attr[attrName] value since $set might have sanitized the url.
|
||||
if (msie && propName) element.prop(propName, attr[name]);
|
||||
});
|
||||
}
|
||||
|
||||
+30
-11
@@ -9,7 +9,8 @@ var nullFormCtrl = {
|
||||
$setValidity: noop,
|
||||
$setDirty: noop,
|
||||
$setPristine: noop,
|
||||
$setSubmitted: noop
|
||||
$setSubmitted: noop,
|
||||
$$setSubmitted: noop
|
||||
},
|
||||
PENDING_CLASS = 'ng-pending',
|
||||
SUBMITTED_CLASS = 'ng-submitted';
|
||||
@@ -274,12 +275,25 @@ FormController.prototype = {
|
||||
* @name form.FormController#$setSubmitted
|
||||
*
|
||||
* @description
|
||||
* Sets the form to its submitted state.
|
||||
* Sets the form to its `$submitted` state. This will also set `$submitted` on all child and
|
||||
* parent forms of the form.
|
||||
*/
|
||||
$setSubmitted: function() {
|
||||
var rootForm = this;
|
||||
while (rootForm.$$parentForm && (rootForm.$$parentForm !== nullFormCtrl)) {
|
||||
rootForm = rootForm.$$parentForm;
|
||||
}
|
||||
rootForm.$$setSubmitted();
|
||||
},
|
||||
|
||||
$$setSubmitted: function() {
|
||||
this.$$animate.addClass(this.$$element, SUBMITTED_CLASS);
|
||||
this.$submitted = true;
|
||||
this.$$parentForm.$setSubmitted();
|
||||
forEach(this.$$controls, function(control) {
|
||||
if (control.$$setSubmitted) {
|
||||
control.$$setSubmitted();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -338,16 +352,21 @@ addSetValidityMethod({
|
||||
* @restrict EAC
|
||||
*
|
||||
* @description
|
||||
* Nestable alias of {@link ng.directive:form `form`} directive. HTML
|
||||
* does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a
|
||||
* sub-group of controls needs to be determined.
|
||||
* Helper directive that makes it possible to create control groups inside a
|
||||
* {@link ng.directive:form `form`} directive.
|
||||
* These "child forms" can be used, for example, to determine the validity of a sub-group of
|
||||
* controls.
|
||||
*
|
||||
* Note: the purpose of `ngForm` is to group controls,
|
||||
* but not to be a replacement for the `<form>` tag with all of its capabilities
|
||||
* (e.g. posting to the server, ...).
|
||||
* <div class="alert alert-danger">
|
||||
* **Note**: `ngForm` cannot be used as a replacement for `<form>`, because it lacks its
|
||||
* [built-in HTML functionality](https://html.spec.whatwg.org/#the-form-element).
|
||||
* Specifically, you cannot submit `ngForm` like a `<form>` tag. That means,
|
||||
* you cannot send data to the server with `ngForm`, or integrate it with
|
||||
* {@link ng.directive:ngSubmit `ngSubmit`}.
|
||||
* </div>
|
||||
*
|
||||
* @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into
|
||||
* related scope, under this name.
|
||||
* @param {string=} ngForm|name Name of the form. If specified, the form controller will
|
||||
* be published into the related scope, under this name.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
+25
-59
@@ -457,6 +457,10 @@ var inputType = {
|
||||
* The model must always be a Date object, otherwise AngularJS will throw an error.
|
||||
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
|
||||
*
|
||||
* The value of the resulting Date object will be set to Thursday at 00:00:00 of the requested week,
|
||||
* due to ISO-8601 week numbering standards. Information on ISO's system for numbering the weeks of the
|
||||
* year can be found at: https://en.wikipedia.org/wiki/ISO_8601#Week_dates
|
||||
*
|
||||
* The timezone to be used to read/write the `Date` instance in the model can be defined using
|
||||
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
|
||||
*
|
||||
@@ -1123,8 +1127,8 @@ var inputType = {
|
||||
* Can be interpolated.
|
||||
* @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
|
||||
* Can be interpolated.
|
||||
* @param {string=} ngChange AngularJS expression to be executed when the ngModel value changes due
|
||||
* to user interaction with the input element.
|
||||
* @param {expression=} ngChange AngularJS expression to be executed when the ngModel value changes due
|
||||
* to user interaction with the input element.
|
||||
* @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the
|
||||
* element. **Note** : `ngChecked` should not be used alongside `ngModel`.
|
||||
* Checkout {@link ng.directive:ngChecked ngChecked} for usage.
|
||||
@@ -1470,12 +1474,11 @@ function createDateParser(regexp, mapping) {
|
||||
|
||||
function createDateInputType(type, regexp, parseDate, format) {
|
||||
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
|
||||
badInputChecker(scope, element, attr, ctrl);
|
||||
badInputChecker(scope, element, attr, ctrl, type);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
var previousDate;
|
||||
var previousTimezone;
|
||||
|
||||
ctrl.$$parserName = type;
|
||||
ctrl.$parsers.push(function(value) {
|
||||
if (ctrl.$isEmpty(value)) return null;
|
||||
|
||||
@@ -1485,6 +1488,7 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
// contains some different data format!
|
||||
return parseDateAndConvertTimeZoneToLocal(value, previousDate);
|
||||
}
|
||||
ctrl.$$parserName = type;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
@@ -1557,22 +1561,28 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
};
|
||||
}
|
||||
|
||||
function badInputChecker(scope, element, attr, ctrl) {
|
||||
function badInputChecker(scope, element, attr, ctrl, parserName) {
|
||||
var node = element[0];
|
||||
var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
|
||||
if (nativeValidation) {
|
||||
ctrl.$parsers.push(function(value) {
|
||||
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
|
||||
return validity.badInput || validity.typeMismatch ? undefined : value;
|
||||
if (validity.badInput || validity.typeMismatch) {
|
||||
ctrl.$$parserName = parserName;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function numberFormatterParser(ctrl) {
|
||||
ctrl.$$parserName = 'number';
|
||||
ctrl.$parsers.push(function(value) {
|
||||
if (ctrl.$isEmpty(value)) return null;
|
||||
if (NUMBER_REGEXP.test(value)) return parseFloat(value);
|
||||
|
||||
ctrl.$$parserName = 'number';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
@@ -1654,7 +1664,7 @@ function isValidForStep(viewValue, stepBase, step) {
|
||||
}
|
||||
|
||||
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
badInputChecker(scope, element, attr, ctrl);
|
||||
badInputChecker(scope, element, attr, ctrl, 'number');
|
||||
numberFormatterParser(ctrl);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
@@ -1662,8 +1672,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
var maxVal;
|
||||
|
||||
if (isDefined(attr.min) || attr.ngMin) {
|
||||
ctrl.$validators.min = function(value) {
|
||||
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
|
||||
ctrl.$validators.min = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
|
||||
};
|
||||
|
||||
attr.$observe('min', function(val) {
|
||||
@@ -1674,8 +1684,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
if (isDefined(attr.max) || attr.ngMax) {
|
||||
ctrl.$validators.max = function(value) {
|
||||
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
|
||||
ctrl.$validators.max = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
|
||||
};
|
||||
|
||||
attr.$observe('max', function(val) {
|
||||
@@ -1701,7 +1711,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
badInputChecker(scope, element, attr, ctrl);
|
||||
badInputChecker(scope, element, attr, ctrl, 'range');
|
||||
numberFormatterParser(ctrl);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
@@ -1840,7 +1850,6 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
stringBasedInputType(ctrl);
|
||||
|
||||
ctrl.$$parserName = 'url';
|
||||
ctrl.$validators.url = function(modelValue, viewValue) {
|
||||
var value = modelValue || viewValue;
|
||||
return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
|
||||
@@ -1853,7 +1862,6 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
stringBasedInputType(ctrl);
|
||||
|
||||
ctrl.$$parserName = 'email';
|
||||
ctrl.$validators.email = function(modelValue, viewValue) {
|
||||
var value = modelValue || viewValue;
|
||||
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
|
||||
@@ -1878,7 +1886,7 @@ function radioInputType(scope, element, attr, ctrl) {
|
||||
}
|
||||
};
|
||||
|
||||
element.on('click', listener);
|
||||
element.on('change', listener);
|
||||
|
||||
ctrl.$render = function() {
|
||||
var value = attr.value;
|
||||
@@ -1912,7 +1920,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
|
||||
ctrl.$setViewValue(element[0].checked, ev && ev.type);
|
||||
};
|
||||
|
||||
element.on('click', listener);
|
||||
element.on('change', listener);
|
||||
|
||||
ctrl.$render = function() {
|
||||
element[0].checked = ctrl.$viewValue;
|
||||
@@ -2139,48 +2147,6 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
|
||||
}];
|
||||
|
||||
|
||||
var hiddenInputBrowserCacheDirective = function() {
|
||||
var valueProperty = {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: function() {
|
||||
return this.getAttribute('value') || '';
|
||||
},
|
||||
set: function(val) {
|
||||
this.setAttribute('value', val);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
priority: 200,
|
||||
compile: function(_, attr) {
|
||||
if (lowercase(attr.type) !== 'hidden') {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
pre: function(scope, element, attr, ctrls) {
|
||||
var node = element[0];
|
||||
|
||||
// Support: Edge
|
||||
// Moving the DOM around prevents autofillling
|
||||
if (node.parentNode) {
|
||||
node.parentNode.insertBefore(node, node.nextSibling);
|
||||
}
|
||||
|
||||
// Support: FF, IE
|
||||
// Avoiding direct assignment to .value prevents autofillling
|
||||
if (Object.defineProperty) {
|
||||
Object.defineProperty(node, 'value', valueProperty);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
|
||||
/**
|
||||
|
||||
@@ -14,13 +14,6 @@ function classDirective(name, selector) {
|
||||
return {
|
||||
restrict: 'AC',
|
||||
link: function(scope, element, attr) {
|
||||
var expression = attr[name].trim();
|
||||
var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':');
|
||||
|
||||
var watchInterceptor = isOneTime ? toFlatValue : toClassString;
|
||||
var watchExpression = $parse(expression, watchInterceptor);
|
||||
var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction;
|
||||
|
||||
var classCounts = element.data('$classCounts');
|
||||
var oldModulo = true;
|
||||
var oldClassString;
|
||||
@@ -43,7 +36,7 @@ function classDirective(name, selector) {
|
||||
scope.$watch(indexWatchExpression, ngClassIndexWatchAction);
|
||||
}
|
||||
|
||||
scope.$watch(watchExpression, watchAction, isOneTime);
|
||||
scope.$watch($parse(attr[name], toClassString), ngClassWatchAction);
|
||||
|
||||
function addClasses(classString) {
|
||||
classString = digestClassCounts(split(classString), 1);
|
||||
@@ -85,9 +78,9 @@ function classDirective(name, selector) {
|
||||
}
|
||||
|
||||
function ngClassIndexWatchAction(newModulo) {
|
||||
// This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it
|
||||
// This watch-action should run before the `ngClassWatchAction()`, thus it
|
||||
// adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the
|
||||
// `ngClass[OneTime]WatchAction()` will update the classes.
|
||||
// `ngClassWatchAction()` will update the classes.
|
||||
if (newModulo === selector) {
|
||||
addClasses(oldClassString);
|
||||
} else {
|
||||
@@ -97,14 +90,6 @@ function classDirective(name, selector) {
|
||||
oldModulo = newModulo;
|
||||
}
|
||||
|
||||
function ngClassOneTimeWatchAction(newClassValue) {
|
||||
var newClassString = toClassString(newClassValue);
|
||||
|
||||
if (newClassString !== oldClassString) {
|
||||
ngClassWatchAction(newClassString);
|
||||
}
|
||||
}
|
||||
|
||||
function ngClassWatchAction(newClassString) {
|
||||
if (oldModulo === selector) {
|
||||
updateClasses(oldClassString, newClassString);
|
||||
@@ -152,34 +137,6 @@ function classDirective(name, selector) {
|
||||
|
||||
return classString;
|
||||
}
|
||||
|
||||
function toFlatValue(classValue) {
|
||||
var flatValue = classValue;
|
||||
|
||||
if (isArray(classValue)) {
|
||||
flatValue = classValue.map(toFlatValue);
|
||||
} else if (isObject(classValue)) {
|
||||
var hasUndefined = false;
|
||||
|
||||
flatValue = Object.keys(classValue).filter(function(key) {
|
||||
var value = classValue[key];
|
||||
|
||||
if (!hasUndefined && isUndefined(value)) {
|
||||
hasUndefined = true;
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
if (hasUndefined) {
|
||||
// Prevent the `oneTimeLiteralWatchInterceptor` from unregistering
|
||||
// the watcher, by including at least one `undefined` value.
|
||||
flatValue.push(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
return flatValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,10 +31,6 @@
|
||||
* The controller instance can be published into a scope property by specifying
|
||||
* `ng-controller="as propertyName"`.
|
||||
*
|
||||
* If the current `$controllerProvider` is configured to use globals (via
|
||||
* {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may
|
||||
* also be the name of a globally accessible constructor function (deprecated, not recommended).
|
||||
*
|
||||
* @example
|
||||
* Here is a simple form for editing user contact information. Adding, removing, clearing, and
|
||||
* greeting are methods declared on the controller (see source tab). These methods can
|
||||
|
||||
@@ -282,12 +282,11 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
|
||||
this.$$ngModelSet = this.$$parsedNgModelAssign;
|
||||
this.$$pendingDebounce = null;
|
||||
this.$$parserValid = undefined;
|
||||
this.$$parserName = 'parse';
|
||||
|
||||
this.$$currentValidationRunId = 0;
|
||||
|
||||
// https://github.com/angular/angular.js/issues/15833
|
||||
// Prevent `$$scope` from being iterated over by `copy` when NgModelController is deep watched
|
||||
Object.defineProperty(this, '$$scope', {value: $scope});
|
||||
this.$$scope = $scope;
|
||||
this.$$attr = $attr;
|
||||
this.$$element = $element;
|
||||
this.$$animate = $animate;
|
||||
@@ -614,7 +613,8 @@ NgModelController.prototype = {
|
||||
processAsyncValidators();
|
||||
|
||||
function processParseErrors() {
|
||||
var errorKey = that.$$parserName || 'parse';
|
||||
var errorKey = that.$$parserName;
|
||||
|
||||
if (isUndefined(that.$$parserValid)) {
|
||||
setValidity(errorKey, null);
|
||||
} else {
|
||||
@@ -626,6 +626,7 @@ NgModelController.prototype = {
|
||||
setValidity(name, null);
|
||||
});
|
||||
}
|
||||
|
||||
// Set the parse error last, to prevent unsetting it, should a $validators key == parserName
|
||||
setValidity(errorKey, that.$$parserValid);
|
||||
return that.$$parserValid;
|
||||
@@ -728,6 +729,10 @@ NgModelController.prototype = {
|
||||
|
||||
this.$$parserValid = isUndefined(modelValue) ? undefined : true;
|
||||
|
||||
// Reset any previous parse error
|
||||
this.$setValidity(this.$$parserName, null);
|
||||
this.$$parserName = 'parse';
|
||||
|
||||
if (this.$$parserValid) {
|
||||
for (var i = 0; i < this.$parsers.length; i++) {
|
||||
modelValue = this.$parsers[i](modelValue);
|
||||
@@ -845,8 +850,12 @@ NgModelController.prototype = {
|
||||
|
||||
if (isNumber(debounceDelay[trigger])) {
|
||||
debounceDelay = debounceDelay[trigger];
|
||||
} else if (isNumber(debounceDelay['default'])) {
|
||||
} else if (isNumber(debounceDelay['default']) &&
|
||||
this.$options.getOption('updateOn').indexOf(trigger) === -1
|
||||
) {
|
||||
debounceDelay = debounceDelay['default'];
|
||||
} else if (isNumber(debounceDelay['*'])) {
|
||||
debounceDelay = debounceDelay['*'];
|
||||
}
|
||||
|
||||
this.$$timeout.cancel(this.$$pendingDebounce);
|
||||
|
||||
@@ -264,6 +264,105 @@ defaultModelOptions = new ModelOptions({
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
* ### Default events, extra triggers, and catch-all debounce values
|
||||
*
|
||||
* This example shows the relationship between "default" update events and
|
||||
* additional `updateOn` triggers.
|
||||
*
|
||||
* `default` events are those that are bound to the control, and when fired, update the `$viewValue`
|
||||
* via {@link ngModel.NgModelController#$setViewValue $setViewValue}. Every event that is not listed
|
||||
* in `updateOn` is considered a "default" event, since different control types have different
|
||||
* default events.
|
||||
*
|
||||
* The control in this example updates by "default", "click", and "blur", with different `debounce`
|
||||
* values. You can see that "click" doesn't have an individual `debounce` value -
|
||||
* therefore it uses the `*` debounce value.
|
||||
*
|
||||
* There is also a button that calls {@link ngModel.NgModelController#$setViewValue $setViewValue}
|
||||
* directly with a "custom" event. Since "custom" is not defined in the `updateOn` list,
|
||||
* it is considered a "default" event and will update the
|
||||
* control if "default" is defined in `updateOn`, and will receive the "default" debounce value.
|
||||
* Note that this is just to illustrate how custom controls would possibly call `$setViewValue`.
|
||||
*
|
||||
* You can change the `updateOn` and `debounce` configuration to test different scenarios. This
|
||||
* is done with {@link ngModel.NgModelController#$overrideModelOptions $overrideModelOptions}.
|
||||
*
|
||||
<example name="ngModelOptions-advanced" module="optionsExample">
|
||||
<file name="index.html">
|
||||
<model-update-demo></model-update-demo>
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('optionsExample', [])
|
||||
.component('modelUpdateDemo', {
|
||||
templateUrl: 'template.html',
|
||||
controller: function() {
|
||||
this.name = 'Chinua';
|
||||
|
||||
this.options = {
|
||||
updateOn: 'default blur click',
|
||||
debounce: {
|
||||
default: 2000,
|
||||
blur: 0,
|
||||
'*': 1000
|
||||
}
|
||||
};
|
||||
|
||||
this.updateEvents = function() {
|
||||
var eventList = this.options.updateOn.split(' ');
|
||||
eventList.push('*');
|
||||
var events = {};
|
||||
|
||||
for (var i = 0; i < eventList.length; i++) {
|
||||
events[eventList[i]] = this.options.debounce[eventList[i]];
|
||||
}
|
||||
|
||||
this.events = events;
|
||||
};
|
||||
|
||||
this.updateOptions = function() {
|
||||
var options = angular.extend(this.options, {
|
||||
updateOn: Object.keys(this.events).join(' ').replace('*', ''),
|
||||
debounce: this.events
|
||||
});
|
||||
|
||||
this.form.input.$overrideModelOptions(options);
|
||||
};
|
||||
|
||||
// Initialize the event form
|
||||
this.updateEvents();
|
||||
}
|
||||
});
|
||||
</file>
|
||||
<file name="template.html">
|
||||
<form name="$ctrl.form">
|
||||
Input: <input type="text" name="input" ng-model="$ctrl.name" ng-model-options="$ctrl.options" />
|
||||
</form>
|
||||
Model: <tt>{{$ctrl.name}}</tt>
|
||||
<hr>
|
||||
<button ng-click="$ctrl.form.input.$setViewValue('some value', 'custom')">Trigger setViewValue with 'some value' and 'custom' event</button>
|
||||
|
||||
<hr>
|
||||
<form ng-submit="$ctrl.updateOptions()">
|
||||
<b>updateOn</b><br>
|
||||
<input type="text" ng-model="$ctrl.options.updateOn" ng-change="$ctrl.updateEvents()" ng-model-options="{debounce: 500}">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Debounce value</th>
|
||||
</tr>
|
||||
<tr ng-repeat="(key, value) in $ctrl.events">
|
||||
<td>{{key}}</td>
|
||||
<td><input type="number" ng-model="$ctrl.events[key]" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
<input type="submit" value="Update options">
|
||||
</form>
|
||||
</file>
|
||||
</example>
|
||||
*
|
||||
*
|
||||
* ## Model updates and validation
|
||||
*
|
||||
@@ -319,6 +418,9 @@ defaultModelOptions = new ModelOptions({
|
||||
* watched for changes. However, it is possible to override the options on a single
|
||||
* {@link ngModel.NgModelController} instance with
|
||||
* {@link ngModel.NgModelController#$overrideModelOptions `NgModelController#$overrideModelOptions()`}.
|
||||
* See also the example for
|
||||
* {@link ngModelOptions#default-events-extra-triggers-and-catch-all-debounce-values
|
||||
* Default events, extra triggers, and catch-all debounce values}.
|
||||
*
|
||||
*
|
||||
* @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
|
||||
@@ -339,14 +441,18 @@ defaultModelOptions = new ModelOptions({
|
||||
* custom value for each event. For example:
|
||||
* ```
|
||||
* ng-model-options="{
|
||||
* updateOn: 'default blur click',
|
||||
* updateOn: 'default blur',
|
||||
* debounce: { 'default': 500, 'blur': 0 }
|
||||
* }"
|
||||
* ```
|
||||
*
|
||||
* "default" also applies to all events that are listed in `updateOn` but are not
|
||||
* listed in `debounce`, i.e. "click" would also be debounced by 500 milliseconds.
|
||||
*
|
||||
* You can use the `*` key to specify a debounce value that applies to all events that are not
|
||||
* specifically listed. In the following example, `mouseup` would have a debounce delay of 1000:
|
||||
* ```
|
||||
* ng-model-options="{
|
||||
* updateOn: 'default blur mouseup',
|
||||
* debounce: { 'default': 500, 'blur': 0, '*': 1000 }
|
||||
* }"
|
||||
* ```
|
||||
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
|
||||
* not validate correctly instead of the default behavior of setting the model to undefined.
|
||||
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
</example>
|
||||
*/
|
||||
var ngStyleDirective = ngDirective(function(scope, element, attr) {
|
||||
scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
|
||||
scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
|
||||
if (oldStyles && (newStyles !== oldStyles)) {
|
||||
forEach(oldStyles, function(val, style) { element.css(style, '');});
|
||||
}
|
||||
if (newStyles) element.css(newStyles);
|
||||
}, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
});
|
||||
* </file>
|
||||
* </example>
|
||||
*/ /** CLOSURE HACK COMMENT */
|
||||
*/
|
||||
var requiredDirective = function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -161,7 +161,7 @@ var requiredDirective = function() {
|
||||
});
|
||||
* </file>
|
||||
* </example>
|
||||
*/ /** CLOSURE HACK COMMENT */
|
||||
*/
|
||||
var patternDirective = function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -263,7 +263,7 @@ var patternDirective = function() {
|
||||
});
|
||||
* </file>
|
||||
* </example>
|
||||
*/ /** CLOSURE HACK COMMENT */
|
||||
*/
|
||||
var maxlengthDirective = function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -352,7 +352,7 @@ var maxlengthDirective = function() {
|
||||
});
|
||||
* </file>
|
||||
* </example>
|
||||
*/ /** CLOSURE HACK COMMENT */
|
||||
*/
|
||||
var minlengthDirective = function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
* index: ...
|
||||
* }
|
||||
* ```
|
||||
* **Note:** `null` values use `'null'` as their type.
|
||||
* 2. The comparator function is used to sort the items, based on the derived values, types and
|
||||
* indices.
|
||||
*
|
||||
@@ -74,11 +75,15 @@
|
||||
*
|
||||
* The default, built-in comparator should be sufficient for most usecases. In short, it compares
|
||||
* numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
|
||||
* using their index in the original collection, and sorts values of different types by type.
|
||||
* using their index in the original collection, sorts values of different types by type and puts
|
||||
* `undefined` and `null` values at the end of the sorted list.
|
||||
*
|
||||
* More specifically, it follows these steps to determine the relative order of items:
|
||||
*
|
||||
* 1. If the compared values are of different types, compare the types themselves alphabetically.
|
||||
* 1. If the compared values are of different types:
|
||||
* - If one of the values is undefined, consider it "greater than" the other.
|
||||
* - Else if one of the values is null, consider it "greater than" the other.
|
||||
* - Else compare the types themselves alphabetically.
|
||||
* 2. If both values are of type `string`, compare them alphabetically in a case- and
|
||||
* locale-insensitive way.
|
||||
* 3. If both values are objects, compare their indices instead.
|
||||
@@ -89,9 +94,10 @@
|
||||
*
|
||||
* **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
|
||||
* saved as numbers and not strings.
|
||||
* **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e.
|
||||
* `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to
|
||||
* other values.
|
||||
* **Note:** For the purpose of sorting, `null` and `undefined` are considered "greater than"
|
||||
* any other value (with undefined "greater than" null). This effectively means that `null`
|
||||
* and `undefined` values end up at the end of a list sorted in ascending order.
|
||||
* **Note:** `null` values use `'null'` as their type to be able to distinguish them from objects.
|
||||
*
|
||||
* @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
|
||||
* @param {(Function|string|Array.<Function|string>)=} expression - A predicate (or list of
|
||||
@@ -658,8 +664,7 @@ function orderByFilter($parse) {
|
||||
function getPredicateValue(value, index) {
|
||||
var type = typeof value;
|
||||
if (value === null) {
|
||||
type = 'string';
|
||||
value = 'null';
|
||||
type = 'null';
|
||||
} else if (type === 'object') {
|
||||
value = objectValue(value);
|
||||
}
|
||||
@@ -690,7 +695,11 @@ function orderByFilter($parse) {
|
||||
result = value1 < value2 ? -1 : 1;
|
||||
}
|
||||
} else {
|
||||
result = type1 < type2 ? -1 : 1;
|
||||
result = (type1 === 'undefined') ? 1 :
|
||||
(type2 === 'undefined') ? -1 :
|
||||
(type1 === 'null') ? 1 :
|
||||
(type2 === 'null') ? -1 :
|
||||
(type1 < type2) ? -1 : 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
+5
-2
@@ -110,7 +110,6 @@ function $HttpParamSerializerJQLikeProvider() {
|
||||
return parts.join('&');
|
||||
|
||||
function serialize(toSerialize, prefix, topLevel) {
|
||||
if (toSerialize === null || isUndefined(toSerialize)) return;
|
||||
if (isArray(toSerialize)) {
|
||||
forEach(toSerialize, function(value, index) {
|
||||
serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']');
|
||||
@@ -123,7 +122,11 @@ function $HttpParamSerializerJQLikeProvider() {
|
||||
(topLevel ? '' : ']'));
|
||||
});
|
||||
} else {
|
||||
parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize)));
|
||||
if (isFunction(toSerialize)) {
|
||||
toSerialize = toSerialize();
|
||||
}
|
||||
parts.push(encodeUriQuery(prefix) + '=' +
|
||||
(toSerialize == null ? '' : encodeUriQuery(serializeValue(toSerialize))));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+53
-30
@@ -73,9 +73,8 @@ function $InterpolateProvider() {
|
||||
if (value) {
|
||||
startSymbol = value;
|
||||
return this;
|
||||
} else {
|
||||
return startSymbol;
|
||||
}
|
||||
return startSymbol;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,9 +90,8 @@ function $InterpolateProvider() {
|
||||
if (value) {
|
||||
endSymbol = value;
|
||||
return this;
|
||||
} else {
|
||||
return endSymbol;
|
||||
}
|
||||
return endSymbol;
|
||||
};
|
||||
|
||||
|
||||
@@ -140,7 +138,7 @@ function $InterpolateProvider() {
|
||||
* ```js
|
||||
* var $interpolate = ...; // injected
|
||||
* var exp = $interpolate('Hello {{name | uppercase}}!');
|
||||
* expect(exp({name:'AngularJS'})).toEqual('Hello ANGULAR!');
|
||||
* expect(exp({name:'AngularJS'})).toEqual('Hello ANGULARJS!');
|
||||
* ```
|
||||
*
|
||||
* `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is
|
||||
@@ -240,16 +238,21 @@ function $InterpolateProvider() {
|
||||
* - `context`: evaluation context for all expressions embedded in the interpolated text
|
||||
*/
|
||||
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
|
||||
var contextAllowsConcatenation = trustedContext === $sce.URL || trustedContext === $sce.MEDIA_URL;
|
||||
|
||||
// Provide a quick exit and simplified result function for text with no interpolation
|
||||
if (!text.length || text.indexOf(startSymbol) === -1) {
|
||||
var constantInterp;
|
||||
if (!mustHaveExpression) {
|
||||
var unescapedText = unescapeText(text);
|
||||
constantInterp = valueFn(unescapedText);
|
||||
constantInterp.exp = text;
|
||||
constantInterp.expressions = [];
|
||||
constantInterp.$$watchDelegate = constantWatchDelegate;
|
||||
if (mustHaveExpression && !contextAllowsConcatenation) return;
|
||||
|
||||
var unescapedText = unescapeText(text);
|
||||
if (contextAllowsConcatenation) {
|
||||
unescapedText = $sce.getTrusted(trustedContext, unescapedText);
|
||||
}
|
||||
var constantInterp = valueFn(unescapedText);
|
||||
constantInterp.exp = text;
|
||||
constantInterp.expressions = [];
|
||||
constantInterp.$$watchDelegate = constantWatchDelegate;
|
||||
|
||||
return constantInterp;
|
||||
}
|
||||
|
||||
@@ -258,11 +261,13 @@ function $InterpolateProvider() {
|
||||
endIndex,
|
||||
index = 0,
|
||||
expressions = [],
|
||||
parseFns = [],
|
||||
parseFns,
|
||||
textLength = text.length,
|
||||
exp,
|
||||
concat = [],
|
||||
expressionPositions = [];
|
||||
expressionPositions = [],
|
||||
singleExpression;
|
||||
|
||||
|
||||
while (index < textLength) {
|
||||
if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
|
||||
@@ -272,10 +277,9 @@ function $InterpolateProvider() {
|
||||
}
|
||||
exp = text.substring(startIndex + startSymbolLength, endIndex);
|
||||
expressions.push(exp);
|
||||
parseFns.push($parse(exp, parseStringifyInterceptor));
|
||||
index = endIndex + endSymbolLength;
|
||||
expressionPositions.push(concat.length);
|
||||
concat.push('');
|
||||
concat.push(''); // Placeholder that will get replaced with the evaluated expression.
|
||||
} else {
|
||||
// we did not find an interpolation, so we have to add the remainder to the separators array
|
||||
if (index !== textLength) {
|
||||
@@ -285,15 +289,25 @@ function $InterpolateProvider() {
|
||||
}
|
||||
}
|
||||
|
||||
singleExpression = concat.length === 1 && expressionPositions.length === 1;
|
||||
// Intercept expression if we need to stringify concatenated inputs, which may be SCE trusted
|
||||
// objects rather than simple strings
|
||||
// (we don't modify the expression if the input consists of only a single trusted input)
|
||||
var interceptor = contextAllowsConcatenation && singleExpression ? undefined : parseStringifyInterceptor;
|
||||
parseFns = expressions.map(function(exp) { return $parse(exp, interceptor); });
|
||||
|
||||
// Concatenating expressions makes it hard to reason about whether some combination of
|
||||
// concatenated values are unsafe to use and could easily lead to XSS. By requiring that a
|
||||
// single expression be used for iframe[src], object[src], etc., we ensure that the value
|
||||
// that's used is assigned or constructed by some JS code somewhere that is more testable or
|
||||
// make it obvious that you bound the value to some user controlled value. This helps reduce
|
||||
// the load when auditing for XSS issues.
|
||||
if (trustedContext && concat.length > 1) {
|
||||
$interpolateMinErr.throwNoconcat(text);
|
||||
}
|
||||
// single expression be used for some $sce-managed secure contexts (RESOURCE_URLs mostly),
|
||||
// we ensure that the value that's used is assigned or constructed by some JS code somewhere
|
||||
// that is more testable or make it obvious that you bound the value to some user controlled
|
||||
// value. This helps reduce the load when auditing for XSS issues.
|
||||
|
||||
// Note that URL and MEDIA_URL $sce contexts do not need this, since `$sce` can sanitize the values
|
||||
// passed to it. In that case, `$sce.getTrusted` will be called on either the single expression
|
||||
// or on the overall concatenated string (losing trusted types used in the mix, by design).
|
||||
// Both these methods will sanitize plain strings. Also, HTML could be included, but since it's
|
||||
// only used in srcdoc attributes, this would not be very useful.
|
||||
|
||||
if (!mustHaveExpression || expressions.length) {
|
||||
var compute = function(values) {
|
||||
@@ -301,13 +315,16 @@ function $InterpolateProvider() {
|
||||
if (allOrNothing && isUndefined(values[i])) return;
|
||||
concat[expressionPositions[i]] = values[i];
|
||||
}
|
||||
return concat.join('');
|
||||
};
|
||||
|
||||
var getValue = function(value) {
|
||||
return trustedContext ?
|
||||
$sce.getTrusted(trustedContext, value) :
|
||||
$sce.valueOf(value);
|
||||
if (contextAllowsConcatenation) {
|
||||
// If `singleExpression` then `concat[0]` might be a "trusted" value or `null`, rather than a string
|
||||
return $sce.getTrusted(trustedContext, singleExpression ? concat[0] : concat.join(''));
|
||||
} else if (trustedContext && concat.length > 1) {
|
||||
// This context does not allow more than one part, e.g. expr + string or exp + exp.
|
||||
$interpolateMinErr.throwNoconcat(text);
|
||||
}
|
||||
// In an unprivileged context or only one part: just concatenate and return.
|
||||
return concat.join('');
|
||||
};
|
||||
|
||||
return extend(function interpolationFn(context) {
|
||||
@@ -342,7 +359,13 @@ function $InterpolateProvider() {
|
||||
|
||||
function parseStringifyInterceptor(value) {
|
||||
try {
|
||||
value = getValue(value);
|
||||
// In concatenable contexts, getTrusted comes at the end, to avoid sanitizing individual
|
||||
// parts of a full URL. We don't care about losing the trustedness here.
|
||||
// In non-concatenable contexts, where there is only one expression, this interceptor is
|
||||
// not applied to the expression.
|
||||
value = (trustedContext && !contextAllowsConcatenation) ?
|
||||
$sce.getTrusted(trustedContext, value) :
|
||||
$sce.valueOf(value);
|
||||
return allOrNothing && !isDefined(value) ? value : stringify(value);
|
||||
} catch (err) {
|
||||
$exceptionHandler($interpolateMinErr.interr(text, err));
|
||||
|
||||
+20
-8
@@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var $intervalMinErr = minErr('$interval');
|
||||
|
||||
/** @this */
|
||||
function $IntervalProvider() {
|
||||
this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
|
||||
@@ -188,15 +190,25 @@ function $IntervalProvider() {
|
||||
* @returns {boolean} Returns `true` if the task was successfully canceled.
|
||||
*/
|
||||
interval.cancel = function(promise) {
|
||||
if (promise && promise.$$intervalId in intervals) {
|
||||
// Interval cancels should not report as unhandled promise.
|
||||
markQExceptionHandled(intervals[promise.$$intervalId].promise);
|
||||
intervals[promise.$$intervalId].reject('canceled');
|
||||
$window.clearInterval(promise.$$intervalId);
|
||||
delete intervals[promise.$$intervalId];
|
||||
return true;
|
||||
if (!promise) return false;
|
||||
|
||||
if (!promise.hasOwnProperty('$$intervalId')) {
|
||||
throw $intervalMinErr('badprom',
|
||||
'`$interval.cancel()` called with a promise that was not generated by `$interval()`.');
|
||||
}
|
||||
return false;
|
||||
|
||||
if (!intervals.hasOwnProperty(promise.$$intervalId)) return false;
|
||||
|
||||
var id = promise.$$intervalId;
|
||||
var deferred = intervals[id];
|
||||
|
||||
// Interval cancels should not report an unhandled promise.
|
||||
markQExceptionHandled(deferred.promise);
|
||||
deferred.reject('canceled');
|
||||
$window.clearInterval(id);
|
||||
delete intervals[id];
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return interval;
|
||||
|
||||
@@ -938,8 +938,6 @@ function $LocationProvider() {
|
||||
// update location manually
|
||||
if ($location.absUrl() !== $browser.url()) {
|
||||
$rootScope.$apply();
|
||||
// hack to work around FF6 bug 684208 when scenario runner clicks on links
|
||||
$window.angular['ff-684208-preventDefault'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+94
-85
@@ -1803,15 +1803,8 @@ function $ParseProvider() {
|
||||
var lexer = new Lexer($parseOptions);
|
||||
var parser = new Parser(lexer, $filter, $parseOptions);
|
||||
parsedExpression = parser.parse(exp);
|
||||
if (parsedExpression.constant) {
|
||||
parsedExpression.$$watchDelegate = constantWatchDelegate;
|
||||
} else if (parsedExpression.oneTime) {
|
||||
parsedExpression.$$watchDelegate = parsedExpression.literal ?
|
||||
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
|
||||
} else if (parsedExpression.inputs) {
|
||||
parsedExpression.$$watchDelegate = inputsWatchDelegate;
|
||||
}
|
||||
cache[cacheKey] = parsedExpression;
|
||||
|
||||
cache[cacheKey] = addWatchDelegate(parsedExpression);
|
||||
}
|
||||
return addInterceptor(parsedExpression, interceptorFn);
|
||||
|
||||
@@ -1899,57 +1892,48 @@ function $ParseProvider() {
|
||||
}
|
||||
|
||||
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
|
||||
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
|
||||
var unwatch, lastValue;
|
||||
if (parsedExpression.inputs) {
|
||||
unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression);
|
||||
} else {
|
||||
unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality);
|
||||
}
|
||||
|
||||
var exp = parsedExpression.$$intercepted || parsedExpression;
|
||||
var post = parsedExpression.$$interceptor || identity;
|
||||
|
||||
var useInputs = parsedExpression.inputs && !exp.inputs;
|
||||
|
||||
// Propogate the literal/inputs/constant attributes
|
||||
// ... but not oneTime since we are handling it
|
||||
oneTimeWatch.literal = parsedExpression.literal;
|
||||
oneTimeWatch.constant = parsedExpression.constant;
|
||||
oneTimeWatch.inputs = parsedExpression.inputs;
|
||||
|
||||
// Allow other delegates to run on this wrapped expression
|
||||
addWatchDelegate(oneTimeWatch);
|
||||
|
||||
unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
|
||||
|
||||
return unwatch;
|
||||
|
||||
function oneTimeWatch(scope) {
|
||||
return parsedExpression(scope);
|
||||
function unwatchIfDone() {
|
||||
if (isDone(lastValue)) {
|
||||
unwatch();
|
||||
}
|
||||
}
|
||||
function oneTimeListener(value, old, scope) {
|
||||
lastValue = value;
|
||||
if (isFunction(listener)) {
|
||||
listener(value, old, scope);
|
||||
}
|
||||
if (isDefined(value)) {
|
||||
scope.$$postDigest(function() {
|
||||
if (isDefined(lastValue)) {
|
||||
unwatch();
|
||||
}
|
||||
});
|
||||
|
||||
function oneTimeWatch(scope, locals, assign, inputs) {
|
||||
lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs);
|
||||
if (isDone(lastValue)) {
|
||||
scope.$$postDigest(unwatchIfDone);
|
||||
}
|
||||
return post(lastValue);
|
||||
}
|
||||
}
|
||||
|
||||
function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
|
||||
var unwatch, lastValue;
|
||||
unwatch = scope.$watch(function oneTimeWatch(scope) {
|
||||
return parsedExpression(scope);
|
||||
}, function oneTimeListener(value, old, scope) {
|
||||
lastValue = value;
|
||||
if (isFunction(listener)) {
|
||||
listener(value, old, scope);
|
||||
}
|
||||
if (isAllDefined(value)) {
|
||||
scope.$$postDigest(function() {
|
||||
if (isAllDefined(lastValue)) unwatch();
|
||||
});
|
||||
}
|
||||
}, objectEquality);
|
||||
|
||||
return unwatch;
|
||||
|
||||
function isAllDefined(value) {
|
||||
var allDefined = true;
|
||||
forEach(value, function(val) {
|
||||
if (!isDefined(val)) allDefined = false;
|
||||
});
|
||||
return allDefined;
|
||||
}
|
||||
function isAllDefined(value) {
|
||||
var allDefined = true;
|
||||
forEach(value, function(val) {
|
||||
if (!isDefined(val)) allDefined = false;
|
||||
});
|
||||
return allDefined;
|
||||
}
|
||||
|
||||
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
|
||||
@@ -1960,49 +1944,74 @@ function $ParseProvider() {
|
||||
return unwatch;
|
||||
}
|
||||
|
||||
function addInterceptor(parsedExpression, interceptorFn) {
|
||||
if (!interceptorFn) return parsedExpression;
|
||||
var watchDelegate = parsedExpression.$$watchDelegate;
|
||||
var useInputs = false;
|
||||
|
||||
var regularWatch =
|
||||
watchDelegate !== oneTimeLiteralWatchDelegate &&
|
||||
watchDelegate !== oneTimeWatchDelegate;
|
||||
|
||||
var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
|
||||
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
|
||||
return interceptorFn(value, scope, locals);
|
||||
} : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
|
||||
var value = parsedExpression(scope, locals, assign, inputs);
|
||||
var result = interceptorFn(value, scope, locals);
|
||||
// we only return the interceptor's result if the
|
||||
// initial value is defined (for bind-once)
|
||||
return isDefined(value) ? result : value;
|
||||
};
|
||||
|
||||
// Propagate $$watchDelegates other then inputsWatchDelegate
|
||||
useInputs = !parsedExpression.inputs;
|
||||
if (watchDelegate && watchDelegate !== inputsWatchDelegate) {
|
||||
fn.$$watchDelegate = watchDelegate;
|
||||
fn.inputs = parsedExpression.inputs;
|
||||
} else if (!interceptorFn.$stateful) {
|
||||
// Treat interceptor like filters - assume non-stateful by default and use the inputsWatchDelegate
|
||||
fn.$$watchDelegate = inputsWatchDelegate;
|
||||
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
|
||||
function addWatchDelegate(parsedExpression) {
|
||||
if (parsedExpression.constant) {
|
||||
parsedExpression.$$watchDelegate = constantWatchDelegate;
|
||||
} else if (parsedExpression.oneTime) {
|
||||
parsedExpression.$$watchDelegate = oneTimeWatchDelegate;
|
||||
} else if (parsedExpression.inputs) {
|
||||
parsedExpression.$$watchDelegate = inputsWatchDelegate;
|
||||
}
|
||||
|
||||
if (fn.inputs) {
|
||||
fn.inputs = fn.inputs.map(function(e) {
|
||||
return parsedExpression;
|
||||
}
|
||||
|
||||
function chainInterceptors(first, second) {
|
||||
function chainedInterceptor(value) {
|
||||
return second(first(value));
|
||||
}
|
||||
chainedInterceptor.$stateful = first.$stateful || second.$stateful;
|
||||
chainedInterceptor.$$pure = first.$$pure && second.$$pure;
|
||||
|
||||
return chainedInterceptor;
|
||||
}
|
||||
|
||||
function addInterceptor(parsedExpression, interceptorFn) {
|
||||
if (!interceptorFn) return parsedExpression;
|
||||
|
||||
// Extract any existing interceptors out of the parsedExpression
|
||||
// to ensure the original parsedExpression is always the $$intercepted
|
||||
if (parsedExpression.$$interceptor) {
|
||||
interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn);
|
||||
parsedExpression = parsedExpression.$$intercepted;
|
||||
}
|
||||
|
||||
var useInputs = false;
|
||||
|
||||
var fn = function interceptedExpression(scope, locals, assign, inputs) {
|
||||
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
|
||||
return interceptorFn(value);
|
||||
};
|
||||
|
||||
// Maintain references to the interceptor/intercepted
|
||||
fn.$$intercepted = parsedExpression;
|
||||
fn.$$interceptor = interceptorFn;
|
||||
|
||||
// Propogate the literal/oneTime/constant attributes
|
||||
fn.literal = parsedExpression.literal;
|
||||
fn.oneTime = parsedExpression.oneTime;
|
||||
fn.constant = parsedExpression.constant;
|
||||
|
||||
// Treat the interceptor like filters.
|
||||
// If it is not $stateful then only watch its inputs.
|
||||
// If the expression itself has no inputs then use the full expression as an input.
|
||||
if (!interceptorFn.$stateful) {
|
||||
useInputs = !parsedExpression.inputs;
|
||||
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
|
||||
|
||||
if (!interceptorFn.$$pure) {
|
||||
fn.inputs = fn.inputs.map(function(e) {
|
||||
// Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a
|
||||
// potentially non-pure interceptor function.
|
||||
// non-pure interceptor function.
|
||||
if (e.isPure === PURITY_RELATIVE) {
|
||||
return function depurifier(s) { return e(s); };
|
||||
}
|
||||
return e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fn;
|
||||
return addWatchDelegate(fn);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
+21
-45
@@ -299,8 +299,8 @@ function $RootScopeProvider() {
|
||||
* according to the {@link angular.equals} function. To save the value of the object for
|
||||
* later comparison, the {@link angular.copy} function is used. This therefore means that
|
||||
* watching complex objects will have adverse memory and performance implications.
|
||||
* - This should not be used to watch for changes in objects that are
|
||||
* or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}.
|
||||
* - This should not be used to watch for changes in objects that are (or contain)
|
||||
* [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}.
|
||||
* - The watch `listener` may change the model, which may trigger other `listener`s to fire.
|
||||
* This is achieved by rerunning the watchers until no changes are detected. The rerun
|
||||
* iteration limit is 10 to prevent an infinite loop deadlock.
|
||||
@@ -448,12 +448,6 @@ function $RootScopeProvider() {
|
||||
* values are examined for changes on every call to `$digest`.
|
||||
* - The `listener` is called whenever any expression in the `watchExpressions` array changes.
|
||||
*
|
||||
* `$watchGroup` is more performant than watching each expression individually, and should be
|
||||
* used when the listener does not need to know which expression has changed.
|
||||
* If the listener needs to know which expression has changed,
|
||||
* {@link ng.$rootScope.Scope#$watch $watch()} or
|
||||
* {@link ng.$rootScope.Scope#$watchCollection $watchCollection()} should be used.
|
||||
*
|
||||
* @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually
|
||||
* watched using {@link ng.$rootScope.Scope#$watch $watch()}
|
||||
*
|
||||
@@ -462,34 +456,7 @@ function $RootScopeProvider() {
|
||||
* The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching
|
||||
* those of `watchExpression`
|
||||
* and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching
|
||||
* those of `watchExpression`.
|
||||
*
|
||||
* Note that `newValues` and `oldValues` reflect the differences in each **individual**
|
||||
* expression, and not the difference of the values between each call of the listener.
|
||||
* That means the difference between `newValues` and `oldValues` cannot be used to determine
|
||||
* which expression has changed / remained stable:
|
||||
*
|
||||
* ```js
|
||||
*
|
||||
* $scope.$watchGroup(['v1', 'v2'], function(newValues, oldValues) {
|
||||
* console.log(newValues, oldValues);
|
||||
* });
|
||||
*
|
||||
* // newValues, oldValues initially
|
||||
* // [undefined, undefined], [undefined, undefined]
|
||||
*
|
||||
* $scope.v1 = 'a';
|
||||
* $scope.v2 = 'a';
|
||||
*
|
||||
* // ['a', 'a'], [undefined, undefined]
|
||||
*
|
||||
* $scope.v2 = 'b'
|
||||
*
|
||||
* // v1 hasn't changed since it became `'a'`, therefore its oldValue is still `undefined`
|
||||
* // ['a', 'b'], [undefined, 'a']
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* those of `watchExpression`
|
||||
* The `scope` refers to the current scope.
|
||||
* @returns {function()} Returns a de-registration function for all listeners.
|
||||
*/
|
||||
@@ -522,9 +489,8 @@ function $RootScopeProvider() {
|
||||
}
|
||||
|
||||
forEach(watchExpressions, function(expr, i) {
|
||||
var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) {
|
||||
var unwatchFn = self.$watch(expr, function watchGroupSubAction(value) {
|
||||
newValues[i] = value;
|
||||
oldValues[i] = oldValue;
|
||||
if (!changeReactionScheduled) {
|
||||
changeReactionScheduled = true;
|
||||
self.$evalAsync(watchGroupAction);
|
||||
@@ -536,11 +502,17 @@ function $RootScopeProvider() {
|
||||
function watchGroupAction() {
|
||||
changeReactionScheduled = false;
|
||||
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
listener(newValues, newValues, self);
|
||||
} else {
|
||||
listener(newValues, oldValues, self);
|
||||
try {
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
listener(newValues, newValues, self);
|
||||
} else {
|
||||
listener(newValues, oldValues, self);
|
||||
}
|
||||
} finally {
|
||||
for (var i = 0; i < watchExpressions.length; i++) {
|
||||
oldValues[i] = newValues[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,7 +580,11 @@ function $RootScopeProvider() {
|
||||
* de-registration function is executed, the internal watch operation is terminated.
|
||||
*/
|
||||
$watchCollection: function(obj, listener) {
|
||||
$watchCollectionInterceptor.$stateful = true;
|
||||
// Mark the interceptor as
|
||||
// ... $$pure when literal since the instance will change when any input changes
|
||||
$watchCollectionInterceptor.$$pure = $parse(obj).literal;
|
||||
// ... $stateful when non-literal since we must read the state of the collection
|
||||
$watchCollectionInterceptor.$stateful = !$watchCollectionInterceptor.$$pure;
|
||||
|
||||
var self = this;
|
||||
// the current value, updated on each dirty-check run
|
||||
@@ -796,7 +772,7 @@ function $RootScopeProvider() {
|
||||
var watch, value, last, fn, get,
|
||||
watchers,
|
||||
dirty, ttl = TTL,
|
||||
next, current, target = this,
|
||||
next, current, target = asyncQueue.length ? $rootScope : this,
|
||||
watchLog = [],
|
||||
logIdx, asyncTask;
|
||||
|
||||
|
||||
+24
-14
@@ -6,6 +6,7 @@
|
||||
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
|
||||
*/
|
||||
function $$SanitizeUriProvider() {
|
||||
|
||||
var aHrefSanitizationWhitelist = /^\s*(https?|s?ftp|mailto|tel|file):/,
|
||||
imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
|
||||
|
||||
@@ -14,12 +15,16 @@ function $$SanitizeUriProvider() {
|
||||
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
|
||||
* urls during a[href] sanitization.
|
||||
*
|
||||
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
|
||||
* The sanitization is a security measure aimed at prevent XSS attacks via HTML anchor links.
|
||||
*
|
||||
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
|
||||
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
|
||||
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
|
||||
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
|
||||
* Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring
|
||||
* the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)`
|
||||
* which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL.
|
||||
*
|
||||
* If the URL matches the `aHrefSanitizationWhitelist` regular expression, it is returned unchanged.
|
||||
*
|
||||
* If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
|
||||
* to the DOM it is inactive and potentially malicious code will not be executed.
|
||||
*
|
||||
* @param {RegExp=} regexp New regexp to whitelist urls with.
|
||||
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
|
||||
@@ -39,12 +44,17 @@ function $$SanitizeUriProvider() {
|
||||
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
|
||||
* urls during img[src] sanitization.
|
||||
*
|
||||
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
|
||||
* The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links.
|
||||
*
|
||||
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into
|
||||
* an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
|
||||
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
|
||||
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
|
||||
* Any URL due to be assigned to an `img[src]` attribute via interpolation is marked as requiring
|
||||
* the $sce.MEDIA_URL security context. When interpolation occurs a call is made to
|
||||
* `$sce.trustAsMediaUrl(url)` which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize
|
||||
* the potentially malicious URL.
|
||||
*
|
||||
* If the URL matches the `aImgSanitizationWhitelist` regular expression, it is returned unchanged.
|
||||
*
|
||||
* If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
|
||||
* to the DOM it is inactive and potentially malicious code will not be executed.
|
||||
*
|
||||
* @param {RegExp=} regexp New regexp to whitelist urls with.
|
||||
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
|
||||
@@ -59,10 +69,10 @@ function $$SanitizeUriProvider() {
|
||||
};
|
||||
|
||||
this.$get = function() {
|
||||
return function sanitizeUri(uri, isImage) {
|
||||
var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
|
||||
var normalizedVal;
|
||||
normalizedVal = urlResolve(uri && uri.trim()).href;
|
||||
return function sanitizeUri(uri, isMediaUrl) {
|
||||
// if (!uri) return uri;
|
||||
var regex = isMediaUrl ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
|
||||
var normalizedVal = urlResolve(uri && uri.trim()).href;
|
||||
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
|
||||
return 'unsafe:' + normalizedVal;
|
||||
}
|
||||
|
||||
+67
-32
@@ -22,12 +22,17 @@ var SCE_CONTEXTS = {
|
||||
// Style statements or stylesheets. Currently unused in AngularJS.
|
||||
CSS: 'css',
|
||||
|
||||
// An URL used in a context where it does not refer to a resource that loads code. Currently
|
||||
// unused in AngularJS.
|
||||
// An URL used in a context where it refers to the source of media, which are not expected to be run
|
||||
// as scripts, such as an image, audio, video, etc.
|
||||
MEDIA_URL: 'mediaUrl',
|
||||
|
||||
// An URL used in a context where it does not refer to a resource that loads code.
|
||||
// A value that can be trusted as a URL can also trusted as a MEDIA_URL.
|
||||
URL: 'url',
|
||||
|
||||
// RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as
|
||||
// code. (e.g. ng-include, script src binding, templateUrl)
|
||||
// A value that can be trusted as a RESOURCE_URL, can also trusted as a URL and a MEDIA_URL.
|
||||
RESOURCE_URL: 'resourceUrl',
|
||||
|
||||
// Script. Currently unused in AngularJS.
|
||||
@@ -242,7 +247,7 @@ function $SceDelegateProvider() {
|
||||
return resourceUrlBlacklist;
|
||||
};
|
||||
|
||||
this.$get = ['$injector', function($injector) {
|
||||
this.$get = ['$injector', '$$sanitizeUri', function($injector, $$sanitizeUri) {
|
||||
|
||||
var htmlSanitizer = function htmlSanitizer(html) {
|
||||
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
@@ -255,7 +260,7 @@ function $SceDelegateProvider() {
|
||||
|
||||
function matchUrl(matcher, parsedUrl) {
|
||||
if (matcher === 'self') {
|
||||
return urlIsSameOrigin(parsedUrl);
|
||||
return urlIsSameOrigin(parsedUrl) || urlIsSameOriginAsBaseUrl(parsedUrl);
|
||||
} else {
|
||||
// definitely a regex. See adjustMatchers()
|
||||
return !!matcher.exec(parsedUrl.href);
|
||||
@@ -307,7 +312,8 @@ function $SceDelegateProvider() {
|
||||
|
||||
byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase);
|
||||
byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase);
|
||||
byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase);
|
||||
byType[SCE_CONTEXTS.MEDIA_URL] = generateHolderType(trustedValueHolderBase);
|
||||
byType[SCE_CONTEXTS.URL] = generateHolderType(byType[SCE_CONTEXTS.MEDIA_URL]);
|
||||
byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase);
|
||||
byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]);
|
||||
|
||||
@@ -386,15 +392,27 @@ function $SceDelegateProvider() {
|
||||
* @name $sceDelegate#getTrusted
|
||||
*
|
||||
* @description
|
||||
* Takes any input, and either returns a value that's safe to use in the specified context, or
|
||||
* throws an exception.
|
||||
* Given an object and a security context in which to assign it, returns a value that's safe to
|
||||
* use in this context, which was represented by the parameter. To do so, this function either
|
||||
* unwraps the safe type it has been given (for instance, a {@link ng.$sceDelegate#trustAs
|
||||
* `$sceDelegate.trustAs`} result), or it might try to sanitize the value given, depending on
|
||||
* the context and sanitizer availablility.
|
||||
*
|
||||
* In practice, there are several cases. When given a string, this function runs checks
|
||||
* and sanitization to make it safe without prior assumptions. When given the result of a {@link
|
||||
* ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied
|
||||
* value if that value's context is valid for this call's context. Finally, this function can
|
||||
* also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization
|
||||
* is available or possible.)
|
||||
* The contexts that can be sanitized are $sce.MEDIA_URL, $sce.URL and $sce.HTML. The first two are available
|
||||
* by default, and the third one relies on the `$sanitize` service (which may be loaded through
|
||||
* the `ngSanitize` module). Furthermore, for $sce.RESOURCE_URL context, a plain string may be
|
||||
* accepted if the resource url policy defined by {@link ng.$sceDelegateProvider#resourceUrlWhitelist
|
||||
* `$sceDelegateProvider.resourceUrlWhitelist`} and {@link ng.$sceDelegateProvider#resourceUrlBlacklist
|
||||
* `$sceDelegateProvider.resourceUrlBlacklist`} accepts that resource.
|
||||
*
|
||||
* This function will throw if the safe type isn't appropriate for this context, or if the
|
||||
* value given cannot be accepted in the context (which might be caused by sanitization not
|
||||
* being available, or the value not being recognized as safe).
|
||||
*
|
||||
* <div class="alert alert-danger">
|
||||
* Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
|
||||
* (XSS) vulnerability in your application.
|
||||
* </div>
|
||||
*
|
||||
* @param {string} type The context in which this value is to be used (such as `$sce.HTML`).
|
||||
* @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs
|
||||
@@ -412,12 +430,18 @@ function $SceDelegateProvider() {
|
||||
if (constructor && maybeTrusted instanceof constructor) {
|
||||
return maybeTrusted.$$unwrapTrustedValue();
|
||||
}
|
||||
// Otherwise, if we get here, then we may either make it safe, or throw an exception. This
|
||||
// depends on the context: some are sanitizatible (HTML), some use whitelists (RESOURCE_URL),
|
||||
// some are impossible to do (JS). This step isn't implemented for CSS and URL, as AngularJS
|
||||
// has no corresponding sinks.
|
||||
if (type === SCE_CONTEXTS.RESOURCE_URL) {
|
||||
// RESOURCE_URL uses a whitelist.
|
||||
|
||||
// If maybeTrusted is a trusted class instance but not of the correct trusted type
|
||||
// then unwrap it and allow it to pass through to the rest of the checks
|
||||
if (isFunction(maybeTrusted.$$unwrapTrustedValue)) {
|
||||
maybeTrusted = maybeTrusted.$$unwrapTrustedValue();
|
||||
}
|
||||
|
||||
// If we get here, then we will either sanitize the value or throw an exception.
|
||||
if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) {
|
||||
// we attempt to sanitize non-resource URLs
|
||||
return $$sanitizeUri(maybeTrusted, type === SCE_CONTEXTS.MEDIA_URL);
|
||||
} else if (type === SCE_CONTEXTS.RESOURCE_URL) {
|
||||
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
|
||||
return maybeTrusted;
|
||||
} else {
|
||||
@@ -572,9 +596,10 @@ function $SceDelegateProvider() {
|
||||
*
|
||||
* If your expressions are constant literals, they're automatically trusted and you don't need to
|
||||
* call `$sce.trustAs` on them (e.g.
|
||||
* `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works. The `$sceDelegate` will
|
||||
* also use the `$sanitize` service if it is available when binding untrusted values to
|
||||
* `$sce.HTML` context. AngularJS provides an implementation in `angular-sanitize.js`, and if you
|
||||
* `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works (remember to include the
|
||||
* `ngSanitize` module). The `$sceDelegate` will also use the `$sanitize` service if it is available
|
||||
* when binding untrusted values to `$sce.HTML` context.
|
||||
* AngularJS provides an implementation in `angular-sanitize.js`, and if you
|
||||
* wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in
|
||||
* your application.
|
||||
*
|
||||
@@ -594,17 +619,27 @@ function $SceDelegateProvider() {
|
||||
*
|
||||
* | Context | Notes |
|
||||
* |---------------------|----------------|
|
||||
* | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered, and the {@link ngSanitize.$sanitize $sanitize} service is available (implemented by the {@link ngSanitize ngSanitize} module) this will sanitize the value instead of throwing an error. |
|
||||
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently, no bindings require this context. Feel free to use it in your own directives. |
|
||||
* | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=`, `<img src=`, and some others sanitize their urls and don't constitute an SCE context.) |
|
||||
* | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG`, `VIDEO`, `AUDIO`, `SOURCE`, and `TRACK` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does (it's not just the URL that matters, but also what is at the end of it), and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
|
||||
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently, no bindings require this context. Feel free to use it in your own directives. |
|
||||
* | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. |
|
||||
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
|
||||
* | `$sce.MEDIA_URL` | For URLs that are safe to render as media. Is automatically converted from string by sanitizing when needed. |
|
||||
* | `$sce.URL` | For URLs that are safe to follow as links. Is automatically converted from string by sanitizing when needed. Note that `$sce.URL` makes a stronger statement about the URL than `$sce.MEDIA_URL` does and therefore contexts requiring values trusted for `$sce.URL` can be used anywhere that values trusted for `$sce.MEDIA_URL` are required.|
|
||||
* | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` or `$sce.MEDIA_URL` do and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` or `$sce.MEDIA_URL` are required. |
|
||||
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
|
||||
*
|
||||
*
|
||||
* Be aware that `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them
|
||||
* through {@link ng.$sce#getTrusted $sce.getTrusted}. There's no CSS-, URL-, or JS-context bindings
|
||||
* in AngularJS currently, so their corresponding `$sce.trustAs` functions aren't useful yet. This
|
||||
* might evolve.
|
||||
* <div class="alert alert-warning">
|
||||
* Be aware that, before AngularJS 1.7.0, `a[href]` and `img[src]` used to sanitize their
|
||||
* interpolated values directly rather than rely upon {@link ng.$sce#getTrusted `$sce.getTrusted`}.
|
||||
*
|
||||
* **As of 1.7.0, this is no longer the case.**
|
||||
*
|
||||
* Now such interpolations are marked as requiring `$sce.URL` (for `a[href]`) or `$sce.MEDIA_URL`
|
||||
* (for `img[src]`), so that the sanitization happens (via `$sce.getTrusted...`) when the `$interpolate`
|
||||
* service evaluates the expressions.
|
||||
* </div>
|
||||
*
|
||||
* There are no CSS or JS context bindings in AngularJS currently, so their corresponding `$sce.trustAs`
|
||||
* functions aren't useful yet. This might evolve.
|
||||
*
|
||||
* ### Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a>
|
||||
*
|
||||
@@ -778,7 +813,7 @@ function $SceProvider() {
|
||||
* such a value.
|
||||
*
|
||||
* - getTrusted(contextEnum, value)
|
||||
* This function should return the a value that is safe to use in the context specified by
|
||||
* This function should return the value that is safe to use in the context specified by
|
||||
* contextEnum or throw and exception otherwise.
|
||||
*
|
||||
* NOTE: This contract deliberately does NOT state that values returned by trustAs() must be
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var $templateRequestMinErr = minErr('$compile');
|
||||
var $templateRequestMinErr = minErr('$templateRequest');
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
@@ -99,8 +99,7 @@ function $TemplateRequestProvider() {
|
||||
handleRequestFn.totalPendingRequests--;
|
||||
})
|
||||
.then(function(response) {
|
||||
$templateCache.put(tpl, response.data);
|
||||
return response.data;
|
||||
return $templateCache.put(tpl, response.data);
|
||||
}, handleError);
|
||||
|
||||
function handleError(resp) {
|
||||
|
||||
+19
-7
@@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var $timeoutMinErr = minErr('$timeout');
|
||||
|
||||
/** @this */
|
||||
function $TimeoutProvider() {
|
||||
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
|
||||
@@ -83,14 +85,24 @@ function $TimeoutProvider() {
|
||||
* canceled.
|
||||
*/
|
||||
timeout.cancel = function(promise) {
|
||||
if (promise && promise.$$timeoutId in deferreds) {
|
||||
// Timeout cancels should not report an unhandled promise.
|
||||
markQExceptionHandled(deferreds[promise.$$timeoutId].promise);
|
||||
deferreds[promise.$$timeoutId].reject('canceled');
|
||||
delete deferreds[promise.$$timeoutId];
|
||||
return $browser.defer.cancel(promise.$$timeoutId);
|
||||
if (!promise) return false;
|
||||
|
||||
if (!promise.hasOwnProperty('$$timeoutId')) {
|
||||
throw $timeoutMinErr('badprom',
|
||||
'`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.');
|
||||
}
|
||||
return false;
|
||||
|
||||
if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false;
|
||||
|
||||
var id = promise.$$timeoutId;
|
||||
var deferred = deferreds[id];
|
||||
|
||||
// Timeout cancels should not report an unhandled promise.
|
||||
markQExceptionHandled(deferred.promise);
|
||||
deferred.reject('canceled');
|
||||
delete deferreds[id];
|
||||
|
||||
return $browser.defer.cancel(id);
|
||||
};
|
||||
|
||||
return timeout;
|
||||
|
||||
+43
-8
@@ -8,6 +8,7 @@
|
||||
// service.
|
||||
var urlParsingNode = window.document.createElement('a');
|
||||
var originUrl = urlResolve(window.location.href);
|
||||
var baseUrlParsingNode;
|
||||
|
||||
|
||||
/**
|
||||
@@ -44,16 +45,16 @@ var originUrl = urlResolve(window.location.href);
|
||||
* @description Normalizes and parses a URL.
|
||||
* @returns {object} Returns the normalized URL as a dictionary.
|
||||
*
|
||||
* | member name | Description |
|
||||
* |---------------|----------------|
|
||||
* | member name | Description |
|
||||
* |---------------|------------------------------------------------------------------------|
|
||||
* | href | A normalized version of the provided URL if it was not an absolute URL |
|
||||
* | protocol | The protocol including the trailing colon |
|
||||
* | protocol | The protocol without the trailing colon |
|
||||
* | host | The host and port (if the port is non-default) of the normalizedUrl |
|
||||
* | search | The search params, minus the question mark |
|
||||
* | hash | The hash string, minus the hash symbol
|
||||
* | hostname | The hostname
|
||||
* | port | The port, without ":"
|
||||
* | pathname | The pathname, beginning with "/"
|
||||
* | hash | The hash string, minus the hash symbol |
|
||||
* | hostname | The hostname |
|
||||
* | port | The port, without ":" |
|
||||
* | pathname | The pathname, beginning with "/" |
|
||||
*
|
||||
*/
|
||||
function urlResolve(url) {
|
||||
@@ -71,7 +72,6 @@ function urlResolve(url) {
|
||||
|
||||
urlParsingNode.setAttribute('href', href);
|
||||
|
||||
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
|
||||
return {
|
||||
href: urlParsingNode.href,
|
||||
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
|
||||
@@ -98,6 +98,20 @@ function urlIsSameOrigin(requestUrl) {
|
||||
return urlsAreSameOrigin(requestUrl, originUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a request URL and determine whether it is same-origin as the current document base URL.
|
||||
*
|
||||
* Note: The base URL is usually the same as the document location (`location.href`) but can
|
||||
* be overriden by using the `<base>` tag.
|
||||
*
|
||||
* @param {string|object} requestUrl The url of the request as a string that will be resolved
|
||||
* or a parsed URL object.
|
||||
* @returns {boolean} Whether the URL is same-origin as the document base URL.
|
||||
*/
|
||||
function urlIsSameOriginAsBaseUrl(requestUrl) {
|
||||
return urlsAreSameOrigin(requestUrl, getBaseUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a function that can check a URL's origin against a list of allowed/whitelisted origins.
|
||||
* The current location's origin is implicitly trusted.
|
||||
@@ -143,3 +157,24 @@ function urlsAreSameOrigin(url1, url2) {
|
||||
return (url1.protocol === url2.protocol &&
|
||||
url1.host === url2.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current document base URL.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
if (window.document.baseURI) {
|
||||
return window.document.baseURI;
|
||||
}
|
||||
|
||||
// `document.baseURI` is available everywhere except IE
|
||||
if (!baseUrlParsingNode) {
|
||||
baseUrlParsingNode = window.document.createElement('a');
|
||||
baseUrlParsingNode.href = '.';
|
||||
|
||||
// Work-around for IE bug described in Implementation Notes. The fix in `urlResolve()` is not
|
||||
// suitable here because we need to track changes to the base URL.
|
||||
baseUrlParsingNode = baseUrlParsingNode.cloneNode(false);
|
||||
}
|
||||
return baseUrlParsingNode.href;
|
||||
}
|
||||
|
||||
+4
-1
@@ -224,7 +224,10 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
.directive('ngModel', ['$aria', function($aria) {
|
||||
|
||||
function shouldAttachAttr(attr, normalizedAttr, elem, allowBlacklistEls) {
|
||||
return $aria.config(normalizedAttr) && !elem.attr(attr) && (allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList));
|
||||
return $aria.config(normalizedAttr) &&
|
||||
!elem.attr(attr) &&
|
||||
(allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList)) &&
|
||||
(elem.attr('type') !== 'hidden' || elem[0].nodeName !== 'INPUT');
|
||||
}
|
||||
|
||||
function shouldAttachRole(role, elem) {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ngCookies').
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $cookieStore
|
||||
* @deprecated
|
||||
* sinceVersion="v1.4.0"
|
||||
* Please use the {@link ngCookies.$cookies `$cookies`} service instead.
|
||||
*
|
||||
* @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 AngularJS's `toJson`/`fromJson`.
|
||||
*
|
||||
* Requires the {@link ngCookies `ngCookies`} module to be installed.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* angular.module('cookieStoreExample', ['ngCookies'])
|
||||
* .controller('ExampleController', ['$cookieStore', function($cookieStore) {
|
||||
* // Put cookie
|
||||
* $cookieStore.put('myFavorite','oatmeal');
|
||||
* // Get cookie
|
||||
* var favoriteCookie = $cookieStore.get('myFavorite');
|
||||
* // Removing a cookie
|
||||
* $cookieStore.remove('myFavorite');
|
||||
* }]);
|
||||
* ```
|
||||
*/
|
||||
factory('$cookieStore', ['$cookies', function($cookies) {
|
||||
|
||||
return {
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $cookieStore#get
|
||||
*
|
||||
* @description
|
||||
* Returns the value of given cookie key
|
||||
*
|
||||
* @param {string} key Id to use for lookup.
|
||||
* @returns {Object} Deserialized cookie value, undefined if the cookie does not exist.
|
||||
*/
|
||||
get: function(key) {
|
||||
return $cookies.getObject(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $cookieStore#put
|
||||
*
|
||||
* @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) {
|
||||
$cookies.putObject(key, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $cookieStore#remove
|
||||
*
|
||||
* @description
|
||||
* Remove given cookie
|
||||
*
|
||||
* @param {string} key Id of the key-value pair to delete.
|
||||
*/
|
||||
remove: function(key) {
|
||||
$cookies.remove(key);
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
Vendored
+4
-20
@@ -2094,13 +2094,13 @@ function MockXhr() {
|
||||
var header = this.$$respHeaders[name];
|
||||
if (header) return header;
|
||||
|
||||
name = angular.lowercase(name);
|
||||
name = angular.$$lowercase(name);
|
||||
header = this.$$respHeaders[name];
|
||||
if (header) return header;
|
||||
|
||||
header = undefined;
|
||||
angular.forEach(this.$$respHeaders, function(headerVal, headerName) {
|
||||
if (!header && angular.lowercase(headerName) === name) header = headerVal;
|
||||
if (!header && angular.$$lowercase(headerName) === name) header = headerVal;
|
||||
});
|
||||
return header;
|
||||
};
|
||||
@@ -2232,11 +2232,6 @@ angular.mock.$RootElementProvider = function() {
|
||||
* A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing
|
||||
* controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}.
|
||||
*
|
||||
* Depending on the value of
|
||||
* {@link ng.$compileProvider#preAssignBindingsEnabled `preAssignBindingsEnabled()`}, the properties
|
||||
* will be bound before or after invoking the constructor.
|
||||
*
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```js
|
||||
@@ -2282,8 +2277,6 @@ angular.mock.$RootElementProvider = function() {
|
||||
*
|
||||
* * check if a controller with given name is registered via `$controllerProvider`
|
||||
* * check if evaluating the string on the current scope returns a constructor
|
||||
* * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
|
||||
* `window` object (deprecated, not recommended)
|
||||
*
|
||||
* The string can use the `controller as property` syntax, where the controller instance is published
|
||||
* as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
|
||||
@@ -2294,22 +2287,13 @@ angular.mock.$RootElementProvider = function() {
|
||||
* the `bindToController` feature and simplify certain kinds of tests.
|
||||
* @return {Object} Instance of given controller.
|
||||
*/
|
||||
function createControllerDecorator(compileProvider) {
|
||||
function createControllerDecorator() {
|
||||
angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
|
||||
return function(expression, locals, later, ident) {
|
||||
if (later && typeof later === 'object') {
|
||||
var preAssignBindingsEnabled = compileProvider.preAssignBindingsEnabled();
|
||||
|
||||
var instantiate = $delegate(expression, locals, true, ident);
|
||||
if (preAssignBindingsEnabled) {
|
||||
angular.extend(instantiate.instance, later);
|
||||
}
|
||||
|
||||
var instance = instantiate();
|
||||
if (!preAssignBindingsEnabled || instance !== instantiate.instance) {
|
||||
angular.extend(instance, later);
|
||||
}
|
||||
|
||||
angular.extend(instance, later);
|
||||
return instance;
|
||||
}
|
||||
return $delegate(expression, locals, later, ident);
|
||||
|
||||
@@ -165,30 +165,11 @@
|
||||
|
||||
if (!evnt) return;
|
||||
|
||||
var originalPreventDefault = evnt.preventDefault,
|
||||
appWindow = element.ownerDocument.defaultView,
|
||||
fakeProcessDefault = true,
|
||||
finalProcessDefault,
|
||||
angular = appWindow.angular || {};
|
||||
|
||||
// igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208
|
||||
angular['ff-684208-preventDefault'] = false;
|
||||
evnt.preventDefault = function() {
|
||||
fakeProcessDefault = false;
|
||||
return originalPreventDefault.apply(evnt, arguments);
|
||||
};
|
||||
|
||||
if (!eventData.bubbles || supportsEventBubblingInDetachedTree() || isAttachedToDocument(element)) {
|
||||
element.dispatchEvent(evnt);
|
||||
return element.dispatchEvent(evnt);
|
||||
} else {
|
||||
triggerForPath(element, evnt);
|
||||
}
|
||||
|
||||
finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault);
|
||||
|
||||
delete angular['ff-684208-preventDefault'];
|
||||
|
||||
return finalProcessDefault;
|
||||
};
|
||||
|
||||
function supportsTouchEvents() {
|
||||
|
||||
+231
-177
@@ -110,13 +110,13 @@ function shallowClearAndCopy(src, dst) {
|
||||
*
|
||||
* @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
|
||||
* `actions` methods. If a parameter value is a function, it will be called every time
|
||||
* a param value needs to be obtained for a request (unless the param was overridden). The function
|
||||
* will be passed the current data value as an argument.
|
||||
* a param value needs to be obtained for a request (unless the param was overridden). The
|
||||
* function will be passed the current data value as an argument.
|
||||
*
|
||||
* Each key value in the parameter object is first bound to url template if present and then any
|
||||
* excess keys are appended to the url search query after the `?`.
|
||||
*
|
||||
* Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
|
||||
* Given a template `/path/:verb` and parameter `{verb: 'greet', salutation: 'Hello'}` results in
|
||||
* URL `/path/greet?salutation=Hello`.
|
||||
*
|
||||
* If the parameter value is prefixed with `@`, then the value for that parameter will be
|
||||
@@ -125,7 +125,7 @@ function shallowClearAndCopy(src, dst) {
|
||||
* For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of
|
||||
* `someParam` will be `data.someProp`.
|
||||
* Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action
|
||||
* method that does not accept a request body)
|
||||
* method that does not accept a request body).
|
||||
*
|
||||
* @param {Object.<Object>=} actions Hash with declaration of custom actions that will be available
|
||||
* in addition to the default set of resource actions (see below). If a custom action has the same
|
||||
@@ -134,9 +134,11 @@ function shallowClearAndCopy(src, dst) {
|
||||
*
|
||||
* The declaration should be created in the format of {@link ng.$http#usage $http.config}:
|
||||
*
|
||||
* {action1: {method:?, params:?, isArray:?, headers:?, ...},
|
||||
* action2: {method:?, params:?, isArray:?, headers:?, ...},
|
||||
* ...}
|
||||
* {
|
||||
* action1: {method:?, params:?, isArray:?, headers:?, ...},
|
||||
* action2: {method:?, params:?, isArray:?, headers:?, ...},
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Where:
|
||||
*
|
||||
@@ -148,54 +150,58 @@ function shallowClearAndCopy(src, dst) {
|
||||
* the parameter value is a function, it will be called every time when a param value needs to
|
||||
* be obtained for a request (unless the param was overridden). The function will be passed the
|
||||
* current data value as an argument.
|
||||
* - **`url`** – {string} – action specific `url` override. The url templating is supported just
|
||||
* - **`url`** – {string} – Action specific `url` override. The url templating is supported just
|
||||
* like for the resource-level urls.
|
||||
* - **`isArray`** – {boolean=} – If true then the returned object for this action is an array,
|
||||
* see `returns` section.
|
||||
* - **`transformRequest`** –
|
||||
* `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
|
||||
* transform function or an array of such functions. The transform function takes the http
|
||||
* Transform function or an array of such functions. The transform function takes the http
|
||||
* request body and headers and returns its transformed (typically serialized) version.
|
||||
* By default, transformRequest will contain one function that checks if the request data is
|
||||
* an object and serializes it using `angular.toJson`. To prevent this behavior, set
|
||||
* `transformRequest` to an empty array: `transformRequest: []`
|
||||
* - **`transformResponse`** –
|
||||
* `{function(data, headersGetter, status)|Array.<function(data, headersGetter, status)>}` –
|
||||
* transform function or an array of such functions. The transform function takes the http
|
||||
* Transform function or an array of such functions. The transform function takes the HTTP
|
||||
* response body, headers and status and returns its transformed (typically deserialized)
|
||||
* version.
|
||||
* By default, transformResponse will contain one function that checks if the response looks
|
||||
* like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior,
|
||||
* set `transformResponse` to an empty array: `transformResponse: []`
|
||||
* - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
|
||||
* GET request, otherwise if a cache instance built with
|
||||
* {@link ng.$cacheFactory $cacheFactory} is supplied, this cache will be used for
|
||||
* caching.
|
||||
* - **`timeout`** – `{number}` – timeout in milliseconds.<br />
|
||||
* - **`cache`** – `{boolean|Cache}` – A boolean value or object created with
|
||||
* {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response.
|
||||
* See {@link $http#caching $http Caching} for more information.
|
||||
* - **`timeout`** – `{number}` – Timeout in milliseconds.<br />
|
||||
* **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are
|
||||
* **not** supported in $resource, because the same value would be used for multiple requests.
|
||||
* **not** supported in `$resource`, because the same value would be used for multiple requests.
|
||||
* If you are looking for a way to cancel requests, you should use the `cancellable` option.
|
||||
* - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call
|
||||
* will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's
|
||||
* return value. Calling `$cancelRequest()` for a non-cancellable or an already
|
||||
* completed/cancelled request will have no effect.<br />
|
||||
* - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
|
||||
* - **`cancellable`** – `{boolean}` – If true, the request made by a "non-instance" call will be
|
||||
* cancelled (if not already completed) by calling `$cancelRequest()` on the call's return
|
||||
* value. Calling `$cancelRequest()` for a non-cancellable or an already completed/cancelled
|
||||
* request will have no effect.
|
||||
* - **`withCredentials`** – `{boolean}` – Whether to set the `withCredentials` flag on the
|
||||
* XHR object. See
|
||||
* [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5)
|
||||
* [XMLHttpRequest.withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
|
||||
* for more information.
|
||||
* - **`responseType`** - `{string}` - see
|
||||
* [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
|
||||
* - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
|
||||
* `response` and `responseError`. Both `response` and `responseError` interceptors get called
|
||||
* with `http response` object. See {@link ng.$http $http interceptors}. In addition, the
|
||||
* resource instance or array object is accessible by the `resource` property of the
|
||||
* `http response` object.
|
||||
* - **`responseType`** – `{string}` – See
|
||||
* [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType).
|
||||
* - **`interceptor`** – `{Object=}` – The interceptor object has four optional methods -
|
||||
* `request`, `requestError`, `response`, and `responseError`. See
|
||||
* {@link ng.$http#interceptors $http interceptors} for details. Note that
|
||||
* `request`/`requestError` interceptors are applied before calling `$http`, thus before any
|
||||
* global `$http` interceptors. Also, rejecting or throwing an error inside the `request`
|
||||
* interceptor will result in calling the `responseError` interceptor.
|
||||
* The resource instance or collection is available on the `resource` property of the
|
||||
* `http response` object passed to `response`/`responseError` interceptors.
|
||||
* Keep in mind that the associated promise will be resolved with the value returned by the
|
||||
* response interceptor, if one is specified. The default response interceptor returns
|
||||
* `response.resource` (i.e. the resource instance or array).
|
||||
* - **`hasBody`** - `{boolean}` - allows to specify if a request body should be included or not.
|
||||
* If not specified only POST, PUT and PATCH requests will have a body.
|
||||
*
|
||||
* response interceptors. Make sure you return an appropriate value and not the `response`
|
||||
* object passed as input. For reference, the default `response` interceptor (which gets applied
|
||||
* if you don't specify a custom one) returns `response.resource`.<br />
|
||||
* See {@link ngResource.$resource#using-interceptors below} for an example of using
|
||||
* interceptors in `$resource`.
|
||||
* - **`hasBody`** – `{boolean}` – If true, then the request will have a body.
|
||||
* If not specified, then only POST, PUT and PATCH requests will have a body. *
|
||||
* @param {Object} options Hash with custom settings that should extend the
|
||||
* default `$resourceProvider` behavior. The supported options are:
|
||||
*
|
||||
@@ -208,27 +214,29 @@ function shallowClearAndCopy(src, dst) {
|
||||
* @returns {Object} A resource "class" object with methods for the default set of resource actions
|
||||
* optionally extended with custom `actions`. The default set contains these actions:
|
||||
* ```js
|
||||
* { 'get': {method:'GET'},
|
||||
* 'save': {method:'POST'},
|
||||
* 'query': {method:'GET', isArray:true},
|
||||
* 'remove': {method:'DELETE'},
|
||||
* 'delete': {method:'DELETE'} };
|
||||
* {
|
||||
* 'get': {method: 'GET'},
|
||||
* 'save': {method: 'POST'},
|
||||
* 'query': {method: 'GET', isArray: true},
|
||||
* 'remove': {method: 'DELETE'},
|
||||
* 'delete': {method: 'DELETE'}
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Calling these methods invoke an {@link ng.$http} with the specified http method,
|
||||
* destination and parameters. When the data is returned from the server then the object is an
|
||||
* instance of the resource class. The actions `save`, `remove` and `delete` are available on it
|
||||
* as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
|
||||
* read, update, delete) on server-side data like this:
|
||||
* Calling these methods invoke {@link ng.$http} with the specified http method, destination and
|
||||
* parameters. When the data is returned from the server then the object is an instance of the
|
||||
* resource class. The actions `save`, `remove` and `delete` are available on it as methods with
|
||||
* the `$` prefix. This allows you to easily perform CRUD operations (create, read, update,
|
||||
* delete) on server-side data like this:
|
||||
* ```js
|
||||
* var User = $resource('/user/:userId', {userId:'@id'});
|
||||
* var user = User.get({userId:123}, function() {
|
||||
* var User = $resource('/user/:userId', {userId: '@id'});
|
||||
* User.get({userId: 123}).$promise.then(function(user) {
|
||||
* user.abc = true;
|
||||
* user.$save();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* It is important to realize that invoking a $resource object method immediately returns an
|
||||
* It is important to realize that invoking a `$resource` object method immediately returns an
|
||||
* empty reference (object or array depending on `isArray`). Once the data is returned from the
|
||||
* server the existing reference is populated with the actual data. This is a useful trick since
|
||||
* usually the resource is assigned to a model which is then rendered by the view. Having an empty
|
||||
@@ -251,30 +259,31 @@ function shallowClearAndCopy(src, dst) {
|
||||
*
|
||||
*
|
||||
* Success callback is called with (value (Object|Array), responseHeaders (Function),
|
||||
* status (number), statusText (string)) arguments, where the value is the populated resource
|
||||
* status (number), statusText (string)) arguments, where `value` is the populated resource
|
||||
* instance or collection object. The error callback is called with (httpResponse) argument.
|
||||
*
|
||||
* Class actions return empty instance (with additional properties below).
|
||||
* Instance actions return promise of the action.
|
||||
* Class actions return an empty instance (with the additional properties listed below).
|
||||
* Instance actions return a promise for the operation.
|
||||
*
|
||||
* The Resource instances and collections have these additional properties:
|
||||
*
|
||||
* - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
|
||||
* - `$promise`: The {@link ng.$q promise} of the original server interaction that created this
|
||||
* instance or collection.
|
||||
*
|
||||
* On success, the promise is resolved with the same resource instance or collection object,
|
||||
* updated with data from server. This makes it easy to use in
|
||||
* {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view
|
||||
* updated with data from server. This makes it easy to use in the
|
||||
* {@link ngRoute.$routeProvider `resolve` section of `$routeProvider.when()`} to defer view
|
||||
* rendering until the resource(s) are loaded.
|
||||
*
|
||||
* On failure, the promise is rejected with the {@link ng.$http http response} object.
|
||||
*
|
||||
* If an interceptor object was provided, the promise will instead be resolved with the value
|
||||
* returned by the interceptor.
|
||||
* returned by the response interceptor (on success) or responceError interceptor (on failure).
|
||||
*
|
||||
* - `$resolved`: `true` after first server interaction is completed (either with success or
|
||||
* rejection), `false` before that. Knowing if the Resource has been resolved is useful in
|
||||
* data-binding.
|
||||
* data-binding. If there is a response/responseError interceptor and it returns a promise,
|
||||
* `$resolved` will wait for that too.
|
||||
*
|
||||
* The Resource instances and collections have these additional methods:
|
||||
*
|
||||
@@ -291,121 +300,128 @@ function shallowClearAndCopy(src, dst) {
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ### Credit card resource
|
||||
* ### Basic usage
|
||||
*
|
||||
* ```js
|
||||
// Define CreditCard class
|
||||
var CreditCard = $resource('/user/:userId/card/:cardId',
|
||||
{userId:123, cardId:'@id'}, {
|
||||
charge: {method:'POST', params:{charge:true}}
|
||||
});
|
||||
```js
|
||||
// Define a CreditCard class
|
||||
var CreditCard = $resource('/users/:userId/cards/:cardId',
|
||||
{userId: 123, cardId: '@id'}, {
|
||||
charge: {method: 'POST', params: {charge: true}}
|
||||
});
|
||||
|
||||
// We can retrieve a collection from the server
|
||||
var cards = CreditCard.query(function() {
|
||||
// GET: /user/123/card
|
||||
// server returns: [ {id:456, number:'1234', name:'Smith'} ];
|
||||
var cards = CreditCard.query();
|
||||
// GET: /users/123/cards
|
||||
// server returns: [{id: 456, number: '1234', name: 'Smith'}]
|
||||
|
||||
// Wait for the request to complete
|
||||
cards.$promise.then(function() {
|
||||
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'}
|
||||
// Each item is an instance of CreditCard
|
||||
expect(card instanceof CreditCard).toEqual(true);
|
||||
|
||||
// Non-GET methods are mapped onto the instances
|
||||
card.name = 'J. Smith';
|
||||
card.$save();
|
||||
// POST: /users/123/cards/456 {id: 456, number: '1234', name: 'J. Smith'}
|
||||
// server returns: {id: 456, number: '1234', name: 'J. Smith'}
|
||||
|
||||
// Our custom method is mapped as well (since it uses POST)
|
||||
card.$charge({amount: 9.99});
|
||||
// POST: /users/123/cards/456?amount=9.99&charge=true {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:'0123', name: 'Mike Smith'};
|
||||
expect(newCard.id).toEqual(789);
|
||||
* ```
|
||||
// We can create an instance as well
|
||||
var newCard = new CreditCard({number: '0123'});
|
||||
newCard.name = 'Mike Smith';
|
||||
|
||||
var savePromise = newCard.$save();
|
||||
// POST: /users/123/cards {number: '0123', name: 'Mike Smith'}
|
||||
// server returns: {id: 789, number: '0123', name: 'Mike Smith'}
|
||||
|
||||
savePromise.then(function() {
|
||||
// Once the promise is resolved, the created instance
|
||||
// is populated with the data returned by the server
|
||||
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.
|
||||
* The object returned from a call to `$resource` is a resource "class" which has one "static"
|
||||
* method for each action in the definition.
|
||||
*
|
||||
* Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and
|
||||
* `headers`.
|
||||
* Calling these methods invokes `$http` on the `url` template with the given HTTP `method`,
|
||||
* `params` and `headers`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ### User resource
|
||||
* ### Accessing the response
|
||||
*
|
||||
* 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.
|
||||
|
||||
*
|
||||
```js
|
||||
var User = $resource('/user/:userId', {userId:'@id'});
|
||||
User.get({userId:123}, function(user) {
|
||||
var User = $resource('/users/:userId', {userId: '@id'});
|
||||
User.get({userId: 123}).$promise.then(function(user) {
|
||||
user.abc = true;
|
||||
user.$save();
|
||||
});
|
||||
```
|
||||
*
|
||||
* It's worth noting that the success callback for `get`, `query` and other methods gets passed
|
||||
* in the response that came from the server as well as $http header getter function, so one
|
||||
* could rewrite the above example and get access to http headers as:
|
||||
* It's worth noting that the success callback for `get`, `query` and other methods gets called with
|
||||
* the resource instance (populated with the data that came from the server) as well as an `$http`
|
||||
* header getter function, the HTTP status code and the response status text. So one could rewrite
|
||||
* the above example and get access to HTTP headers as follows:
|
||||
*
|
||||
```js
|
||||
var User = $resource('/user/:userId', {userId:'@id'});
|
||||
User.get({userId:123}, function(user, getResponseHeaders){
|
||||
var User = $resource('/users/:userId', {userId: '@id'});
|
||||
User.get({userId: 123}, function(user, getResponseHeaders) {
|
||||
user.abc = true;
|
||||
user.$save(function(user, putResponseHeaders) {
|
||||
//user => saved user object
|
||||
//putResponseHeaders => $http header getter
|
||||
// `user` => saved `User` object
|
||||
// `putResponseHeaders` => `$http` header getter
|
||||
});
|
||||
});
|
||||
```
|
||||
*
|
||||
* You can also access the raw `$http` promise via the `$promise` property on the object returned
|
||||
*
|
||||
```
|
||||
var User = $resource('/user/:userId', {userId:'@id'});
|
||||
User.get({userId:123})
|
||||
.$promise.then(function(user) {
|
||||
$scope.user = user;
|
||||
});
|
||||
```
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ### Creating a custom 'PUT' request
|
||||
* ### Creating custom actions
|
||||
*
|
||||
* In this example we create a custom method on our resource to make a PUT request
|
||||
* ```js
|
||||
* var app = angular.module('app', ['ngResource', 'ngRoute']);
|
||||
* In this example we create a custom method on our resource to make a PUT request:
|
||||
*
|
||||
* // Some APIs expect a PUT request in the format URL/object/ID
|
||||
* // Here we are creating an 'update' method
|
||||
* app.factory('Notes', ['$resource', function($resource) {
|
||||
* return $resource('/notes/:id', null,
|
||||
* {
|
||||
* 'update': { method:'PUT' }
|
||||
* });
|
||||
* }]);
|
||||
*
|
||||
* // In our controller we get the ID from the URL using ngRoute and $routeParams
|
||||
* // We pass in $routeParams and our Notes factory along with $scope
|
||||
* app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes',
|
||||
function($scope, $routeParams, Notes) {
|
||||
* // First get a note object from the factory
|
||||
* var note = Notes.get({ id:$routeParams.id });
|
||||
* $id = note.id;
|
||||
*
|
||||
* // Now call update passing in the ID first then the object you are updating
|
||||
* Notes.update({ id:$id }, note);
|
||||
*
|
||||
* // This will PUT /notes/ID with the note object in the request payload
|
||||
* }]);
|
||||
* ```
|
||||
```js
|
||||
var app = angular.module('app', ['ngResource']);
|
||||
|
||||
// Some APIs expect a PUT request in the format URL/object/ID
|
||||
// Here we are creating an 'update' method
|
||||
app.factory('Notes', ['$resource', function($resource) {
|
||||
return $resource('/notes/:id', {id: '@id'}, {
|
||||
update: {method: 'PUT'}
|
||||
});
|
||||
}]);
|
||||
|
||||
// In our controller we get the ID from the URL using `$location`
|
||||
app.controller('NotesCtrl', ['$location', 'Notes', function($location, Notes) {
|
||||
// First, retrieve the corresponding `Note` object from the server
|
||||
// (Assuming a URL of the form `.../notes?id=XYZ`)
|
||||
var noteId = $location.search().id;
|
||||
var note = Notes.get({id: noteId});
|
||||
|
||||
note.$promise.then(function() {
|
||||
note.content = 'Hello, world!';
|
||||
|
||||
// Now call `update` to save the changes on the server
|
||||
Notes.update(note);
|
||||
// This will PUT /notes/ID with the note object as the request payload
|
||||
|
||||
// Since `update` is a non-GET method, it will also be available on the instance
|
||||
// (prefixed with `$`), so we could replace the `Note.update()` call with:
|
||||
//note.$update();
|
||||
});
|
||||
}]);
|
||||
```
|
||||
*
|
||||
* @example
|
||||
*
|
||||
@@ -416,7 +432,7 @@ function shallowClearAndCopy(src, dst) {
|
||||
*
|
||||
```js
|
||||
// ...defining the `Hotel` resource...
|
||||
var Hotel = $resource('/api/hotel/:id', {id: '@id'}, {
|
||||
var Hotel = $resource('/api/hotels/:id', {id: '@id'}, {
|
||||
// Let's make the `query()` method cancellable
|
||||
query: {method: 'get', isArray: true, cancellable: true}
|
||||
});
|
||||
@@ -426,14 +442,54 @@ function shallowClearAndCopy(src, dst) {
|
||||
this.onDestinationChanged = function onDestinationChanged(destination) {
|
||||
// We don't care about any pending request for hotels
|
||||
// in a different destination any more
|
||||
this.availableHotels.$cancelRequest();
|
||||
if (this.availableHotels) {
|
||||
this.availableHotels.$cancelRequest();
|
||||
}
|
||||
|
||||
// Let's query for hotels in '<destination>'
|
||||
// (calls: /api/hotel?location=<destination>)
|
||||
// Let's query for hotels in `destination`
|
||||
// (calls: /api/hotels?location=<destination>)
|
||||
this.availableHotels = Hotel.query({location: destination});
|
||||
};
|
||||
```
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ### Using interceptors
|
||||
*
|
||||
* You can use interceptors to transform the request or response, perform additional operations, and
|
||||
* modify the returned instance/collection. The following example, uses `request` and `response`
|
||||
* interceptors to augment the returned instance with additional info:
|
||||
*
|
||||
```js
|
||||
var Thing = $resource('/api/things/:id', {id: '@id'}, {
|
||||
save: {
|
||||
method: 'POST',
|
||||
interceptor: {
|
||||
request: function(config) {
|
||||
// Before the request is sent out, store a timestamp on the request config
|
||||
config.requestTimestamp = Date.now();
|
||||
return config;
|
||||
},
|
||||
response: function(response) {
|
||||
// Get the instance from the response object
|
||||
var instance = response.resource;
|
||||
|
||||
// Augment the instance with a custom `saveLatency` property, computed as the time
|
||||
// between sending the request and receiving the response.
|
||||
instance.saveLatency = Date.now() - response.config.requestTimestamp;
|
||||
|
||||
// Return the instance
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Thing.save({foo: 'bar'}).$promise.then(function(thing) {
|
||||
console.log('That thing was saved in ' + thing.saveLatency + 'ms.');
|
||||
});
|
||||
```
|
||||
*
|
||||
*/
|
||||
angular.module('ngResource', ['ng']).
|
||||
info({ angularVersion: '"NG_VERSION_FULL"' }).
|
||||
@@ -666,34 +722,34 @@ angular.module('ngResource', ['ng']).
|
||||
}
|
||||
|
||||
Resource[name] = function(a1, a2, a3, a4) {
|
||||
var params = {}, data, success, error;
|
||||
var params = {}, data, onSuccess, onError;
|
||||
|
||||
switch (arguments.length) {
|
||||
case 4:
|
||||
error = a4;
|
||||
success = a3;
|
||||
onError = a4;
|
||||
onSuccess = a3;
|
||||
// falls through
|
||||
case 3:
|
||||
case 2:
|
||||
if (isFunction(a2)) {
|
||||
if (isFunction(a1)) {
|
||||
success = a1;
|
||||
error = a2;
|
||||
onSuccess = a1;
|
||||
onError = a2;
|
||||
break;
|
||||
}
|
||||
|
||||
success = a2;
|
||||
error = a3;
|
||||
onSuccess = a2;
|
||||
onError = a3;
|
||||
// falls through
|
||||
} else {
|
||||
params = a1;
|
||||
data = a2;
|
||||
success = a3;
|
||||
onSuccess = a3;
|
||||
break;
|
||||
}
|
||||
// falls through
|
||||
case 1:
|
||||
if (isFunction(a1)) success = a1;
|
||||
if (isFunction(a1)) onSuccess = a1;
|
||||
else if (hasBody) data = a1;
|
||||
else params = a1;
|
||||
break;
|
||||
@@ -707,14 +763,20 @@ angular.module('ngResource', ['ng']).
|
||||
var isInstanceCall = this instanceof Resource;
|
||||
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
|
||||
var httpConfig = {};
|
||||
var requestInterceptor = action.interceptor && action.interceptor.request || undefined;
|
||||
var requestErrorInterceptor = action.interceptor && action.interceptor.requestError ||
|
||||
undefined;
|
||||
var responseInterceptor = action.interceptor && action.interceptor.response ||
|
||||
defaultResponseInterceptor;
|
||||
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
|
||||
undefined;
|
||||
var hasError = !!error;
|
||||
var hasResponseErrorInterceptor = !!responseErrorInterceptor;
|
||||
$q.reject;
|
||||
var successCallback = onSuccess ? function(val) {
|
||||
onSuccess(val, response.headers, response.status, response.statusText);
|
||||
} : undefined;
|
||||
var errorCallback = onError || undefined;
|
||||
var timeoutDeferred;
|
||||
var numericTimeoutPromise;
|
||||
var response;
|
||||
|
||||
forEach(action, function(value, key) {
|
||||
switch (key) {
|
||||
@@ -743,8 +805,15 @@ angular.module('ngResource', ['ng']).
|
||||
extend({}, extractParams(data, action.params || {}), params),
|
||||
action.url);
|
||||
|
||||
var promise = $http(httpConfig).then(function(response) {
|
||||
var data = response.data;
|
||||
// Start the promise chain
|
||||
var promise = $q.
|
||||
resolve(httpConfig).
|
||||
then(requestInterceptor).
|
||||
catch(requestErrorInterceptor).
|
||||
then($http);
|
||||
|
||||
promise = promise.then(function(resp) {
|
||||
var data = resp.data;
|
||||
|
||||
if (data) {
|
||||
// Need to convert action.isArray to boolean in case it is undefined
|
||||
@@ -772,12 +841,14 @@ angular.module('ngResource', ['ng']).
|
||||
value.$promise = promise; // Restore the promise
|
||||
}
|
||||
}
|
||||
response.resource = value;
|
||||
|
||||
return response;
|
||||
}, function(response) {
|
||||
response.resource = value;
|
||||
return $q.reject(response);
|
||||
resp.resource = value;
|
||||
response = resp;
|
||||
return responseInterceptor(resp);
|
||||
}, function(rejectionOrResponse) {
|
||||
rejectionOrResponse.resource = value;
|
||||
response = rejectionOrResponse;
|
||||
return responseErrorInterceptor(rejectionOrResponse);
|
||||
});
|
||||
|
||||
promise = promise['finally'](function() {
|
||||
@@ -789,25 +860,8 @@ angular.module('ngResource', ['ng']).
|
||||
}
|
||||
});
|
||||
|
||||
promise = promise.then(
|
||||
function(response) {
|
||||
var value = responseInterceptor(response);
|
||||
(success || noop)(value, response.headers, response.status, response.statusText);
|
||||
return value;
|
||||
},
|
||||
(hasError || hasResponseErrorInterceptor) ?
|
||||
function(response) {
|
||||
if (hasError && !hasResponseErrorInterceptor) {
|
||||
// Avoid `Possibly Unhandled Rejection` error,
|
||||
// but still fulfill the returned promise with a rejection
|
||||
promise.catch(noop);
|
||||
}
|
||||
if (hasError) error(response);
|
||||
return hasResponseErrorInterceptor ?
|
||||
responseErrorInterceptor(response) :
|
||||
$q.reject(response);
|
||||
} :
|
||||
undefined);
|
||||
// Run the `success`/`error` callbacks, but do not let them affect the returned promise.
|
||||
promise.then(successCallback, errorCallback);
|
||||
|
||||
if (!isInstanceCall) {
|
||||
// we are creating instance / collection
|
||||
|
||||
@@ -42,12 +42,11 @@ var htmlSanitizeWriter;
|
||||
* Sanitizes an html string by stripping all potentially dangerous tokens.
|
||||
*
|
||||
* 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
|
||||
* then serialized back to a properly escaped HTML string. This means that no unsafe input can make
|
||||
* it into the returned string.
|
||||
*
|
||||
* The whitelist for URL sanitization of attribute values is configured using the functions
|
||||
* `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
|
||||
* `$compileProvider`}.
|
||||
* `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link $compileProvider}.
|
||||
*
|
||||
* The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
|
||||
*
|
||||
@@ -311,7 +310,7 @@ function $SanitizeProvider() {
|
||||
forEach = angular.forEach;
|
||||
isArray = angular.isArray;
|
||||
isDefined = angular.isDefined;
|
||||
lowercase = angular.lowercase;
|
||||
lowercase = angular.$$lowercase;
|
||||
noop = angular.noop;
|
||||
|
||||
htmlParser = htmlParserImpl;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"globals": {
|
||||
"includes": false,
|
||||
"asyncForEach": false,
|
||||
"msie": false,
|
||||
"browserTrigger": false,
|
||||
"console": false,
|
||||
"alert": false,
|
||||
"_jQuery": false,
|
||||
"angularInit": false,
|
||||
"formatException": false,
|
||||
"$runner": false,
|
||||
"callerFile": false
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 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(
|
||||
'<h2>Current URL: <a href="about:blank">None</a></h2>' +
|
||||
'<div id="test-frames"></div>'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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_().prop('contentWindow');
|
||||
if (!contentWindow) {
|
||||
throw new Error('Frame window is not accessible.');
|
||||
}
|
||||
return contentWindow;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = self.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);
|
||||
self.executeAction(loadFn);
|
||||
} else {
|
||||
frame.remove();
|
||||
self.context.find('#test-frames').append('<iframe>');
|
||||
frame = self.getFrame_();
|
||||
|
||||
frame.on('load', function() {
|
||||
frame.off();
|
||||
try {
|
||||
var $window = self.getWindow_();
|
||||
|
||||
if (!$window.angular) {
|
||||
self.executeAction(loadFn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$window.angular.resumeBootstrap) {
|
||||
$window.angular.resumeDeferredBootstrap = resumeDeferredBootstrap;
|
||||
} else {
|
||||
resumeDeferredBootstrap();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
errorFn(e);
|
||||
}
|
||||
|
||||
function resumeDeferredBootstrap() {
|
||||
// Disable animations
|
||||
var $injector = $window.angular.resumeBootstrap([['$provide', function($provide) {
|
||||
return ['$animate', function($animate) {
|
||||
$animate.enabled(false);
|
||||
}];
|
||||
}]]);
|
||||
self.rootElement = $injector.get('$rootElement')[0];
|
||||
self.executeAction(loadFn);
|
||||
}
|
||||
}).attr('src', url);
|
||||
|
||||
// for IE compatibility set the name *after* setting the frame url
|
||||
frame[0].contentWindow.name = 'NG_DEFER_BOOTSTRAP!';
|
||||
}
|
||||
self.context.find('> h2 a').attr('href', url).text(url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a function in the context of the tested application. Will wait
|
||||
* for all pending AngularJS xhr requests before executing.
|
||||
*
|
||||
* @param {function()} action The callback to execute. function($window, $document)
|
||||
* $document is a jQuery wrapped document.
|
||||
*/
|
||||
angular.scenario.Application.prototype.executeAction = function(action) {
|
||||
var self = this;
|
||||
var $window = this.getWindow_();
|
||||
if (!$window.document) {
|
||||
throw new Error('Sandbox Error: Application document not accessible.');
|
||||
}
|
||||
if (!$window.angular) {
|
||||
return action.call(this, $window, _jQuery($window.document));
|
||||
}
|
||||
|
||||
if (this.rootElement) {
|
||||
executeWithElement(this.rootElement);
|
||||
} else {
|
||||
angularInit($window.document, angular.bind(this, executeWithElement));
|
||||
}
|
||||
|
||||
function executeWithElement(element) {
|
||||
var $injector = $window.angular.element(element).injector();
|
||||
var $element = _jQuery(element);
|
||||
|
||||
$element.injector = function() {
|
||||
return $injector;
|
||||
};
|
||||
|
||||
$injector.invoke(function($browser) {
|
||||
$browser.notifyWhenNoOutstandingRequests(function() {
|
||||
action.call(self, $window, $element);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,155 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* The representation of define blocks. Don't used directly, instead use
|
||||
* define() in your tests.
|
||||
*
|
||||
* @param {string} descName Name of the block
|
||||
* @param {Object} parent describe or undefined if the root.
|
||||
*/
|
||||
angular.scenario.Describe = function(descName, parent) {
|
||||
this.only = parent && parent.only;
|
||||
this.beforeEachFns = [];
|
||||
this.afterEachFns = [];
|
||||
this.its = [];
|
||||
this.children = [];
|
||||
this.name = descName;
|
||||
this.parent = parent;
|
||||
this.id = angular.scenario.Describe.id++;
|
||||
|
||||
/**
|
||||
* Calls all before functions.
|
||||
*/
|
||||
var beforeEachFns = this.beforeEachFns;
|
||||
this.setupBefore = function() {
|
||||
if (parent) parent.setupBefore.call(this);
|
||||
angular.forEach(beforeEachFns, /** @this */ function(fn) { fn.call(this); }, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls all after functions.
|
||||
*/
|
||||
var afterEachFns = this.afterEachFns;
|
||||
this.setupAfter = function() {
|
||||
angular.forEach(afterEachFns, /** @this */ function(fn) { fn.call(this); }, this);
|
||||
if (parent) parent.setupAfter.call(this);
|
||||
};
|
||||
};
|
||||
|
||||
// Shared Unique ID generator for every describe block
|
||||
angular.scenario.Describe.id = 0;
|
||||
|
||||
// Shared Unique ID generator for every it (spec)
|
||||
angular.scenario.Describe.specId = 0;
|
||||
|
||||
/**
|
||||
* Defines a block to execute before each it or nested describe.
|
||||
*
|
||||
* @param {function()} body Body of the block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.beforeEach = function(body) {
|
||||
this.beforeEachFns.push(body);
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a block to execute after each it or nested describe.
|
||||
*
|
||||
* @param {function()} body Body of the block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.afterEach = function(body) {
|
||||
this.afterEachFns.push(body);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new describe block that's a child of this one.
|
||||
*
|
||||
* @param {string} name Name of the block. Appended to the parent block's name.
|
||||
* @param {function()} body Body of the block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.describe = function(name, body) {
|
||||
var child = new angular.scenario.Describe(name, this);
|
||||
this.children.push(child);
|
||||
body.call(child);
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as describe() but makes ddescribe blocks the only to run.
|
||||
*
|
||||
* @param {string} name Name of the test.
|
||||
* @param {function()} body Body of the block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.ddescribe = function(name, body) {
|
||||
var child = new angular.scenario.Describe(name, this);
|
||||
child.only = true;
|
||||
this.children.push(child);
|
||||
body.call(child);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use to disable a describe block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.xdescribe = angular.noop;
|
||||
|
||||
/**
|
||||
* Defines a test.
|
||||
*
|
||||
* @param {string} name Name of the test.
|
||||
* @param {function()} body Body of the block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.it = function(name, body) {
|
||||
this.its.push({
|
||||
id: angular.scenario.Describe.specId++,
|
||||
definition: this,
|
||||
only: this.only,
|
||||
name: name,
|
||||
before: this.setupBefore,
|
||||
body: body,
|
||||
after: this.setupAfter
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as it() but makes iit tests the only test to run.
|
||||
*
|
||||
* @param {string} name Name of the test.
|
||||
* @param {function()} body Body of the block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.iit = function(name, body) {
|
||||
this.it.apply(this, arguments);
|
||||
this.its[this.its.length - 1].only = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Use to disable a test block.
|
||||
*/
|
||||
angular.scenario.Describe.prototype.xit = angular.noop;
|
||||
|
||||
/**
|
||||
* Gets an array of functions representing all the tests (recursively).
|
||||
* that can be executed with SpecRunner's.
|
||||
*
|
||||
* @return {Array<Object>} Array of it blocks {
|
||||
* definition : Object // parent Describe
|
||||
* only: boolean
|
||||
* name: string
|
||||
* before: Function
|
||||
* body: Function
|
||||
* after: Function
|
||||
* }
|
||||
*/
|
||||
angular.scenario.Describe.prototype.getSpecs = function() {
|
||||
var specs = arguments[0] || [];
|
||||
angular.forEach(this.children, function(child) {
|
||||
child.getSpecs(specs);
|
||||
});
|
||||
angular.forEach(this.its, function(it) {
|
||||
specs.push(it);
|
||||
});
|
||||
var only = [];
|
||||
angular.forEach(specs, function(it) {
|
||||
if (it.only) {
|
||||
only.push(it);
|
||||
}
|
||||
});
|
||||
return (only.length && only) || specs;
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* A future action in a spec.
|
||||
*
|
||||
* @param {string} name name of the future action
|
||||
* @param {function()} behavior future callback(error, result)
|
||||
* @param {function()} line Optional. function that returns the file/line number.
|
||||
*/
|
||||
angular.scenario.Future = function(name, behavior, line) {
|
||||
this.name = name;
|
||||
this.behavior = behavior;
|
||||
this.fulfilled = false;
|
||||
this.value = undefined;
|
||||
this.parser = angular.identity;
|
||||
this.line = line || function() { return ''; };
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes the behavior of the closure.
|
||||
*
|
||||
* @param {function()} doneFn Callback function(error, result)
|
||||
*/
|
||||
angular.scenario.Future.prototype.execute = function(doneFn) {
|
||||
var self = this;
|
||||
this.behavior(function(error, result) {
|
||||
self.fulfilled = true;
|
||||
if (result) {
|
||||
try {
|
||||
result = self.parser(result);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
self.value = error || result;
|
||||
doneFn(error, result);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the future to convert its final with a function fn(value)
|
||||
*
|
||||
* @param {function()} fn function(value) that returns the parsed value
|
||||
*/
|
||||
angular.scenario.Future.prototype.parsedWith = function(fn) {
|
||||
this.parser = fn;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the future to parse its final value from JSON
|
||||
* into objects.
|
||||
*/
|
||||
angular.scenario.Future.prototype.fromJson = function() {
|
||||
return this.parsedWith(angular.fromJson);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the future to convert its final value from objects
|
||||
* into JSON.
|
||||
*/
|
||||
angular.scenario.Future.prototype.toJson = function() {
|
||||
return this.parsedWith(angular.toJson);
|
||||
};
|
||||
@@ -1,252 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Maintains an object tree from the runner events.
|
||||
*
|
||||
* @param {Object} runner The scenario Runner instance to connect to.
|
||||
*
|
||||
* TODO(esprehn): Every output type creates one of these, but we probably
|
||||
* want one global shared instance. Need to handle events better too
|
||||
* so the HTML output doesn't need to do spec model.getSpec(spec.id)
|
||||
* silliness.
|
||||
*
|
||||
* TODO(vojta) refactor on, emit methods (from all objects) - use inheritance
|
||||
*/
|
||||
angular.scenario.ObjectModel = function(runner) {
|
||||
var self = this;
|
||||
|
||||
this.specMap = {};
|
||||
this.listeners = [];
|
||||
this.value = {
|
||||
name: '',
|
||||
children: {}
|
||||
};
|
||||
|
||||
runner.on('SpecBegin', function(spec) {
|
||||
var block = self.value,
|
||||
definitions = [];
|
||||
|
||||
angular.forEach(self.getDefinitionPath(spec), function(def) {
|
||||
if (!block.children[def.name]) {
|
||||
block.children[def.name] = {
|
||||
id: def.id,
|
||||
name: def.name,
|
||||
children: {},
|
||||
specs: {}
|
||||
};
|
||||
}
|
||||
block = block.children[def.name];
|
||||
definitions.push(def.name);
|
||||
});
|
||||
|
||||
var it = self.specMap[spec.id] =
|
||||
block.specs[spec.name] =
|
||||
new angular.scenario.ObjectModel.Spec(spec.id, spec.name, definitions);
|
||||
|
||||
// forward the event
|
||||
self.emit('SpecBegin', it);
|
||||
});
|
||||
|
||||
runner.on('SpecError', function(spec, error) {
|
||||
var it = self.getSpec(spec.id);
|
||||
it.status = 'error';
|
||||
it.error = error;
|
||||
|
||||
// forward the event
|
||||
self.emit('SpecError', it, error);
|
||||
});
|
||||
|
||||
runner.on('SpecEnd', function(spec) {
|
||||
var it = self.getSpec(spec.id);
|
||||
complete(it);
|
||||
|
||||
// forward the event
|
||||
self.emit('SpecEnd', it);
|
||||
});
|
||||
|
||||
runner.on('StepBegin', function(spec, step) {
|
||||
var it = self.getSpec(spec.id);
|
||||
step = new angular.scenario.ObjectModel.Step(step.name);
|
||||
it.steps.push(step);
|
||||
|
||||
// forward the event
|
||||
self.emit('StepBegin', it, step);
|
||||
});
|
||||
|
||||
runner.on('StepEnd', function(spec) {
|
||||
var it = self.getSpec(spec.id);
|
||||
var step = it.getLastStep();
|
||||
if (step.name !== step.name) {
|
||||
throw new Error('Events fired in the wrong order. Step names don\'t match.');
|
||||
}
|
||||
complete(step);
|
||||
|
||||
// forward the event
|
||||
self.emit('StepEnd', it, step);
|
||||
});
|
||||
|
||||
runner.on('StepFailure', function(spec, step, error) {
|
||||
var it = self.getSpec(spec.id),
|
||||
modelStep = it.getLastStep();
|
||||
|
||||
modelStep.setErrorStatus('failure', error, step.line());
|
||||
it.setStatusFromStep(modelStep);
|
||||
|
||||
// forward the event
|
||||
self.emit('StepFailure', it, modelStep, error);
|
||||
});
|
||||
|
||||
runner.on('StepError', function(spec, step, error) {
|
||||
var it = self.getSpec(spec.id),
|
||||
modelStep = it.getLastStep();
|
||||
|
||||
modelStep.setErrorStatus('error', error, step.line());
|
||||
it.setStatusFromStep(modelStep);
|
||||
|
||||
// forward the event
|
||||
self.emit('StepError', it, modelStep, error);
|
||||
});
|
||||
|
||||
runner.on('RunnerBegin', function() {
|
||||
self.emit('RunnerBegin');
|
||||
});
|
||||
runner.on('RunnerEnd', function() {
|
||||
self.emit('RunnerEnd');
|
||||
});
|
||||
|
||||
function complete(item) {
|
||||
item.endTime = Date.now();
|
||||
item.duration = item.endTime - item.startTime;
|
||||
item.status = item.status || 'success';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a listener for an event.
|
||||
*
|
||||
* @param {string} eventName Name of the event to add a handler for
|
||||
* @param {function()} listener Function that will be called when event is fired
|
||||
*/
|
||||
angular.scenario.ObjectModel.prototype.on = function(eventName, listener) {
|
||||
eventName = eventName.toLowerCase();
|
||||
this.listeners[eventName] = this.listeners[eventName] || [];
|
||||
this.listeners[eventName].push(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits an event which notifies listeners and passes extra
|
||||
* arguments.
|
||||
*
|
||||
* @param {string} eventName Name of the event to fire.
|
||||
*/
|
||||
angular.scenario.ObjectModel.prototype.emit = function(eventName) {
|
||||
var self = this,
|
||||
args = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
eventName = eventName.toLowerCase();
|
||||
|
||||
if (this.listeners[eventName]) {
|
||||
angular.forEach(this.listeners[eventName], function(listener) {
|
||||
listener.apply(self, args);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the path of definition describe blocks that wrap around
|
||||
* this spec.
|
||||
*
|
||||
* @param spec Spec to compute the path for.
|
||||
* @return {Array<Describe>} The describe block path
|
||||
*/
|
||||
angular.scenario.ObjectModel.prototype.getDefinitionPath = function(spec) {
|
||||
var path = [];
|
||||
var currentDefinition = spec.definition;
|
||||
while (currentDefinition && currentDefinition.name) {
|
||||
path.unshift(currentDefinition);
|
||||
currentDefinition = currentDefinition.parent;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a spec by id.
|
||||
*
|
||||
* @param {string} id The id of the spec to get the object for.
|
||||
* @return {Object} the Spec instance
|
||||
*/
|
||||
angular.scenario.ObjectModel.prototype.getSpec = function(id) {
|
||||
return this.specMap[id];
|
||||
};
|
||||
|
||||
/**
|
||||
* A single it block.
|
||||
*
|
||||
* @param {string} id Id of the spec
|
||||
* @param {string} name Name of the spec
|
||||
* @param {Array<string>=} definitionNames List of all describe block names that wrap this spec
|
||||
*/
|
||||
angular.scenario.ObjectModel.Spec = function(id, name, definitionNames) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.startTime = Date.now();
|
||||
this.steps = [];
|
||||
this.fullDefinitionName = (definitionNames || []).join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a new step to the Spec.
|
||||
*
|
||||
* @param {string} name Name of the step (really name of the future)
|
||||
* @return {Object} the added step
|
||||
*/
|
||||
angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) {
|
||||
var step = new angular.scenario.ObjectModel.Step(name);
|
||||
this.steps.push(step);
|
||||
return step;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the most recent step.
|
||||
*
|
||||
* @return {Object} the step
|
||||
*/
|
||||
angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() {
|
||||
return this.steps[this.steps.length - 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Set status of the Spec from given Step
|
||||
*
|
||||
* @param {angular.scenario.ObjectModel.Step} step
|
||||
*/
|
||||
angular.scenario.ObjectModel.Spec.prototype.setStatusFromStep = function(step) {
|
||||
if (!this.status || step.status === 'error') {
|
||||
this.status = step.status;
|
||||
this.error = step.error;
|
||||
this.line = step.line;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A single step inside a Spec.
|
||||
*
|
||||
* @param {string} name Name of the step
|
||||
*/
|
||||
angular.scenario.ObjectModel.Step = function(name) {
|
||||
this.name = name;
|
||||
this.startTime = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method for setting all error status related properties
|
||||
*
|
||||
* @param {string} status
|
||||
* @param {string} error
|
||||
* @param {string} line
|
||||
*/
|
||||
angular.scenario.ObjectModel.Step.prototype.setErrorStatus = function(status, error, line) {
|
||||
this.status = status;
|
||||
this.error = error;
|
||||
this.line = line;
|
||||
};
|
||||
@@ -1,229 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Runner for scenarios
|
||||
*
|
||||
* Has to be initialized before any test is loaded,
|
||||
* because it publishes the API into window (global space).
|
||||
*/
|
||||
angular.scenario.Runner = function($window) {
|
||||
this.listeners = [];
|
||||
this.$window = $window;
|
||||
this.rootDescribe = new angular.scenario.Describe();
|
||||
this.currentDescribe = this.rootDescribe;
|
||||
this.api = {
|
||||
it: this.it,
|
||||
iit: this.iit,
|
||||
xit: angular.noop,
|
||||
describe: this.describe,
|
||||
ddescribe: this.ddescribe,
|
||||
xdescribe: angular.noop,
|
||||
beforeEach: this.beforeEach,
|
||||
afterEach: this.afterEach
|
||||
};
|
||||
angular.forEach(this.api, angular.bind(this, /** @this */ function(fn, key) {
|
||||
this.$window[key] = angular.bind(this, fn);
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits an event which notifies listeners and passes extra
|
||||
* arguments.
|
||||
*
|
||||
* @param {string} eventName Name of the event to fire.
|
||||
*/
|
||||
angular.scenario.Runner.prototype.emit = function(eventName) {
|
||||
var self = this;
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
eventName = eventName.toLowerCase();
|
||||
if (!this.listeners[eventName]) {
|
||||
return;
|
||||
}
|
||||
angular.forEach(this.listeners[eventName], function(listener) {
|
||||
listener.apply(self, args);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a listener for an event.
|
||||
*
|
||||
* @param {string} eventName The name of the event to add a handler for
|
||||
* @param {string} listener The fn(...) that takes the extra arguments from emit()
|
||||
*/
|
||||
angular.scenario.Runner.prototype.on = function(eventName, listener) {
|
||||
eventName = eventName.toLowerCase();
|
||||
this.listeners[eventName] = this.listeners[eventName] || [];
|
||||
this.listeners[eventName].push(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a describe block of a spec.
|
||||
*
|
||||
* @see Describe.js
|
||||
*
|
||||
* @param {string} name Name of the block
|
||||
* @param {function()} body Body of the block
|
||||
*/
|
||||
angular.scenario.Runner.prototype.describe = function(name, body) {
|
||||
var self = this;
|
||||
this.currentDescribe.describe(name, /** @this */ function() {
|
||||
var parentDescribe = self.currentDescribe;
|
||||
self.currentDescribe = this;
|
||||
try {
|
||||
body.call(this);
|
||||
} finally {
|
||||
self.currentDescribe = parentDescribe;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as describe, but makes ddescribe the only blocks to run.
|
||||
*
|
||||
* @see Describe.js
|
||||
*
|
||||
* @param {string} name Name of the block
|
||||
* @param {function()} body Body of the block
|
||||
*/
|
||||
angular.scenario.Runner.prototype.ddescribe = function(name, body) {
|
||||
var self = this;
|
||||
this.currentDescribe.ddescribe(name, /** @this */ function() {
|
||||
var parentDescribe = self.currentDescribe;
|
||||
self.currentDescribe = this;
|
||||
try {
|
||||
body.call(this);
|
||||
} finally {
|
||||
self.currentDescribe = parentDescribe;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a test in a describe block of a spec.
|
||||
*
|
||||
* @see Describe.js
|
||||
*
|
||||
* @param {string} name Name of the block
|
||||
* @param {function()} body Body of the block
|
||||
*/
|
||||
angular.scenario.Runner.prototype.it = function(name, body) {
|
||||
this.currentDescribe.it(name, body);
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as it, but makes iit tests the only tests to run.
|
||||
*
|
||||
* @see Describe.js
|
||||
*
|
||||
* @param {string} name Name of the block
|
||||
* @param {function()} body Body of the block
|
||||
*/
|
||||
angular.scenario.Runner.prototype.iit = function(name, body) {
|
||||
this.currentDescribe.iit(name, body);
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a function to be called before each it block in the describe
|
||||
* (and before all nested describes).
|
||||
*
|
||||
* @see Describe.js
|
||||
*
|
||||
* @param {function()} Callback to execute
|
||||
*/
|
||||
angular.scenario.Runner.prototype.beforeEach = function(body) {
|
||||
this.currentDescribe.beforeEach(body);
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a function to be called after each it block in the describe
|
||||
* (and before all nested describes).
|
||||
*
|
||||
* @see Describe.js
|
||||
*
|
||||
* @param {function()} Callback to execute
|
||||
*/
|
||||
angular.scenario.Runner.prototype.afterEach = function(body) {
|
||||
this.currentDescribe.afterEach(body);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new spec runner.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} scope parent scope
|
||||
*/
|
||||
angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) {
|
||||
var child = scope.$new();
|
||||
var Cls = angular.scenario.SpecRunner;
|
||||
|
||||
// Export all the methods to child scope manually as now we don't mess controllers with scopes
|
||||
// TODO(vojta): refactor scenario runner so that these objects are not tightly coupled as current
|
||||
for (var name in Cls.prototype) {
|
||||
child[name] = angular.bind(child, Cls.prototype[name]);
|
||||
}
|
||||
|
||||
Cls.call(child);
|
||||
return child;
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs all the loaded tests with the specified runner class on the
|
||||
* provided application.
|
||||
*
|
||||
* @param {angular.scenario.Application} application App to remote control.
|
||||
*/
|
||||
angular.scenario.Runner.prototype.run = function(application) {
|
||||
var self = this;
|
||||
var $root = angular.injector(['ng']).get('$rootScope');
|
||||
angular.extend($root, this);
|
||||
angular.forEach(angular.scenario.Runner.prototype, function(fn, name) {
|
||||
$root[name] = angular.bind(self, fn);
|
||||
});
|
||||
$root.application = application;
|
||||
$root.emit('RunnerBegin');
|
||||
asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) {
|
||||
var dslCache = {};
|
||||
var runner = self.createSpecRunner_($root);
|
||||
angular.forEach(angular.scenario.dsl, function(fn, key) {
|
||||
dslCache[key] = fn.call($root);
|
||||
});
|
||||
angular.forEach(angular.scenario.dsl, function(fn, key) {
|
||||
self.$window[key] = function() {
|
||||
var line = callerFile(3);
|
||||
var scope = runner.$new();
|
||||
|
||||
// Make the dsl accessible on the current chain
|
||||
scope.dsl = {};
|
||||
angular.forEach(dslCache, function(fn, key) {
|
||||
scope.dsl[key] = function() {
|
||||
return dslCache[key].apply(scope, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
// Make these methods work on the current chain
|
||||
scope.addFuture = function() {
|
||||
Array.prototype.push.call(arguments, line);
|
||||
return angular.scenario.SpecRunner.
|
||||
prototype.addFuture.apply(scope, arguments);
|
||||
};
|
||||
scope.addFutureAction = function() {
|
||||
Array.prototype.push.call(arguments, line);
|
||||
return angular.scenario.SpecRunner.
|
||||
prototype.addFutureAction.apply(scope, arguments);
|
||||
};
|
||||
|
||||
return scope.dsl[key].apply(scope, arguments);
|
||||
};
|
||||
});
|
||||
runner.run(spec, /** @this */ function() {
|
||||
runner.$destroy();
|
||||
specDone.apply(this, arguments);
|
||||
});
|
||||
},
|
||||
function(error) {
|
||||
if (error) {
|
||||
self.emit('RunnerError', error);
|
||||
}
|
||||
self.emit('RunnerEnd');
|
||||
});
|
||||
};
|
||||
@@ -1,329 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
/**
|
||||
* Setup file for the Scenario.
|
||||
* Must be first in the compilation/bootstrap list.
|
||||
*/
|
||||
|
||||
// Public namespace
|
||||
angular.scenario = angular.scenario || {};
|
||||
|
||||
/**
|
||||
* Expose jQuery (e.g. for custom dsl extensions).
|
||||
*/
|
||||
angular.scenario.jQuery = _jQuery;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
// The dsl binds `this` for us when calling chained functions
|
||||
/** @this */
|
||||
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 /** @this */ 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 description = this.future.name +
|
||||
(this.inverse ? ' not ' : ' ') + name +
|
||||
' ' + angular.toJson(expected);
|
||||
var self = this;
|
||||
this.addFuture('expect ' + description,
|
||||
function(done) {
|
||||
var error;
|
||||
self.actual = self.future.value;
|
||||
if ((self.inverse && fn.call(self, expected)) ||
|
||||
(!self.inverse && !fn.call(self, expected))) {
|
||||
error = 'expected ' + description +
|
||||
' but was ' + angular.toJson(self.actual);
|
||||
}
|
||||
done(error);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the scenario runner and run !
|
||||
*
|
||||
* Access global window and document object
|
||||
* Access $runner through closure
|
||||
*
|
||||
* @param {Object=} config Config options
|
||||
*/
|
||||
angular.scenario.setUpAndRun = function(config) {
|
||||
var href = window.location.href;
|
||||
var body = _jQuery(window.document.body);
|
||||
var output = [];
|
||||
var objModel = new angular.scenario.ObjectModel($runner);
|
||||
|
||||
if (config && config.scenario_output) {
|
||||
output = config.scenario_output.split(',');
|
||||
}
|
||||
|
||||
angular.forEach(angular.scenario.output, function(fn, name) {
|
||||
if (!output.length || output.indexOf(name) !== -1) {
|
||||
var context = body.append('<div></div>').find('div:last');
|
||||
context.attr('id', name);
|
||||
fn.call({}, context, $runner, objModel);
|
||||
}
|
||||
});
|
||||
|
||||
if (!/^http/.test(href) && !/^https/.test(href)) {
|
||||
body.append('<p id="system-error"></p>');
|
||||
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('<div id="application"></div>').find('#application');
|
||||
var application = new angular.scenario.Application(appFrame);
|
||||
|
||||
$runner.on('RunnerEnd', function() {
|
||||
appFrame.css('display', 'none');
|
||||
appFrame.find('iframe').attr('src', 'about:blank');
|
||||
});
|
||||
|
||||
$runner.on('RunnerError', function(error) {
|
||||
if (window.console) {
|
||||
console.log(formatException(error));
|
||||
} else {
|
||||
// Do something for IE
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(error);
|
||||
}
|
||||
});
|
||||
|
||||
$runner.run(application);
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterates through list with iterator function that must call the
|
||||
* continueFunction to continue 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=5] 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 || '';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// We need a handle to the original trigger function for input tests.
|
||||
var parentTrigger = fn._originalTrigger = fn.trigger;
|
||||
fn.trigger = function(type) {
|
||||
if (/(click|change|keydown|blur|input|mousedown|mouseup)/.test(type)) {
|
||||
var processDefaults = [];
|
||||
this.each(function(index, node) {
|
||||
processDefaults.push(browserTrigger(node, type));
|
||||
});
|
||||
|
||||
// this is not compatible with jQuery - we return an array of returned values,
|
||||
// so that scenario runner know whether JS code has preventDefault() of the event or not...
|
||||
return processDefaults;
|
||||
}
|
||||
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} bindExp The name to match
|
||||
* @return {Array.<string>} String of binding values
|
||||
*/
|
||||
_jQuery.fn.bindings = function(windowJquery, bindExp) {
|
||||
var result = [], match,
|
||||
bindSelector = '.ng-binding:visible';
|
||||
if (angular.isString(bindExp)) {
|
||||
bindExp = bindExp.replace(/\s/g, '');
|
||||
match = function(actualExp) {
|
||||
if (actualExp) {
|
||||
actualExp = actualExp.replace(/\s/g, '');
|
||||
if (actualExp === bindExp) return true;
|
||||
if (actualExp.indexOf(bindExp) === 0) {
|
||||
return actualExp.charAt(bindExp.length) === '|';
|
||||
}
|
||||
}
|
||||
};
|
||||
} else if (bindExp) {
|
||||
match = function(actualExp) {
|
||||
return actualExp && bindExp.exec(actualExp);
|
||||
};
|
||||
} else {
|
||||
match = function(actualExp) {
|
||||
return !!actualExp;
|
||||
};
|
||||
}
|
||||
var selection = this.find(bindSelector);
|
||||
if (this.is(bindSelector)) {
|
||||
selection = selection.add(this);
|
||||
}
|
||||
|
||||
function push(value) {
|
||||
if (angular.isUndefined(value)) {
|
||||
value = '';
|
||||
} else if (typeof value !== 'string') {
|
||||
value = angular.toJson(value);
|
||||
}
|
||||
result.push('' + value);
|
||||
}
|
||||
|
||||
selection.each(/* @this Node */ function() {
|
||||
var element = windowJquery(this),
|
||||
bindings;
|
||||
if ((bindings = element.data('$binding'))) {
|
||||
for (var expressions = [], binding, j = 0, jj = bindings.length; j < jj; j++) {
|
||||
binding = bindings[j];
|
||||
|
||||
if (binding.expressions) {
|
||||
expressions = binding.expressions;
|
||||
} else {
|
||||
expressions = [binding];
|
||||
}
|
||||
for (var scope, expression, i = 0, ii = expressions.length; i < ii; i++) {
|
||||
expression = expressions[i];
|
||||
if (match(expression)) {
|
||||
scope = scope || element.scope();
|
||||
push(scope.$eval(expression));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
@@ -1,149 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This class is the "this" of the it/beforeEach/afterEach method.
|
||||
* Responsibilities:
|
||||
* - "this" for it/beforeEach/afterEach
|
||||
* - keep state for single it/beforeEach/afterEach execution
|
||||
* - keep track of all of the futures to execute
|
||||
* - run single spec (execute each future)
|
||||
*/
|
||||
angular.scenario.SpecRunner = function() {
|
||||
this.futures = [];
|
||||
this.afterIndex = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a spec which is an it block with associated before/after functions
|
||||
* based on the describe nesting.
|
||||
*
|
||||
* @param {Object} spec A spec object
|
||||
* @param {function()} specDone function that is called when the spec finishes,
|
||||
* of the form `Function(error, index)`
|
||||
*/
|
||||
angular.scenario.SpecRunner.prototype.run = function(spec, specDone) {
|
||||
var self = this;
|
||||
this.spec = spec;
|
||||
|
||||
this.emit('SpecBegin', spec);
|
||||
|
||||
try {
|
||||
spec.before.call(this);
|
||||
spec.body.call(this);
|
||||
this.afterIndex = this.futures.length;
|
||||
spec.after.call(this);
|
||||
} catch (e) {
|
||||
this.emit('SpecError', spec, e);
|
||||
this.emit('SpecEnd', spec);
|
||||
specDone();
|
||||
return;
|
||||
}
|
||||
|
||||
var handleError = function(error, done) {
|
||||
if (self.error) {
|
||||
return done();
|
||||
}
|
||||
self.error = true;
|
||||
done(null, self.afterIndex);
|
||||
};
|
||||
|
||||
asyncForEach(
|
||||
this.futures,
|
||||
function(future, futureDone) {
|
||||
self.step = future;
|
||||
self.emit('StepBegin', spec, future);
|
||||
try {
|
||||
future.execute(function(error) {
|
||||
if (error) {
|
||||
self.emit('StepFailure', spec, future, error);
|
||||
self.emit('StepEnd', spec, future);
|
||||
return handleError(error, futureDone);
|
||||
}
|
||||
self.emit('StepEnd', spec, future);
|
||||
self.$window.setTimeout(function() { futureDone(); }, 0);
|
||||
});
|
||||
} catch (e) {
|
||||
self.emit('StepError', spec, future, e);
|
||||
self.emit('StepEnd', spec, future);
|
||||
handleError(e, futureDone);
|
||||
}
|
||||
},
|
||||
function(e) {
|
||||
if (e) {
|
||||
self.emit('SpecError', spec, e);
|
||||
}
|
||||
self.emit('SpecEnd', spec);
|
||||
// Call done in a timeout so exceptions don't recursively
|
||||
// call this function
|
||||
self.$window.setTimeout(function() { specDone(); }, 0);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a new future action.
|
||||
*
|
||||
* Note: Do not pass line manually. It happens automatically.
|
||||
*
|
||||
* @param {string} name Name of the future
|
||||
* @param {function()} behavior Behavior of the future
|
||||
* @param {function()} line fn() that returns file/line number
|
||||
*/
|
||||
angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) {
|
||||
var future = new angular.scenario.Future(name, angular.bind(this, behavior), line);
|
||||
this.futures.push(future);
|
||||
return future;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a new future action to be executed on the application window.
|
||||
*
|
||||
* Note: Do not pass line manually. It happens automatically.
|
||||
*
|
||||
* @param {string} name Name of the future
|
||||
* @param {function()} behavior Behavior of the future
|
||||
* @param {function()} line fn() that returns file/line number
|
||||
*/
|
||||
angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) {
|
||||
var self = this;
|
||||
var NG = /\[ng\\:/;
|
||||
return this.addFuture(name, /** @this */ function(done) {
|
||||
this.application.executeAction(function($window, $document) {
|
||||
|
||||
//TODO(esprehn): Refactor this so it doesn't need to be in here.
|
||||
$document.elements = function(selector) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
selector = (self.selector || '') + ' ' + (selector || '');
|
||||
selector = _jQuery.trim(selector) || '*';
|
||||
angular.forEach(args, function(value, index) {
|
||||
selector = selector.replace('$' + (index + 1), value);
|
||||
});
|
||||
var result = $document.find(selector);
|
||||
if (selector.match(NG)) {
|
||||
angular.forEach(['[ng-','[data-ng-','[x-ng-'], function(value, index) {
|
||||
result = result.add(selector.replace(NG, value), $document);
|
||||
});
|
||||
}
|
||||
if (!result.length) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw {
|
||||
type: 'selector',
|
||||
message: 'Selector ' + selector + ' did not match any elements.'
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
try {
|
||||
behavior.call(self, $window, $document, done);
|
||||
} catch (e) {
|
||||
if (e.type && e.type === 'selector') {
|
||||
done(e.message);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, line);
|
||||
};
|
||||
Vendored
-60
@@ -1,60 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
(function(previousOnLoad) {
|
||||
var prefix = (function() {
|
||||
var filename = /(.*\/)angular-bootstrap.js(#(.*))?/;
|
||||
var scripts = window.document.getElementsByTagName('script');
|
||||
for (var j = 0; j < scripts.length; j++) {
|
||||
var src = scripts[j].src;
|
||||
if (src && src.match(filename)) {
|
||||
var parts = src.match(filename);
|
||||
return parts[1];
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
function addScript(path) {
|
||||
window.document.write('<script type="text/javascript" src="' + prefix + path + '"></script>');
|
||||
}
|
||||
|
||||
function addCSS(path) {
|
||||
window.document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>');
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
try {
|
||||
if (previousOnLoad) previousOnLoad();
|
||||
} catch (e) { /* empty */ }
|
||||
angular.scenario.setUpAndRun({});
|
||||
};
|
||||
|
||||
addCSS('../../css/angular-scenario.css');
|
||||
addScript('../../lib/jquery/jquery.js');
|
||||
window.document.write(
|
||||
'<script type="text/javascript">' +
|
||||
'var _jQuery = jQuery.noConflict(true);' +
|
||||
'</script>'
|
||||
);
|
||||
addScript('../angular-bootstrap.js');
|
||||
|
||||
addScript('Scenario.js');
|
||||
addScript('Application.js');
|
||||
addScript('Describe.js');
|
||||
addScript('Future.js');
|
||||
addScript('Runner.js');
|
||||
addScript('SpecRunner.js');
|
||||
addScript('dsl.js');
|
||||
addScript('matchers.js');
|
||||
addScript('ObjectModel.js');
|
||||
addScript('output/Html.js');
|
||||
addScript('output/Json.js');
|
||||
addScript('output/Object.js');
|
||||
addScript('output/Xml.js');
|
||||
|
||||
// Create the runner (which also sets up the global API)
|
||||
window.document.write(
|
||||
'<script type="text/javascript">' +
|
||||
' var $runner = new angular.scenario.Runner(window);' +
|
||||
'</script>');
|
||||
|
||||
})(window.onload);
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* @license AngularJS v"NG_VERSION_FULL"
|
||||
* (c) 2010-2017 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window){
|
||||
var _jQuery = window.jQuery.noConflict(true);
|
||||
@@ -1,22 +0,0 @@
|
||||
bindJQuery();
|
||||
publishExternalAPI(angular);
|
||||
|
||||
var $runner = new angular.scenario.Runner(window),
|
||||
scripts = window.document.getElementsByTagName('script'),
|
||||
script = scripts[scripts.length - 1],
|
||||
config = {};
|
||||
|
||||
angular.forEach(script.attributes, function(attr) {
|
||||
var match = attr.name.match(/ng[:\-](.*)/);
|
||||
if (match) {
|
||||
config[match[1]] = attr.value || true;
|
||||
}
|
||||
});
|
||||
|
||||
if (config.autotest) {
|
||||
JQLite(function() {
|
||||
angular.scenario.setUpAndRun(config);
|
||||
});
|
||||
}
|
||||
})(window);
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-invalid-this */
|
||||
|
||||
/**
|
||||
* Shared DSL statements that are useful to all scenarios.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* pause() pauses until you call resume() in the console
|
||||
*/
|
||||
angular.scenario.dsl('pause', function() {
|
||||
return function() {
|
||||
return this.addFuture('pausing for you to resume', function(done) {
|
||||
this.emit('InteractivePause', this.spec, this.step);
|
||||
this.$window.resume = function() { done(); };
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* sleep(seconds) pauses the test for specified number of seconds
|
||||
*/
|
||||
angular.scenario.dsl('sleep', function() {
|
||||
return function(time) {
|
||||
return this.addFuture('sleep for ' + time + ' seconds', function(done) {
|
||||
this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* browser().navigateTo(url) Loads the url into the frame
|
||||
* browser().navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to
|
||||
* browser().reload() refresh the page (reload the same URL)
|
||||
* browser().window.href() window.location.href
|
||||
* browser().window.path() window.location.pathname
|
||||
* browser().window.search() window.location.search
|
||||
* browser().window.hash() window.location.hash without # prefix
|
||||
* browser().location().url() see ng.$location#url
|
||||
* browser().location().path() see ng.$location#path
|
||||
* browser().location().search() see ng.$location#search
|
||||
* browser().location().hash() see ng.$location#hash
|
||||
*/
|
||||
angular.scenario.dsl('browser', function() {
|
||||
var chain = {};
|
||||
|
||||
chain.navigateTo = function(url, delegate) {
|
||||
var application = this.application;
|
||||
return this.addFuture('browser navigate to \'' + url + '\'', function(done) {
|
||||
if (delegate) {
|
||||
url = delegate.call(this, url);
|
||||
}
|
||||
application.navigateTo(url, function() {
|
||||
done(null, url);
|
||||
}, done);
|
||||
});
|
||||
};
|
||||
|
||||
chain.reload = function() {
|
||||
var application = this.application;
|
||||
return this.addFutureAction('browser reload', function($window, $document, done) {
|
||||
var href = $window.location.href;
|
||||
application.navigateTo(href, function() {
|
||||
done(null, href);
|
||||
}, done);
|
||||
});
|
||||
};
|
||||
|
||||
chain.window = function() {
|
||||
var api = {};
|
||||
|
||||
api.href = function() {
|
||||
return this.addFutureAction('window.location.href', function($window, $document, done) {
|
||||
done(null, $window.location.href);
|
||||
});
|
||||
};
|
||||
|
||||
api.path = function() {
|
||||
return this.addFutureAction('window.location.path', function($window, $document, done) {
|
||||
done(null, $window.location.pathname);
|
||||
});
|
||||
};
|
||||
|
||||
api.search = function() {
|
||||
return this.addFutureAction('window.location.search', function($window, $document, done) {
|
||||
done(null, $window.location.search);
|
||||
});
|
||||
};
|
||||
|
||||
api.hash = function() {
|
||||
return this.addFutureAction('window.location.hash', function($window, $document, done) {
|
||||
done(null, $window.location.hash.replace('#', ''));
|
||||
});
|
||||
};
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
chain.location = function() {
|
||||
var api = {};
|
||||
|
||||
api.url = function() {
|
||||
return this.addFutureAction('$location.url()', function($window, $document, done) {
|
||||
done(null, $document.injector().get('$location').url());
|
||||
});
|
||||
};
|
||||
|
||||
api.path = function() {
|
||||
return this.addFutureAction('$location.path()', function($window, $document, done) {
|
||||
done(null, $document.injector().get('$location').path());
|
||||
});
|
||||
};
|
||||
|
||||
api.search = function() {
|
||||
return this.addFutureAction('$location.search()', function($window, $document, done) {
|
||||
done(null, $document.injector().get('$location').search());
|
||||
});
|
||||
};
|
||||
|
||||
api.hash = function() {
|
||||
return this.addFutureAction('$location.hash()', function($window, $document, done) {
|
||||
done(null, $document.injector().get('$location').hash());
|
||||
});
|
||||
};
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
return function() {
|
||||
return chain;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* expect(future).{matcher} where matcher is one of the matchers defined
|
||||
* with angular.scenario.matcher
|
||||
*
|
||||
* ex. expect(binding("name")).toEqual("Elliott")
|
||||
*/
|
||||
angular.scenario.dsl('expect', function() {
|
||||
var chain = angular.extend({}, angular.scenario.matcher);
|
||||
|
||||
chain.not = function() {
|
||||
this.inverse = true;
|
||||
return chain;
|
||||
};
|
||||
|
||||
return function(future) {
|
||||
this.future = future;
|
||||
return chain;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* using(selector, label) scopes the next DSL element selection
|
||||
*
|
||||
* ex.
|
||||
* using('#foo', "'Foo' text field").input('bar')
|
||||
*/
|
||||
angular.scenario.dsl('using', function() {
|
||||
return function(selector, label) {
|
||||
this.selector = _jQuery.trim((this.selector || '') + ' ' + selector);
|
||||
if (angular.isString(label) && label.length) {
|
||||
this.label = label + ' ( ' + this.selector + ' )';
|
||||
} else {
|
||||
this.label = this.selector;
|
||||
}
|
||||
return this.dsl;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* binding(name) returns the value of the first matching binding
|
||||
*/
|
||||
angular.scenario.dsl('binding', function() {
|
||||
return function(name) {
|
||||
return this.addFutureAction('select binding \'' + name + '\'',
|
||||
function($window, $document, done) {
|
||||
var values = $document.elements().bindings($window.angular.element, name);
|
||||
if (!values.length) {
|
||||
return done('Binding selector \'' + name + '\' did not match.');
|
||||
}
|
||||
done(null, values[0]);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* input(name).enter(value) enters value in input with specified name
|
||||
* input(name).check() checks checkbox
|
||||
* input(name).select(value) selects the radio button with specified name/value
|
||||
* input(name).val() returns the value of the input.
|
||||
*/
|
||||
angular.scenario.dsl('input', function() {
|
||||
var chain = {};
|
||||
|
||||
// Support: IE 9-11 only
|
||||
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
|
||||
// it. In particular the event is not fired when backspace or delete key are pressed or
|
||||
// when cut operation is performed.
|
||||
// IE10+ implements 'input' event but it erroneously fires under various situations,
|
||||
// e.g. when placeholder changes, or a form is focused.
|
||||
var supportInputEvent = 'oninput' in window.document.createElement('div') && !msie;
|
||||
|
||||
chain.enter = function(value, event) {
|
||||
return this.addFutureAction('input \'' + this.name + '\' enter \'' + value + '\'',
|
||||
function($window, $document, done) {
|
||||
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
|
||||
input.val(value);
|
||||
input.trigger(event || (supportInputEvent ? 'input' : 'change'));
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
chain.check = function() {
|
||||
return this.addFutureAction('checkbox \'' + this.name + '\' toggle',
|
||||
function($window, $document, done) {
|
||||
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':checkbox');
|
||||
input.trigger('click');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
chain.select = function(value) {
|
||||
return this.addFutureAction('radio button \'' + this.name + '\' toggle \'' + value + '\'',
|
||||
function($window, $document, done) {
|
||||
var input = $document.
|
||||
elements('[ng\\:model="$1"][value="$2"]', this.name, value).filter(':radio');
|
||||
input.trigger('click');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
chain.val = function() {
|
||||
return this.addFutureAction('return input val', function($window, $document, done) {
|
||||
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
|
||||
done(null,input.val());
|
||||
});
|
||||
};
|
||||
|
||||
return function(name) {
|
||||
this.name = name;
|
||||
return chain;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* repeater('#products table', 'Product List').count() number of rows
|
||||
* repeater('#products table', 'Product List').row(1) all bindings in row as an array
|
||||
* repeater('#products table', 'Product List').column('product.name') all values across all rows
|
||||
* in an array
|
||||
*/
|
||||
angular.scenario.dsl('repeater', function() {
|
||||
var chain = {};
|
||||
|
||||
chain.count = function() {
|
||||
return this.addFutureAction('repeater \'' + this.label + '\' count',
|
||||
function($window, $document, done) {
|
||||
try {
|
||||
done(null, $document.elements().length);
|
||||
} catch (e) {
|
||||
done(null, 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
chain.column = function(binding) {
|
||||
return this.addFutureAction('repeater \'' + this.label + '\' column \'' + binding + '\'',
|
||||
function($window, $document, done) {
|
||||
done(null, $document.elements().bindings($window.angular.element, binding));
|
||||
});
|
||||
};
|
||||
|
||||
chain.row = function(index) {
|
||||
return this.addFutureAction('repeater \'' + this.label + '\' row \'' + index + '\'',
|
||||
function($window, $document, done) {
|
||||
var matches = $document.elements().slice(index, index + 1);
|
||||
if (!matches.length) {
|
||||
return done('row ' + index + ' out of bounds');
|
||||
}
|
||||
done(null, matches.bindings($window.angular.element));
|
||||
});
|
||||
};
|
||||
|
||||
return function(selector, label) {
|
||||
this.dsl.using(selector, label);
|
||||
return chain;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* select(name).option('value') select one option
|
||||
* select(name).options('value1', 'value2', ...) select options from a multi select
|
||||
*/
|
||||
angular.scenario.dsl('select', function() {
|
||||
var chain = {};
|
||||
|
||||
chain.option = function(value) {
|
||||
return this.addFutureAction('select \'' + this.name + '\' option \'' + value + '\'',
|
||||
function($window, $document, done) {
|
||||
var select = $document.elements('select[ng\\:model="$1"]', this.name);
|
||||
var option = select.find('option[value="' + value + '"]');
|
||||
if (option.length) {
|
||||
select.val(value);
|
||||
} else {
|
||||
option = select.find('option').filter(function() {
|
||||
return _jQuery(this).text() === value;
|
||||
});
|
||||
if (!option.length) {
|
||||
option = select.find('option:contains("' + value + '")');
|
||||
}
|
||||
if (option.length) {
|
||||
select.val(option.val());
|
||||
} else {
|
||||
return done('option \'' + value + '\' not found');
|
||||
}
|
||||
}
|
||||
select.trigger('change');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
chain.options = function() {
|
||||
var values = arguments;
|
||||
return this.addFutureAction('select \'' + this.name + '\' options \'' + values + '\'',
|
||||
function($window, $document, done) {
|
||||
var select = $document.elements('select[multiple][ng\\:model="$1"]', this.name);
|
||||
select.val(values);
|
||||
select.trigger('change');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
return function(name) {
|
||||
this.name = name;
|
||||
return chain;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* element(selector, label).count() get the number of elements that match selector
|
||||
* element(selector, label).click() clicks an element
|
||||
* element(selector, label).mouseover() mouseover an element
|
||||
* element(selector, label).mousedown() mousedown an element
|
||||
* element(selector, label).mouseup() mouseup an element
|
||||
* element(selector, label).query(fn) executes fn(selectedElements, done)
|
||||
* element(selector, label).{method}() gets the value (as defined by jQuery, ex. val)
|
||||
* element(selector, label).{method}(value) sets the value (as defined by jQuery, ex. val)
|
||||
* element(selector, label).{method}(key) gets the value (as defined by jQuery, ex. attr)
|
||||
* element(selector, label).{method}(key, value) sets the value (as defined by jQuery, ex. attr)
|
||||
*/
|
||||
angular.scenario.dsl('element', function() {
|
||||
var KEY_VALUE_METHODS = ['attr', 'css', 'prop'];
|
||||
var VALUE_METHODS = [
|
||||
'val', 'text', 'html', 'height', 'innerHeight', 'outerHeight', 'width',
|
||||
'innerWidth', 'outerWidth', 'position', 'scrollLeft', 'scrollTop', 'offset'
|
||||
];
|
||||
var chain = {};
|
||||
|
||||
chain.count = function() {
|
||||
return this.addFutureAction('element \'' + this.label + '\' count',
|
||||
function($window, $document, done) {
|
||||
try {
|
||||
done(null, $document.elements().length);
|
||||
} catch (e) {
|
||||
done(null, 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
chain.click = function() {
|
||||
return this.addFutureAction('element \'' + this.label + '\' click',
|
||||
function($window, $document, done) {
|
||||
var elements = $document.elements();
|
||||
var href = elements.attr('href');
|
||||
var eventProcessDefault = elements.trigger('click')[0];
|
||||
|
||||
if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) {
|
||||
this.application.navigateTo(href, function() {
|
||||
done();
|
||||
}, done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
chain.dblclick = function() {
|
||||
return this.addFutureAction('element \'' + this.label + '\' dblclick',
|
||||
function($window, $document, done) {
|
||||
var elements = $document.elements();
|
||||
var href = elements.attr('href');
|
||||
var eventProcessDefault = elements.trigger('dblclick')[0];
|
||||
|
||||
if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) {
|
||||
this.application.navigateTo(href, function() {
|
||||
done();
|
||||
}, done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
chain.mouseover = function() {
|
||||
return this.addFutureAction('element \'' + this.label + '\' mouseover',
|
||||
function($window, $document, done) {
|
||||
var elements = $document.elements();
|
||||
elements.trigger('mouseover');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
chain.mousedown = function() {
|
||||
return this.addFutureAction('element \'' + this.label + '\' mousedown',
|
||||
function($window, $document, done) {
|
||||
var elements = $document.elements();
|
||||
elements.trigger('mousedown');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
chain.mouseup = function() {
|
||||
return this.addFutureAction('element \'' + this.label + '\' mouseup',
|
||||
function($window, $document, done) {
|
||||
var elements = $document.elements();
|
||||
elements.trigger('mouseup');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
chain.query = function(fn) {
|
||||
return this.addFutureAction('element ' + this.label + ' custom query',
|
||||
function($window, $document, done) {
|
||||
fn.call(this, $document.elements(), done);
|
||||
});
|
||||
};
|
||||
|
||||
angular.forEach(KEY_VALUE_METHODS, function(methodName) {
|
||||
chain[methodName] = function(name, value) {
|
||||
var args = arguments,
|
||||
futureName = (args.length === 1)
|
||||
? 'element \'' + this.label + '\' get ' + methodName + ' \'' + name + '\''
|
||||
: 'element \'' + this.label + '\' set ' + methodName + ' \'' + name + '\' to \'' +
|
||||
value + '\'';
|
||||
|
||||
return this.addFutureAction(futureName, function($window, $document, done) {
|
||||
var element = $document.elements();
|
||||
done(null, element[methodName].apply(element, args));
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
angular.forEach(VALUE_METHODS, function(methodName) {
|
||||
chain[methodName] = function(value) {
|
||||
var args = arguments,
|
||||
futureName = (args.length === 0)
|
||||
? 'element \'' + this.label + '\' ' + methodName
|
||||
: 'element \'' + this.label + '\' set ' + methodName + ' to \'' + value + '\'';
|
||||
|
||||
return this.addFutureAction(futureName, function($window, $document, done) {
|
||||
var element = $document.elements();
|
||||
done(null, element[methodName].apply(element, args));
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return function(selector, label) {
|
||||
this.dsl.using(selector, label);
|
||||
return chain;
|
||||
};
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Matchers for implementing specs. Follows the Jasmine spec conventions.
|
||||
*/
|
||||
|
||||
angular.scenario.matcher('toEqual', /** @this */ function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toBe', /** @this */ function(expected) {
|
||||
return this.actual === expected;
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toBeDefined', /** @this */ function() {
|
||||
return angular.isDefined(this.actual);
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toBeTruthy', /** @this */ function() {
|
||||
return this.actual;
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toBeFalsy', /** @this */ function() {
|
||||
return !this.actual;
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toMatch', /** @this */ function(expected) {
|
||||
return new RegExp(expected).test(this.actual);
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toBeNull', /** @this */ function() {
|
||||
return this.actual === null;
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toContain', /** @this */ function(expected) {
|
||||
return includes(this.actual, expected);
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toBeLessThan', /** @this */ function(expected) {
|
||||
return this.actual < expected;
|
||||
});
|
||||
|
||||
angular.scenario.matcher('toBeGreaterThan', /** @this */ function(expected) {
|
||||
return this.actual > expected;
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* User Interface for the Scenario Runner.
|
||||
*
|
||||
* TODO(esprehn): This should be refactored now that ObjectModel exists
|
||||
* to use AngularJS bindings for the UI.
|
||||
*/
|
||||
angular.scenario.output('html', function(context, runner, model) {
|
||||
var specUiMap = {},
|
||||
lastStepUiMap = {};
|
||||
|
||||
context.append(
|
||||
'<div id="header">' +
|
||||
' <h1><span class="angular">AngularJS</span>: Scenario Test Runner</h1>' +
|
||||
' <ul id="status-legend" class="status-display">' +
|
||||
' <li class="status-error">0 Errors</li>' +
|
||||
' <li class="status-failure">0 Failures</li>' +
|
||||
' <li class="status-success">0 Passed</li>' +
|
||||
' </ul>' +
|
||||
'</div>' +
|
||||
'<div id="specs">' +
|
||||
' <div class="test-children"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
runner.on('InteractivePause', function(spec) {
|
||||
var ui = lastStepUiMap[spec.id];
|
||||
ui.find('.test-title').
|
||||
html('paused... <a href="javascript:resume()">resume</a> when ready.');
|
||||
});
|
||||
|
||||
runner.on('SpecBegin', function(spec) {
|
||||
var ui = findContext(spec);
|
||||
ui.find('> .tests').append(
|
||||
'<li class="status-pending test-it"></li>'
|
||||
);
|
||||
ui = ui.find('> .tests li:last');
|
||||
ui.append(
|
||||
'<div class="test-info">' +
|
||||
' <p class="test-title">' +
|
||||
' <span class="timer-result"></span>' +
|
||||
' <span class="test-name"></span>' +
|
||||
' </p>' +
|
||||
'</div>' +
|
||||
'<div class="scrollpane">' +
|
||||
' <ol class="test-actions"></ol>' +
|
||||
'</div>'
|
||||
);
|
||||
ui.find('> .test-info .test-name').text(spec.name);
|
||||
ui.find('> .test-info').click(function() {
|
||||
var scrollpane = ui.find('> .scrollpane');
|
||||
var actions = scrollpane.find('> .test-actions');
|
||||
var name = context.find('> .test-info .test-name');
|
||||
if (actions.find(':visible').length) {
|
||||
actions.hide();
|
||||
name.removeClass('open').addClass('closed');
|
||||
} else {
|
||||
actions.show();
|
||||
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
|
||||
name.removeClass('closed').addClass('open');
|
||||
}
|
||||
});
|
||||
|
||||
specUiMap[spec.id] = ui;
|
||||
});
|
||||
|
||||
runner.on('SpecError', function(spec, error) {
|
||||
var ui = specUiMap[spec.id];
|
||||
ui.append('<pre></pre>');
|
||||
ui.find('> pre').text(formatException(error));
|
||||
});
|
||||
|
||||
runner.on('SpecEnd', function(spec) {
|
||||
var ui = specUiMap[spec.id];
|
||||
spec = model.getSpec(spec.id);
|
||||
ui.removeClass('status-pending');
|
||||
ui.addClass('status-' + spec.status);
|
||||
ui.find('> .test-info .timer-result').text(spec.duration + 'ms');
|
||||
if (spec.status === 'success') {
|
||||
ui.find('> .test-info .test-name').addClass('closed');
|
||||
ui.find('> .scrollpane .test-actions').hide();
|
||||
}
|
||||
updateTotals(spec.status);
|
||||
});
|
||||
|
||||
runner.on('StepBegin', function(spec, step) {
|
||||
var ui = specUiMap[spec.id];
|
||||
spec = model.getSpec(spec.id);
|
||||
step = spec.getLastStep();
|
||||
ui.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>');
|
||||
var stepUi = lastStepUiMap[spec.id] = ui.find('> .scrollpane .test-actions li:last');
|
||||
stepUi.append(
|
||||
'<div class="timer-result"></div>' +
|
||||
'<div class="test-title"></div>'
|
||||
);
|
||||
stepUi.find('> .test-title').text(step.name);
|
||||
var scrollpane = stepUi.parents('.scrollpane');
|
||||
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
|
||||
});
|
||||
|
||||
runner.on('StepFailure', function(spec, step, error) {
|
||||
var ui = lastStepUiMap[spec.id];
|
||||
addError(ui, step.line, error);
|
||||
});
|
||||
|
||||
runner.on('StepError', function(spec, step, error) {
|
||||
var ui = lastStepUiMap[spec.id];
|
||||
addError(ui, step.line, error);
|
||||
});
|
||||
|
||||
runner.on('StepEnd', function(spec, step) {
|
||||
var stepUi = lastStepUiMap[spec.id];
|
||||
spec = model.getSpec(spec.id);
|
||||
step = spec.getLastStep();
|
||||
stepUi.find('.timer-result').text(step.duration + 'ms');
|
||||
stepUi.removeClass('status-pending');
|
||||
stepUi.addClass('status-' + step.status);
|
||||
var scrollpane = specUiMap[spec.id].find('> .scrollpane');
|
||||
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds the context of a spec block defined by the passed definition.
|
||||
*
|
||||
* @param {Object} The definition created by the Describe object.
|
||||
*/
|
||||
function findContext(spec) {
|
||||
var currentContext = context.find('#specs');
|
||||
angular.forEach(model.getDefinitionPath(spec), function(defn) {
|
||||
var id = 'describe-' + defn.id;
|
||||
if (!context.find('#' + id).length) {
|
||||
currentContext.find('> .test-children').append(
|
||||
'<div class="test-describe" id="' + id + '">' +
|
||||
' <h2></h2>' +
|
||||
' <div class="test-children"></div>' +
|
||||
' <ul class="tests"></ul>' +
|
||||
'</div>'
|
||||
);
|
||||
context.find('#' + id).find('> h2').text('describe: ' + defn.name);
|
||||
}
|
||||
currentContext = context.find('#' + id);
|
||||
});
|
||||
return context.find('#describe-' + spec.definition.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the test counter for the status.
|
||||
*
|
||||
* @param {string} the status.
|
||||
*/
|
||||
function updateTotals(status) {
|
||||
var legend = context.find('#status-legend .status-' + status);
|
||||
var parts = legend.text().split(' ');
|
||||
var value = (parts[0] * 1) + 1;
|
||||
legend.text(value + ' ' + parts[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error to a step.
|
||||
*
|
||||
* @param {Object} The JQuery wrapped context
|
||||
* @param {function()} fn() that should return the file/line number of the error
|
||||
* @param {Object} the error.
|
||||
*/
|
||||
function addError(context, line, error) {
|
||||
context.find('.test-title').append('<pre></pre>');
|
||||
var message = _jQuery.trim(line() + '\n\n' + formatException(error));
|
||||
context.find('.test-title pre:last').text(message);
|
||||
}
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Generates JSON output into a context.
|
||||
*/
|
||||
angular.scenario.output('json', function(context, runner, model) {
|
||||
model.on('RunnerEnd', function() {
|
||||
context.text(angular.toJson(model.value));
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Creates a global value $result with the result of the runner.
|
||||
*/
|
||||
angular.scenario.output('object', function(context, runner, model) {
|
||||
runner.$window.$result = model.value;
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Generates XML output into a context.
|
||||
*/
|
||||
angular.scenario.output('xml', function(context, runner, model) {
|
||||
var $ = function(args) {
|
||||
// eslint-disable-next-line new-cap
|
||||
return new context.init(args);
|
||||
};
|
||||
model.on('RunnerEnd', function() {
|
||||
var scenario = $('<scenario></scenario>');
|
||||
context.append(scenario);
|
||||
serializeXml(scenario, model.value);
|
||||
});
|
||||
|
||||
/**
|
||||
* Convert the tree into XML.
|
||||
*
|
||||
* @param {Object} context jQuery context to add the XML to.
|
||||
* @param {Object} tree node to serialize
|
||||
*/
|
||||
function serializeXml(context, tree) {
|
||||
angular.forEach(tree.children, function(child) {
|
||||
var describeContext = $('<describe></describe>');
|
||||
describeContext.attr('id', child.id);
|
||||
describeContext.attr('name', child.name);
|
||||
context.append(describeContext);
|
||||
serializeXml(describeContext, child);
|
||||
});
|
||||
var its = $('<its></its>');
|
||||
context.append(its);
|
||||
angular.forEach(tree.specs, function(spec) {
|
||||
var it = $('<it></it>');
|
||||
it.attr('id', spec.id);
|
||||
it.attr('name', spec.name);
|
||||
it.attr('duration', spec.duration);
|
||||
it.attr('status', spec.status);
|
||||
its.append(it);
|
||||
angular.forEach(spec.steps, function(step) {
|
||||
var stepContext = $('<step></step>');
|
||||
stepContext.attr('name', step.name);
|
||||
stepContext.attr('duration', step.duration);
|
||||
stepContext.attr('status', step.status);
|
||||
it.append(stepContext);
|
||||
if (step.error) {
|
||||
var error = $('<error></error>');
|
||||
stepContext.append(error);
|
||||
error.text(formatException(step.error));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,298 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global ngTouch: false,
|
||||
nodeName_: false
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngClick
|
||||
* @deprecated
|
||||
* sinceVersion="v1.5.0"
|
||||
* This directive is deprecated and **disabled** by default.
|
||||
* The directive will receive no further support and might be removed from future releases.
|
||||
* If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled}
|
||||
* function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick).
|
||||
* To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/)
|
||||
* gives a good overview.
|
||||
*
|
||||
* @description
|
||||
* A more powerful replacement for the default ngClick designed to be used on touchscreen
|
||||
* devices. Most mobile browsers wait about 300ms after a tap-and-release before sending
|
||||
* the click event. This version handles them immediately, and then prevents the
|
||||
* following click event from propagating.
|
||||
*
|
||||
* Requires the {@link ngTouch `ngTouch`} module to be installed.
|
||||
*
|
||||
* This directive can fall back to using an ordinary click event, and so works on desktop
|
||||
* browsers as well as mobile.
|
||||
*
|
||||
* This directive also sets the CSS class `ng-click-active` while the element is being held
|
||||
* down (by a mouse click or touch) so you can restyle the depressed element if you wish.
|
||||
*
|
||||
* @element ANY
|
||||
* @param {expression} ngClick {@link guide/expression Expression} to evaluate
|
||||
* upon tap. (Event object is available as `$event`)
|
||||
*
|
||||
* @example
|
||||
<example module="ngClickExample" deps="angular-touch.js" name="ng-touch-ng-click">
|
||||
<file name="index.html">
|
||||
<button ng-click="count = count + 1" ng-init="count=0">
|
||||
Increment
|
||||
</button>
|
||||
count: {{ count }}
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('ngClickExample', ['ngTouch']);
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
|
||||
function($parse, $timeout, $rootElement) {
|
||||
var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
|
||||
var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
|
||||
var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click
|
||||
var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
|
||||
|
||||
var ACTIVE_CLASS_NAME = 'ng-click-active';
|
||||
var lastPreventedTime;
|
||||
var touchCoordinates;
|
||||
var lastLabelClickCoordinates;
|
||||
|
||||
|
||||
// TAP EVENTS AND GHOST CLICKS
|
||||
//
|
||||
// Why tap events?
|
||||
// Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're
|
||||
// double-tapping, and then fire a click event.
|
||||
//
|
||||
// This delay sucks and makes mobile apps feel unresponsive.
|
||||
// So we detect touchstart, touchcancel and touchend ourselves and determine when
|
||||
// the user has tapped on something.
|
||||
//
|
||||
// What happens when the browser then generates a click event?
|
||||
// The browser, of course, also detects the tap and fires a click after a delay. This results in
|
||||
// tapping/clicking twice. We do "clickbusting" to prevent it.
|
||||
//
|
||||
// How does it work?
|
||||
// We attach global touchstart and click handlers, that run during the capture (early) phase.
|
||||
// So the sequence for a tap is:
|
||||
// - global touchstart: Sets an "allowable region" at the point touched.
|
||||
// - element's touchstart: Starts a touch
|
||||
// (- touchcancel ends the touch, no click follows)
|
||||
// - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
|
||||
// too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
|
||||
// - preventGhostClick() removes the allowable region the global touchstart created.
|
||||
// - The browser generates a click event.
|
||||
// - The global click handler catches the click, and checks whether it was in an allowable region.
|
||||
// - If preventGhostClick was called, the region will have been removed, the click is busted.
|
||||
// - If the region is still there, the click proceeds normally. Therefore clicks on links and
|
||||
// other elements without ngTap on them work normally.
|
||||
//
|
||||
// This is an ugly, terrible hack!
|
||||
// Yeah, tell me about it. The alternatives are using the slow click events, or making our users
|
||||
// deal with the ghost clicks, so I consider this the least of evils. Fortunately AngularJS
|
||||
// encapsulates this ugly logic away from the user.
|
||||
//
|
||||
// Why not just put click handlers on the element?
|
||||
// We do that too, just to be sure. If the tap event caused the DOM to change,
|
||||
// it is possible another element is now in that position. To take account for these possibly
|
||||
// distinct elements, the handlers are global and care only about coordinates.
|
||||
|
||||
// Checks if the coordinates are close enough to be within the region.
|
||||
function hit(x1, y1, x2, y2) {
|
||||
return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD;
|
||||
}
|
||||
|
||||
// Checks a list of allowable regions against a click location.
|
||||
// Returns true if the click should be allowed.
|
||||
// Splices out the allowable region from the list after it has been used.
|
||||
function checkAllowableRegions(touchCoordinates, x, y) {
|
||||
for (var i = 0; i < touchCoordinates.length; i += 2) {
|
||||
if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) {
|
||||
touchCoordinates.splice(i, i + 2);
|
||||
return true; // allowable region
|
||||
}
|
||||
}
|
||||
return false; // No allowable region; bust it.
|
||||
}
|
||||
|
||||
// Global click handler that prevents the click if it's in a bustable zone and preventGhostClick
|
||||
// was called recently.
|
||||
function onClick(event) {
|
||||
if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
|
||||
return; // Too old.
|
||||
}
|
||||
|
||||
var touches = event.touches && event.touches.length ? event.touches : [event];
|
||||
var x = touches[0].clientX;
|
||||
var y = touches[0].clientY;
|
||||
// Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label
|
||||
// and on the input element). Depending on the exact browser, this second click we don't want
|
||||
// to bust has either (0,0), negative coordinates, or coordinates equal to triggering label
|
||||
// click event
|
||||
if (x < 1 && y < 1) {
|
||||
return; // offscreen
|
||||
}
|
||||
if (lastLabelClickCoordinates &&
|
||||
lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) {
|
||||
return; // input click triggered by label click
|
||||
}
|
||||
// reset label click coordinates on first subsequent click
|
||||
if (lastLabelClickCoordinates) {
|
||||
lastLabelClickCoordinates = null;
|
||||
}
|
||||
// remember label click coordinates to prevent click busting of trigger click event on input
|
||||
if (nodeName_(event.target) === 'label') {
|
||||
lastLabelClickCoordinates = [x, y];
|
||||
}
|
||||
|
||||
// Look for an allowable region containing this click.
|
||||
// If we find one, that means it was created by touchstart and not removed by
|
||||
// preventGhostClick, so we don't bust it.
|
||||
if (checkAllowableRegions(touchCoordinates, x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we didn't find an allowable region, bust the click.
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
// Blur focused form elements
|
||||
if (event.target && event.target.blur) {
|
||||
event.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Global touchstart handler that creates an allowable region for a click event.
|
||||
// This allowable region can be removed by preventGhostClick if we want to bust it.
|
||||
function onTouchStart(event) {
|
||||
var touches = event.touches && event.touches.length ? event.touches : [event];
|
||||
var x = touches[0].clientX;
|
||||
var y = touches[0].clientY;
|
||||
touchCoordinates.push(x, y);
|
||||
|
||||
$timeout(function() {
|
||||
// Remove the allowable region.
|
||||
for (var i = 0; i < touchCoordinates.length; i += 2) {
|
||||
if (touchCoordinates[i] === x && touchCoordinates[i + 1] === y) {
|
||||
touchCoordinates.splice(i, i + 2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, PREVENT_DURATION, false);
|
||||
}
|
||||
|
||||
// On the first call, attaches some event handlers. Then whenever it gets called, it creates a
|
||||
// zone around the touchstart where clicks will get busted.
|
||||
function preventGhostClick(x, y) {
|
||||
if (!touchCoordinates) {
|
||||
$rootElement[0].addEventListener('click', onClick, true);
|
||||
$rootElement[0].addEventListener('touchstart', onTouchStart, true);
|
||||
touchCoordinates = [];
|
||||
}
|
||||
|
||||
lastPreventedTime = Date.now();
|
||||
|
||||
checkAllowableRegions(touchCoordinates, x, y);
|
||||
}
|
||||
|
||||
// Actual linking function.
|
||||
return function(scope, element, attr) {
|
||||
var clickHandler = $parse(attr.ngClick),
|
||||
tapping = false,
|
||||
tapElement, // Used to blur the element after a tap.
|
||||
startTime, // Used to check if the tap was held too long.
|
||||
touchStartX,
|
||||
touchStartY;
|
||||
|
||||
function resetState() {
|
||||
tapping = false;
|
||||
element.removeClass(ACTIVE_CLASS_NAME);
|
||||
}
|
||||
|
||||
element.on('touchstart', function(event) {
|
||||
tapping = true;
|
||||
tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement.
|
||||
// Hack for Safari, which can target text nodes instead of containers.
|
||||
if (tapElement.nodeType === 3) {
|
||||
tapElement = tapElement.parentNode;
|
||||
}
|
||||
|
||||
element.addClass(ACTIVE_CLASS_NAME);
|
||||
|
||||
startTime = Date.now();
|
||||
|
||||
// Use jQuery originalEvent
|
||||
var originalEvent = event.originalEvent || event;
|
||||
var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent];
|
||||
var e = touches[0];
|
||||
touchStartX = e.clientX;
|
||||
touchStartY = e.clientY;
|
||||
});
|
||||
|
||||
element.on('touchcancel', function(event) {
|
||||
resetState();
|
||||
});
|
||||
|
||||
element.on('touchend', function(event) {
|
||||
var diff = Date.now() - startTime;
|
||||
|
||||
// Use jQuery originalEvent
|
||||
var originalEvent = event.originalEvent || event;
|
||||
var touches = (originalEvent.changedTouches && originalEvent.changedTouches.length) ?
|
||||
originalEvent.changedTouches :
|
||||
((originalEvent.touches && originalEvent.touches.length) ? originalEvent.touches : [originalEvent]);
|
||||
var e = touches[0];
|
||||
var x = e.clientX;
|
||||
var y = e.clientY;
|
||||
var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2));
|
||||
|
||||
if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) {
|
||||
// Call preventGhostClick so the clickbuster will catch the corresponding click.
|
||||
preventGhostClick(x, y);
|
||||
|
||||
// Blur the focused element (the button, probably) before firing the callback.
|
||||
// This doesn't work perfectly on Android Chrome, but seems to work elsewhere.
|
||||
// I couldn't get anything to work reliably on Android Chrome.
|
||||
if (tapElement) {
|
||||
tapElement.blur();
|
||||
}
|
||||
|
||||
if (!angular.isDefined(attr.disabled) || attr.disabled === false) {
|
||||
element.triggerHandler('click', [event]);
|
||||
}
|
||||
}
|
||||
|
||||
resetState();
|
||||
});
|
||||
|
||||
// Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
|
||||
// something else nearby.
|
||||
element.onclick = function(event) { };
|
||||
|
||||
// Actual click handler.
|
||||
// There are three different kinds of clicks, only two of which reach this point.
|
||||
// - On desktop browsers without touch events, their clicks will always come here.
|
||||
// - On mobile browsers, the simulated "fast" click will call this.
|
||||
// - But the browser's follow-up slow click will be "busted" before it reaches this handler.
|
||||
// Therefore it's safe to use this directive on both mobile and desktop.
|
||||
element.on('click', function(event, touchend) {
|
||||
scope.$apply(function() {
|
||||
clickHandler(scope, {$event: (touchend || event)});
|
||||
});
|
||||
});
|
||||
|
||||
element.on('mousedown', function(event) {
|
||||
element.addClass(ACTIVE_CLASS_NAME);
|
||||
});
|
||||
|
||||
element.on('mousemove mouseup', function(event) {
|
||||
element.removeClass(ACTIVE_CLASS_NAME);
|
||||
});
|
||||
|
||||
};
|
||||
}];
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
* @ngdoc directive
|
||||
* @name ngSwipeLeft
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.7.0"
|
||||
*
|
||||
* See the {@link ngTouch module} documentation for more information.
|
||||
*
|
||||
* @description
|
||||
* Specify custom behavior when an element is swiped to the left on a touchscreen device.
|
||||
* A leftward swipe is a quick, right-to-left slide of the finger.
|
||||
@@ -42,6 +47,11 @@
|
||||
* @ngdoc directive
|
||||
* @name ngSwipeRight
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.7.0"
|
||||
*
|
||||
* See the {@link ngTouch module} documentation for more information.
|
||||
*
|
||||
* @description
|
||||
* Specify custom behavior when an element is swiped to the right on a touchscreen device.
|
||||
* A rightward swipe is a quick, left-to-right slide of the finger.
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
* @ngdoc service
|
||||
* @name $swipe
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.7.0"
|
||||
*
|
||||
* See the {@link ngTouch module} documentation for more information.
|
||||
*
|
||||
* @description
|
||||
* The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe
|
||||
* behavior, to make implementing swipe-related directives more convenient.
|
||||
|
||||
+9
-104
@@ -1,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
/* global ngTouchClickDirectiveFactory: false */
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngTouch
|
||||
@@ -13,114 +11,21 @@
|
||||
*
|
||||
* See {@link ngTouch.$swipe `$swipe`} for usage.
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.7.0"
|
||||
* The ngTouch module with the {@link ngTouch.$swipe `$swipe`} service and
|
||||
* the {@link ngTouch.ngSwipeLeft} and {@link ngTouch.ngSwipeRight} directives are
|
||||
* deprecated. Instead, stand-alone libraries for touch handling and gesture interaction
|
||||
* should be used, for example [HammerJS](https://hammerjs.github.io/) (which is also used by
|
||||
* Angular).
|
||||
*/
|
||||
|
||||
// define ngTouch module
|
||||
/* global -ngTouch */
|
||||
/* global ngTouch */
|
||||
var ngTouch = angular.module('ngTouch', []);
|
||||
|
||||
ngTouch.info({ angularVersion: '"NG_VERSION_FULL"' });
|
||||
|
||||
ngTouch.provider('$touch', $TouchProvider);
|
||||
|
||||
function nodeName_(element) {
|
||||
return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
* @name $touchProvider
|
||||
*
|
||||
* @description
|
||||
* The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}.
|
||||
*/
|
||||
$TouchProvider.$inject = ['$provide', '$compileProvider'];
|
||||
function $TouchProvider($provide, $compileProvider) {
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $touchProvider#ngClickOverrideEnabled
|
||||
*
|
||||
* @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the
|
||||
* current ngClickOverrideEnabled state
|
||||
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
||||
*
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled,
|
||||
* the default ngClick directive will be replaced by a version that eliminates the 300ms delay for
|
||||
* click events on browser for touch-devices.
|
||||
*
|
||||
* The default is `false`.
|
||||
*
|
||||
*/
|
||||
var ngClickOverrideEnabled = false;
|
||||
var ngClickDirectiveAdded = false;
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
this.ngClickOverrideEnabled = function(enabled) {
|
||||
if (angular.isDefined(enabled)) {
|
||||
|
||||
if (enabled && !ngClickDirectiveAdded) {
|
||||
ngClickDirectiveAdded = true;
|
||||
|
||||
// Use this to identify the correct directive in the delegate
|
||||
ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch';
|
||||
$compileProvider.directive('ngClick', ngTouchClickDirectiveFactory);
|
||||
|
||||
$provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
|
||||
if (ngClickOverrideEnabled) {
|
||||
// drop the default ngClick directive
|
||||
$delegate.shift();
|
||||
} else {
|
||||
// drop the ngTouch ngClick directive if the override has been re-disabled (because
|
||||
// we cannot de-register added directives)
|
||||
var i = $delegate.length - 1;
|
||||
while (i >= 0) {
|
||||
if ($delegate[i].$$moduleName === 'ngTouch') {
|
||||
$delegate.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return $delegate;
|
||||
}]);
|
||||
}
|
||||
|
||||
ngClickOverrideEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
return ngClickOverrideEnabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $touch
|
||||
* @kind object
|
||||
*
|
||||
* @description
|
||||
* Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method.
|
||||
*
|
||||
*/
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
this.$get = function() {
|
||||
return {
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $touch#ngClickOverrideEnabled
|
||||
*
|
||||
* @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider},
|
||||
* i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled.
|
||||
*
|
||||
* @kind function
|
||||
*/
|
||||
ngClickOverrideEnabled: function() {
|
||||
return ngClickOverrideEnabled;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return angular.$$lowercase(element.nodeName || (element[0] && element[0].nodeName));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user