Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7717de4c51 | |||
| e68697e2e3 | |||
| a02ed88279 | |||
| a5914c94a8 | |||
| 63c9c9e8d7 | |||
| 4adbf82a84 | |||
| 1144b1eccb | |||
| 8970087e58 | |||
| 131e62a819 | |||
| 2fad638237 | |||
| a0940895a2 | |||
| 0a1db2ad5f | |||
| 4bd4246906 | |||
| 05ac702bc7 | |||
| 6882113bc1 | |||
| 535ee32a0b | |||
| 7cf4a2933c | |||
| 7dd6c87eec | |||
| 17f963c5d8 | |||
| ba09ba5344 | |||
| 8c36a43e91 | |||
| af14d67b84 | |||
| c8acff1cdc | |||
| 876e9842a2 | |||
| 5cb9465093 | |||
| 58f9413ad3 | |||
| 6f7674a7d0 | |||
| 8dc153db75 | |||
| 4a6f0996f6 | |||
| 522d581fc9 | |||
| 17b139f107 | |||
| 10973c3366 | |||
| fc64e68076 | |||
| ac5e92de9b | |||
| 0936353e9a | |||
| ed22d2fe7b | |||
| a5cfa88630 | |||
| bbf74f9994 | |||
| 62ad450d60 | |||
| faa4b17c86 | |||
| 4d980a8771 | |||
| 369469b4f3 | |||
| be417f2854 | |||
| 3a517c25f6 | |||
| 29b8dcf387 | |||
| c9d1e690aa | |||
| a47247b5e0 | |||
| b682213d72 | |||
| 223cf2b5bb | |||
| c387e0d79d | |||
| cbf74d5d64 | |||
| a812327acd | |||
| 35fce310e9 | |||
| 93a754a490 | |||
| 7d9d387195 | |||
| feac52d840 | |||
| f4f571efdf | |||
| a8c263c194 | |||
| 3d6c45d76e | |||
| bf841d3512 | |||
| a1d88457de | |||
| b011ae9544 | |||
| 63fdee6b9c | |||
| 257ebbb514 | |||
| 2b6c986736 | |||
| 2da4950406 | |||
| 789db83a8a | |||
| ce443792c4 | |||
| dd47867bfb | |||
| f2ebb82ba5 | |||
| 12698755be | |||
| f7d7954904 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
# http://editorconfig.org
|
||||
# https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
+196
-2
@@ -1,3 +1,168 @@
|
||||
<a name="1.7.3"></a>
|
||||
# 1.7.3 eventful-proposal (2018-08-03)
|
||||
|
||||
## Bug Fixes
|
||||
- **$location:**
|
||||
- fix infinite recursion/digest on URLs with special characters
|
||||
([e68697](https://github.com/angular/angular.js/commit/e68697e2e30695f509e6c2c1e43c2c02b7af41f0),
|
||||
[#16592](https://github.com/angular/angular.js/issues/16592),
|
||||
[#16611](https://github.com/angular/angular.js/issues/16611))
|
||||
- avoid unnecessary `$locationChange*` events due to empty hash
|
||||
([1144b1](https://github.com/angular/angular.js/commit/1144b1eccb886ea0e4a80bcb07d38a305c3263b4),
|
||||
[#16632](https://github.com/angular/angular.js/issues/16632),
|
||||
[#16636](https://github.com/angular/angular.js/issues/16636))
|
||||
- **ngMock.$httpBackend:**
|
||||
- pass failed HTTP expectations to `$exceptionHandler`
|
||||
([4adbf8](https://github.com/angular/angular.js/commit/4adbf82a84a564a8d3f0982c17a64c6163200bcd),
|
||||
[#16644](https://github.com/angular/angular.js/issues/16644))
|
||||
- correctly ignore query params in {expect,when}Route
|
||||
([be417f](https://github.com/angular/angular.js/commit/be417f28549e184fbc3c7f74251ac21fca965ae8),
|
||||
[#14173](https://github.com/angular/angular.js/issues/14173),
|
||||
[#16589](https://github.com/angular/angular.js/issues/16589))
|
||||
- **Angular:** add workaround for Safari / Webdriver problem
|
||||
([0a1db2](https://github.com/angular/angular.js/commit/0a1db2ad5f8da6902b1711a738ae4177ce9685fa),
|
||||
[#16645](https://github.com/angular/angular.js/issues/16645))
|
||||
- **$animate:** avoid memory leak with `$animate.enabled(element, enabled)`
|
||||
([4bd424](https://github.com/angular/angular.js/commit/4bd424690612885ca06028e9b27de585edc3d3c3),
|
||||
[#16649](https://github.com/angular/angular.js/issues/16649))
|
||||
- **$compile:**
|
||||
- use correct parent element when requiring on html element
|
||||
([05ac70](https://github.com/angular/angular.js/commit/05ac702bc7edae5f89c363ea661774910735ea8b),
|
||||
[#16535](https://github.com/angular/angular.js/issues/16535),
|
||||
[#16647](https://github.com/angular/angular.js/issues/16647))
|
||||
- work around Firefox `DocumentFragment` bug
|
||||
([10973c](https://github.com/angular/angular.js/commit/10973c3366676ac8e5b2728b1e006cdef4ea197e),
|
||||
[#16607](https://github.com/angular/angular.js/issues/16607),
|
||||
[#16615](https://github.com/angular/angular.js/issues/16615))
|
||||
- **ngEventDirs:**
|
||||
- pass error in handler to $exceptionHandler when event was triggered in a digest
|
||||
([688211](https://github.com/angular/angular.js/commit/6882113bc194fb10081db9bab3dd7d69dd59f311))
|
||||
- don't wrap the event handler in $apply if already in $digest
|
||||
([535ee3](https://github.com/angular/angular.js/commit/535ee32a0b4881c9fd526fb5e0ffc10919ba1800),
|
||||
[#14673](https://github.com/angular/angular.js/issues/14673),
|
||||
[#14674](https://github.com/angular/angular.js/issues/14674))
|
||||
- **angular.element:** do not break on `cleanData()` if `_data()` returns undefined
|
||||
([7cf4a2](https://github.com/angular/angular.js/commit/7cf4a2933cb017e45b0c97b0a836cbbd905ee31a),
|
||||
[#16641](https://github.com/angular/angular.js/issues/16641),
|
||||
[#16642](https://github.com/angular/angular.js/issues/16642))
|
||||
- **ngAria:** do not scroll when pressing spacebar on custom buttons
|
||||
([3a517c](https://github.com/angular/angular.js/commit/3a517c25f677294a7a9eca1660654a3edcc9e103),
|
||||
[#14665](https://github.com/angular/angular.js/issues/14665),
|
||||
[#16604](https://github.com/angular/angular.js/issues/16604))
|
||||
|
||||
|
||||
## New Features
|
||||
- **$compile:** add support for arbitrary DOM property and event bindings
|
||||
([a5914c](https://github.com/angular/angular.js/commit/a5914c94a8fa5b1eceeab9e4e6849cbf467bc26d),
|
||||
[#16428](https://github.com/angular/angular.js/issues/16428),
|
||||
[#16235](https://github.com/angular/angular.js/issues/16235),
|
||||
[#16614](https://github.com/angular/angular.js/issues/16614))
|
||||
- **ngMock:** add `$flushPendingTasks()` and `$verifyNoPendingTasks()`
|
||||
([6f7674](https://github.com/angular/angular.js/commit/6f7674a7d063d434205f75f5b861f167e8125999),
|
||||
[#14336](https://github.com/angular/angular.js/issues/14336))
|
||||
- **core:** implement more granular pending task tracking
|
||||
([17b139](https://github.com/angular/angular.js/commit/17b139f107e5471a9351af638093a8e13a69e42a))
|
||||
- **$animate:** add option data to event callbacks
|
||||
([fc64e6](https://github.com/angular/angular.js/commit/fc64e6807642512b567deb52b497bd2bff570a1f),
|
||||
[#12697](https://github.com/angular/angular.js/issues/12697),
|
||||
[#13059](https://github.com/angular/angular.js/issues/13059))
|
||||
- **form.FormController:** add $getControls()
|
||||
([c9d1e6](https://github.com/angular/angular.js/commit/c9d1e690aa597283373b78e646676fa8f1ba1b4d),
|
||||
[#16601](https://github.com/angular/angular.js/issues/16601),
|
||||
[#14749](https://github.com/angular/angular.js/issues/14749),
|
||||
[#14517](https://github.com/angular/angular.js/issues/14517),
|
||||
[#13202](https://github.com/angular/angular.js/issues/13202))
|
||||
- **ngModelOptions:** add `timeStripZeroSeconds` and `timeSecondsFormat`
|
||||
([b68221](https://github.com/angular/angular.js/commit/b682213d72d65c996a6a31ea57b79d4c4f4e3c98),
|
||||
[#10721](https://github.com/angular/angular.js/issues/10721),
|
||||
[#16510](https://github.com/angular/angular.js/issues/16510),
|
||||
[#16584](https://github.com/angular/angular.js/issues/16584))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
- **ngAnimate:** avoid repeated calls to addClass/removeClass when animation has no duration
|
||||
([093635](https://github.com/angular/angular.js/commit/0936353e9a03f072bc3c4056888fd154a96530ef),
|
||||
[#14165](https://github.com/angular/angular.js/issues/14165),
|
||||
[#14166](https://github.com/angular/angular.js/issues/14166),
|
||||
[#16613](https://github.com/angular/angular.js/issues/16613))
|
||||
|
||||
|
||||
<a name="1.7.2"></a>
|
||||
# 1.7.2 extreme-compatiplication (2018-06-12)
|
||||
|
||||
In the previous release, we removed a private, undocumented API that was no longer used by
|
||||
AngularJS. It turned out that several popular UI libraries (such as
|
||||
[AngularJS Material](https://material.angularjs.org/),
|
||||
[UI Bootstrap](https://angular-ui.github.io/bootstrap/),
|
||||
[ngDialog](http://likeastore.github.io/ngDialog/) and probably others) relied on that API.
|
||||
|
||||
In order to avoid unnecessary pain for developers, this release reverts the removal of the private
|
||||
API and restores compatibility of the aforementioned libraries with the latest AngularJS.
|
||||
|
||||
## Reverts
|
||||
- **$compile:** remove `preAssignBindingsEnabled` leftovers
|
||||
([2da495](https://github.com/angular/angular.js/commit/2da49504065e9e2b71a7a5622e45118d8abbe87e),
|
||||
[#16580](https://github.com/angular/angular.js/pull/16580),
|
||||
[a81232](https://github.com/angular/angular.js/commit/a812327acda8bc890a4c4e809f0debb761c29625),
|
||||
[#16595](https://github.com/angular/angular.js/pull/16595))
|
||||
|
||||
|
||||
<a name="1.7.1"></a>
|
||||
# 1.7.1 momentum-defiance (2018-06-08)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
- **$compile:** support transcluding multi-element directives
|
||||
([789db8](https://github.com/angular/angular.js/commit/789db83a8ae0e2db5db13289b2c29e56093d967a),
|
||||
[#15554](https://github.com/angular/angular.js/issues/15554),
|
||||
[#15555](https://github.com/angular/angular.js/issues/15555))
|
||||
- **ngModel:** do not throw if view value changes on destroyed scope
|
||||
([2b6c98](https://github.com/angular/angular.js/commit/2b6c9867369fd3ef1ddb687af1153478ab62ee1b),
|
||||
[#16583](https://github.com/angular/angular.js/issues/16583),
|
||||
[#16585](https://github.com/angular/angular.js/issues/16585))
|
||||
|
||||
|
||||
## New Features
|
||||
- **$compile:** add one-way collection bindings
|
||||
([f9d1ca](https://github.com/angular/angular.js/commit/f9d1ca20c38f065f15769fbe23aee5314cb58bd4),
|
||||
[#14039](https://github.com/angular/angular.js/issues/14039),
|
||||
[#16553](https://github.com/angular/angular.js/issues/16553),
|
||||
[#15874](https://github.com/angular/angular.js/issues/15874))
|
||||
- **ngRef:** add directive to publish controller, or element into scope
|
||||
([bf841d](https://github.com/angular/angular.js/commit/bf841d35120bf3c4655fde46af4105c85a0f1cdc),
|
||||
[#16511](https://github.com/angular/angular.js/issues/16511))
|
||||
- **errorHandlingConfig:** add option to exclude error params from url
|
||||
([3d6c45](https://github.com/angular/angular.js/commit/3d6c45d76e30b1b3c4eb9672cf4a93e5251c06b3),
|
||||
[#14744](https://github.com/angular/angular.js/issues/14744),
|
||||
[#15707](https://github.com/angular/angular.js/issues/15707),
|
||||
[#16283](https://github.com/angular/angular.js/issues/16283),
|
||||
[#16299](https://github.com/angular/angular.js/issues/16299),
|
||||
[#16591](https://github.com/angular/angular.js/issues/16591))
|
||||
- **ngAria:** add support for ignoring a specific element
|
||||
([7d9d38](https://github.com/angular/angular.js/commit/7d9d387195292cb5e04984602b752d31853cfea6),
|
||||
[#14602](https://github.com/angular/angular.js/issues/14602),
|
||||
[#14672](https://github.com/angular/angular.js/issues/14672),
|
||||
[#14833](https://github.com/angular/angular.js/issues/14833))
|
||||
- **ngCookies:** support samesite option
|
||||
([10a229](https://github.com/angular/angular.js/commit/10a229ce1befdeaf6295d1635dc11391c252a91a),
|
||||
[#16543](https://github.com/angular/angular.js/issues/16543),
|
||||
[#16544](https://github.com/angular/angular.js/issues/16544))
|
||||
- **ngMessages:** add support for default message
|
||||
([a8c263](https://github.com/angular/angular.js/commit/a8c263c1947cc85ee60b4732f7e4bcdc7ba463e8),
|
||||
[#12008](https://github.com/angular/angular.js/issues/12008),
|
||||
[#12213](https://github.com/angular/angular.js/issues/12213),
|
||||
[#16587](https://github.com/angular/angular.js/issues/16587))
|
||||
- **ngMock, ngMockE2E:** add option to match latest definition for `$httpBackend` request
|
||||
([773f39](https://github.com/angular/angular.js/commit/773f39c9345479f5f8b6321236ce6ad96f77aa92),
|
||||
[#16251](https://github.com/angular/angular.js/issues/16251),
|
||||
[#11637](https://github.com/angular/angular.js/issues/11637),
|
||||
[#16560](https://github.com/angular/angular.js/issues/16560))
|
||||
- **$route:** add support for the `reloadOnUrl` configuration option
|
||||
([f4f571](https://github.com/angular/angular.js/commit/f4f571efdf86d6acbcd5c6b1de66b4b33a259125),
|
||||
[#7925](https://github.com/angular/angular.js/issues/7925),
|
||||
[#15002](https://github.com/angular/angular.js/issues/15002))
|
||||
|
||||
|
||||
<a name="1.7.0"></a>
|
||||
# 1.7.0 nonexistent-physiology (2018-05-11)
|
||||
|
||||
@@ -372,8 +537,8 @@ 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.
|
||||
|
||||
### **$sce** due to:
|
||||
- **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**: handle URL sanitization through the `$sce` service
|
||||
### **$sce** :
|
||||
- due to **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**: handle URL sanitization through the `$sce` service
|
||||
|
||||
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
|
||||
@@ -387,6 +552,35 @@ Note that values that have been passed through the `$interpolate` service within
|
||||
`URL` or `MEDIA_URL` will have already been sanitized, so you would not need to sanitize
|
||||
these values again.
|
||||
|
||||
- due to **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**: handle URL sanitization through the `$sce` service
|
||||
|
||||
binding `trustAs()` and the short versions (`trustAsResourceUrl()` et al.) to
|
||||
`ngSrc`, `ngSrcset`, and `ngHref` will now raise an infinite digest error:
|
||||
|
||||
```js
|
||||
$scope.imgThumbFn = function(id) {
|
||||
return $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<img ng-src="{{imgThumbFn(imgId)}}">
|
||||
```
|
||||
This is because the `$interpolate` service is now responsible for sanitizing
|
||||
the attribute value, and its watcher receives a new object from `trustAs()`
|
||||
on every digest.
|
||||
To migrate, compute the trusted value only when the input value changes:
|
||||
|
||||
```js
|
||||
$scope.$watch('imgId', function(id) {
|
||||
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
});
|
||||
```
|
||||
|
||||
```html
|
||||
<img ng-src="{{imgThumb}}">
|
||||
```
|
||||
|
||||
### **orderBy** due to:
|
||||
- **[1d8046](https://github.com/angular/angular.js/commit/1d804645f7656d592c90216a0355b4948807f6b8)**: consider `null` and `undefined` greater than other values
|
||||
|
||||
|
||||
Vendored
+6
@@ -28,6 +28,7 @@ var angularFiles = {
|
||||
'src/ng/httpBackend.js',
|
||||
'src/ng/interpolate.js',
|
||||
'src/ng/interval.js',
|
||||
'src/ng/intervalFactory.js',
|
||||
'src/ng/jsonpCallbacks.js',
|
||||
'src/ng/locale.js',
|
||||
'src/ng/location.js',
|
||||
@@ -40,6 +41,7 @@ var angularFiles = {
|
||||
'src/ng/sanitizeUri.js',
|
||||
'src/ng/sce.js',
|
||||
'src/ng/sniffer.js',
|
||||
'src/ng/taskTrackerFactory.js',
|
||||
'src/ng/templateRequest.js',
|
||||
'src/ng/testability.js',
|
||||
'src/ng/timeout.js',
|
||||
@@ -74,6 +76,7 @@ var angularFiles = {
|
||||
'src/ng/directive/ngNonBindable.js',
|
||||
'src/ng/directive/ngOptions.js',
|
||||
'src/ng/directive/ngPluralize.js',
|
||||
'src/ng/directive/ngRef.js',
|
||||
'src/ng/directive/ngRepeat.js',
|
||||
'src/ng/directive/ngShowHide.js',
|
||||
'src/ng/directive/ngStyle.js',
|
||||
@@ -103,6 +106,7 @@ var angularFiles = {
|
||||
'src/ngAnimate/animateJs.js',
|
||||
'src/ngAnimate/animateJsDriver.js',
|
||||
'src/ngAnimate/animateQueue.js',
|
||||
'src/ngAnimate/animateCache.js',
|
||||
'src/ngAnimate/animation.js',
|
||||
'src/ngAnimate/ngAnimateSwap.js',
|
||||
'src/ngAnimate/module.js'
|
||||
@@ -130,6 +134,7 @@ var angularFiles = {
|
||||
],
|
||||
'ngRoute': [
|
||||
'src/shallowCopy.js',
|
||||
'src/routeToRegExp.js',
|
||||
'src/ngRoute/route.js',
|
||||
'src/ngRoute/routeParams.js',
|
||||
'src/ngRoute/directive/ngView.js'
|
||||
@@ -139,6 +144,7 @@ var angularFiles = {
|
||||
'src/ngSanitize/filter/linky.js'
|
||||
],
|
||||
'ngMock': [
|
||||
'src/routeToRegExp.js',
|
||||
'src/ngMock/angular-mocks.js',
|
||||
'src/ngMock/browserTrigger.js'
|
||||
],
|
||||
|
||||
@@ -91,13 +91,16 @@ directivesModule
|
||||
.component('tocTree', {
|
||||
template: '<ul>' +
|
||||
'<li ng-repeat="item in $ctrl.items">' +
|
||||
'<a ng-href="#{{item.fragment}}">{{item.title}}</a>' +
|
||||
'<a ng-href="{{ $ctrl.path }}#{{item.fragment}}">{{item.title}}</a>' +
|
||||
'<toc-tree ng-if="::item.children.length > 0" items="item.children"></toc-tree>' +
|
||||
'</li>' +
|
||||
'</ul>',
|
||||
bindings: {
|
||||
items: '<'
|
||||
}
|
||||
},
|
||||
controller: ['$location', /** @this */ function($location) {
|
||||
this.path = $location.path().replace(/^\/?(.+?)(\/index)?\/?$/, '$1');
|
||||
}]
|
||||
})
|
||||
.directive('tocContainer', function() {
|
||||
return {
|
||||
|
||||
@@ -148,6 +148,7 @@ module.exports = new Package('angularjs', [
|
||||
|
||||
.config(function(checkAnchorLinksProcessor) {
|
||||
checkAnchorLinksProcessor.base = '/';
|
||||
checkAnchorLinksProcessor.errorOnUnmatchedLinks = true;
|
||||
// We are only interested in docs that have an area (i.e. they are pages)
|
||||
checkAnchorLinksProcessor.checkDoc = function(doc) { return doc.area; };
|
||||
})
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@ngdoc error
|
||||
@name $compile:ctxoverride
|
||||
@fullName DOM Property Security Context Override
|
||||
@description
|
||||
|
||||
This error occurs when the security context for a property is defined via {@link ng.$compileProvider#addPropertySecurityContext addPropertySecurityContext()} multiple times under different security contexts.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.MEDIA_URL);
|
||||
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.RESOURCE_URL); //throws
|
||||
```
|
||||
@@ -1,12 +1,12 @@
|
||||
@ngdoc error
|
||||
@name $compile:nodomevents
|
||||
@fullName Interpolated Event Attributes
|
||||
@fullName Event Attribute/Property Binding
|
||||
@description
|
||||
|
||||
This error occurs when one tries to create a binding for event handler attributes like `onclick`, `onload`, `onsubmit`, etc.
|
||||
This error occurs when one tries to create a binding for event handler attributes or properties like `onclick`, `onload`, `onsubmit`, etc.
|
||||
|
||||
There is no practical value in binding to these attributes and doing so only exposes your application to security vulnerabilities like XSS.
|
||||
For these reasons binding to event handler attributes (all attributes that start with `on` and `formaction` attribute) is not supported.
|
||||
There is no practical value in binding to these attributes/properties and doing so only exposes your application to security vulnerabilities like XSS.
|
||||
For these reasons binding to event handler attributes and properties (`formaction` and all starting with `on`) is not supported.
|
||||
|
||||
|
||||
An example code that would allow XSS vulnerability by evaluating user input in the window context could look like this:
|
||||
@@ -17,4 +17,4 @@ An example code that would allow XSS vulnerability by evaluating user input in t
|
||||
|
||||
Since the `onclick` evaluates the value as JavaScript code in the window context, setting the `username` model to a value like `javascript:alert('PWND')` would result in script injection when the `div` is clicked.
|
||||
|
||||
|
||||
Please use the `ng-*` or `ng-on-*` versions instead (such as `ng-click` or `ng-on-click` rather than `onclick`).
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@ngdoc error
|
||||
@name ngRef:noctrl
|
||||
@fullName A controller for the value of `ngRefRead` could not be found on the element.
|
||||
@description
|
||||
|
||||
This error occurs when the {@link ng.ngRef ngRef directive} specifies
|
||||
a value in `ngRefRead` that cannot be resolved to a directive / component controller.
|
||||
|
||||
Causes for this error can be:
|
||||
|
||||
1. Your `ngRefRead` value has a typo.
|
||||
2. You have a typo in the *registered* directive / component name.
|
||||
3. The directive / component does not have a controller.
|
||||
|
||||
Note that `ngRefRead` takes the name of the component / directive, not the name of controller, and
|
||||
also not the combination of directive and 'Controller'. For example, for a directive called 'myDirective',
|
||||
the correct declaration is `<div ng-ref="$ctrl.ref" ng-ref-read="myDirective">`.
|
||||
@@ -0,0 +1,27 @@
|
||||
@ngdoc error
|
||||
@name ngRef:nonassign
|
||||
@fullName Non-Assignable Expression
|
||||
@description
|
||||
|
||||
This error occurs when ngRef defines an expression that is not-assignable.
|
||||
|
||||
In order for ngRef to work, it must be possible to write the reference into the path defined with the expression.
|
||||
|
||||
For example, the following expressions are non-assignable:
|
||||
|
||||
```
|
||||
<my-directive ng-ref="{}"></my-directive>
|
||||
|
||||
<my-directive ng-ref="myFn()"></my-directive>
|
||||
|
||||
<!-- missing attribute value is also invalid -->
|
||||
<my-directive ng-ref></my-directive>
|
||||
|
||||
```
|
||||
|
||||
To resolve this error, use a path expression that is assignable:
|
||||
|
||||
```
|
||||
<my-directive ng-ref="$ctrl.reference"></my-directive>
|
||||
|
||||
```
|
||||
@@ -3,7 +3,7 @@
|
||||
@sortOrder 500
|
||||
@description
|
||||
|
||||
# What does it do?
|
||||
# Using the `$location` service
|
||||
|
||||
The `$location` service parses the URL in the browser address bar (based on [`window.location`](https://developer.mozilla.org/en/window.location)) and makes the URL available to
|
||||
your application. Changes to the URL in the address bar are reflected into the `$location` service and
|
||||
@@ -76,7 +76,7 @@ the current URL in the browser.
|
||||
It does not cause a full page reload when the browser URL is changed. To reload the page after
|
||||
changing the URL, use the lower-level API, `$window.location.href`.
|
||||
|
||||
# General overview of the API
|
||||
## General overview of the API
|
||||
|
||||
The `$location` service can behave differently, depending on the configuration that was provided to
|
||||
it when it was instantiated. The default configuration is suitable for many applications, for
|
||||
@@ -85,7 +85,7 @@ others customizing the configuration can enable new features.
|
||||
Once the `$location` service is instantiated, you can interact with it via jQuery-style getter and
|
||||
setter methods that allow you to get or change the current URL in the browser.
|
||||
|
||||
## `$location` service configuration
|
||||
### `$location` service configuration
|
||||
|
||||
To configure the `$location` service, retrieve the
|
||||
{@link ng.$locationProvider $locationProvider} and set the parameters as follows:
|
||||
@@ -113,12 +113,12 @@ To configure the `$location` service, retrieve the
|
||||
Prefix used for Hashbang URLs (used in Hashbang mode or in legacy browsers in HTML5 mode).<br />
|
||||
Default: `'!'`
|
||||
|
||||
### Example configuration
|
||||
#### Example configuration
|
||||
```js
|
||||
$locationProvider.html5Mode(true).hashPrefix('*');
|
||||
```
|
||||
|
||||
## Getter and setter methods
|
||||
### Getter and setter methods
|
||||
|
||||
`$location` service provides getter methods for read-only parts of the URL (absUrl, protocol, host,
|
||||
port) and getter / setter methods for url, path, search, hash:
|
||||
@@ -137,7 +137,7 @@ change multiple segments in one go, chain setters like this:
|
||||
$location.path('/newValue').search({key: value});
|
||||
```
|
||||
|
||||
## Replace method
|
||||
### Replace method
|
||||
|
||||
There is a special `replace` method which can be used to tell the $location service that the next
|
||||
time the $location service is synced with the browser, the last history record should be replaced
|
||||
@@ -173,7 +173,7 @@ encoded.
|
||||
`/path?search=a&b=c#hash`. The segments are encoded as well.
|
||||
|
||||
|
||||
# Hashbang and HTML5 Modes
|
||||
## Hashbang and HTML5 Modes
|
||||
|
||||
`$location` service has two configuration modes which control the format of the URL in the browser
|
||||
address bar: **Hashbang mode** (the default) and the **HTML5 mode** which is based on using the
|
||||
@@ -221,7 +221,7 @@ facilitate the browser URL change and history management.
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Hashbang mode (default mode)
|
||||
### Hashbang mode (default mode)
|
||||
|
||||
In this mode, `$location` uses Hashbang URLs in all browsers.
|
||||
AngularJS also does not intercept and rewrite links in this mode. I.e. links work
|
||||
@@ -229,7 +229,7 @@ as expected and also perform full page reloads when other parts of the url
|
||||
than the hash fragment was changed.
|
||||
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
```js
|
||||
it('should show example', function() {
|
||||
@@ -255,7 +255,7 @@ it('should show example', function() {
|
||||
});
|
||||
```
|
||||
|
||||
## HTML5 mode
|
||||
### HTML5 mode
|
||||
|
||||
In HTML5 mode, the `$location` service getters and setters interact with the browser URL address
|
||||
through the HTML5 history API. This allows for use of regular URL path and search segments,
|
||||
@@ -271,7 +271,7 @@ Note that in this mode, AngularJS intercepts all links (subject to the "Html lin
|
||||
and updates the url in a way that never performs a full page reload.
|
||||
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
```js
|
||||
it('should show example', function() {
|
||||
@@ -320,14 +320,14 @@ it('should show example (when browser doesn\'t support HTML5 mode', function() {
|
||||
});
|
||||
```
|
||||
|
||||
### Fallback for legacy browsers
|
||||
#### Fallback for legacy browsers
|
||||
|
||||
For browsers that support the HTML5 history API, `$location` uses the HTML5 history API to write
|
||||
path and search. If the history API is not supported by a browser, `$location` supplies a Hashbang
|
||||
URL. This frees you from having to worry about whether the browser viewing your app supports the
|
||||
history API or not; the `$location` service makes this transparent to you.
|
||||
|
||||
### HTML link rewriting
|
||||
#### HTML link rewriting
|
||||
|
||||
When you use HTML5 history API mode, you will not need special hashbang links. All you have to do
|
||||
is specify regular URL links, such as: `<a href="/some?foo=bar">link</a>`
|
||||
@@ -361,7 +361,7 @@ Note that [attribute name normalization](guide/directive#normalization) does not
|
||||
`'internalLink'` will **not** match `'internal-link'`.
|
||||
|
||||
|
||||
### Relative links
|
||||
#### Relative links
|
||||
|
||||
Be sure to check all relative links, images, scripts etc. AngularJS requires you to specify the url
|
||||
base in the head of your main html file (`<base href="/my-base/index.html">`) unless `html5Mode.requireBase`
|
||||
@@ -374,14 +374,14 @@ will only change `$location.hash()` and not modify the url otherwise. This is us
|
||||
to anchors on the same page without needing to know on which page the user currently is.
|
||||
|
||||
|
||||
### Server side
|
||||
#### Server side
|
||||
|
||||
Using this mode requires URL rewriting on server side, basically you have to rewrite all your links
|
||||
to entry point of your application (e.g. index.html). Requiring a `<base>` tag is also important for
|
||||
this case, as it allows AngularJS to differentiate between the part of the url that is the application
|
||||
base and the path that should be handled by the application.
|
||||
|
||||
### Base href constraints
|
||||
#### Base href constraints
|
||||
|
||||
The `$location` service is not able to function properly if the current URL is outside the URL given
|
||||
as the base href. This can have subtle confusing consequences...
|
||||
@@ -403,7 +403,7 @@ legacy browsers and hashbang links in modern browser:
|
||||
- Modern browser will rewrite hashbang URLs to regular URLs.
|
||||
- Older browsers will redirect regular URLs to hashbang URLs.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
Here you can see two `$location` instances that show the difference between **Html5 mode** and **Html5 Fallback mode**.
|
||||
Note that to simulate different levels of browser support, the `$location` instances are connected to
|
||||
@@ -415,7 +415,7 @@ redirect to regular / hashbang url, as this conversion happens only during parsi
|
||||
|
||||
In these examples we use `<base href="/base/index.html" />`. The inputs represent the address bar of the browser.
|
||||
|
||||
#### Browser in HTML5 mode
|
||||
##### Browser in HTML5 mode
|
||||
<example module="html5-mode" name="location-html5-mode">
|
||||
<file name="index.html">
|
||||
<div ng-controller="LocationController">
|
||||
@@ -565,7 +565,7 @@ In these examples we use `<base href="/base/index.html" />`. The inputs represen
|
||||
|
||||
</example>
|
||||
|
||||
#### Browser in HTML5 Fallback mode (Hashbang mode)
|
||||
##### Browser in HTML5 Fallback mode (Hashbang mode)
|
||||
<example module="hashbang-mode" name="location-hashbang-mode">
|
||||
<file name="index.html">
|
||||
<div ng-controller="LocationController">
|
||||
@@ -718,15 +718,15 @@ In these examples we use `<base href="/base/index.html" />`. The inputs represen
|
||||
|
||||
</example>
|
||||
|
||||
# Caveats
|
||||
## Caveats
|
||||
|
||||
## Page reload navigation
|
||||
### Page reload navigation
|
||||
|
||||
The `$location` service allows you to change only the URL; it does not allow you to reload the
|
||||
page. When you need to change the URL and reload the page or navigate to a different page, please
|
||||
use a lower level API, {@link ng.$window $window.location.href}.
|
||||
|
||||
## Using $location outside of the scope life-cycle
|
||||
### Using $location outside of the scope life-cycle
|
||||
|
||||
`$location` knows about AngularJS's {@link ng.$rootScope.Scope scope} life-cycle. When a URL changes in
|
||||
the browser it updates the `$location` and calls `$apply` so that all
|
||||
@@ -738,7 +738,7 @@ propagate this change into browser and will notify all the {@link ng.$rootScope.
|
||||
When you want to change the `$location` from outside AngularJS (for example, through a DOM Event or
|
||||
during testing) - you must call `$apply` to propagate the changes.
|
||||
|
||||
## $location.path() and ! or / prefixes
|
||||
### $location.path() and ! or / prefixes
|
||||
|
||||
A path should always begin with forward slash (`/`); the `$location.path()` setter will add the
|
||||
forward slash if it is missing.
|
||||
@@ -746,22 +746,17 @@ forward slash if it is missing.
|
||||
Note that the `!` prefix in the hashbang mode is not part of `$location.path()`; it is actually
|
||||
`hashPrefix`.
|
||||
|
||||
## Crawling your app
|
||||
### Crawling your app
|
||||
|
||||
To allow indexing of your AJAX application, you have to add special meta tag in the head section of
|
||||
your document:
|
||||
Most modern search engines are able to crawl AJAX applications with dynamic content, provided all
|
||||
included resources are available to the crawler bots.
|
||||
|
||||
```html
|
||||
<meta name="fragment" content="!" />
|
||||
```
|
||||
There also exists a special
|
||||
[AJAX crawling scheme](http://code.google.com/web/ajaxcrawling/docs/specification.html) developed by
|
||||
Google that allows bots to crawl the static equivalent of a dynamically generated page,
|
||||
but this schema has been deprecated, and support for it may vary by search engine.
|
||||
|
||||
This will cause crawler bot to request links with `_escaped_fragment_` param so that your server
|
||||
can recognize the crawler and serve a HTML snapshots. For more information about this technique,
|
||||
see [Making AJAX Applications
|
||||
Crawlable](http://code.google.com/web/ajaxcrawling/docs/specification.html).
|
||||
|
||||
|
||||
# Testing with the $location service
|
||||
## Testing with the $location service
|
||||
|
||||
When using `$location` service during testing, you are outside of the angular's {@link
|
||||
ng.$rootScope.Scope scope} life-cycle. This means it's your responsibility to call `scope.$apply()`.
|
||||
@@ -784,85 +779,6 @@ describe('serviceUnderTest', function() {
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
# Migrating from earlier AngularJS releases
|
||||
|
||||
In earlier releases of AngularJS, `$location` used `hashPath` or `hashSearch` to process path and
|
||||
search methods. With this release, the `$location` service processes path and search methods and
|
||||
then uses the information it obtains to compose hashbang URLs (such as
|
||||
`http://server.com/#!/path?search=a`), when necessary.
|
||||
|
||||
## Changes to your code
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="head">
|
||||
<th>Navigation inside the app</th>
|
||||
<th>Change to</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>$location.href = value<br />$location.hash = value<br />$location.update(value)<br
|
||||
/>$location.updateHash(value)</td>
|
||||
<td>$location.path(path).search(search)</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location.hashPath = path</td>
|
||||
<td>$location.path(path)</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location.hashSearch = search</td>
|
||||
<td>$location.search(search)</td>
|
||||
</tr>
|
||||
|
||||
<tr class="head">
|
||||
<th>Navigation outside the app</td>
|
||||
<th>Use lower level API</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location.href = value<br />$location.update(value)</td>
|
||||
<td>$window.location.href = value</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location[protocol | host | port | path | search]</td>
|
||||
<td>$window.location[protocol | host | port | path | search]</td>
|
||||
</tr>
|
||||
|
||||
<tr class="head">
|
||||
<th>Read access</td>
|
||||
<th>Change to</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location.hashPath</td>
|
||||
<td>$location.path()</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location.hashSearch</td>
|
||||
<td>$location.search()</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location.href<br />$location.protocol<br />$location.host<br />$location.port<br
|
||||
/>$location.hash</td>
|
||||
<td>$location.absUrl()<br />$location.protocol()<br />$location.host()<br />$location.port()<br
|
||||
/>$location.path() + $location.search()</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>$location.path<br />$location.search</td>
|
||||
<td>$window.location.path<br />$window.location.search</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Two-way binding to $location
|
||||
|
||||
Because `$location` uses getters/setters, you can use `ng-model-options="{ getterSetter: true }"`
|
||||
@@ -884,6 +800,6 @@ angular.module('locationExample', [])
|
||||
</file>
|
||||
</example>
|
||||
|
||||
# Related API
|
||||
## Related API
|
||||
|
||||
* {@link ng.$location `$location` API}
|
||||
|
||||
@@ -222,23 +222,26 @@ triggered:
|
||||
|
||||
| Directive | Supported Animations |
|
||||
|-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
|
||||
| {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
|
||||
| {@link ng.directive:ngIf#animations ngIf} | enter and leave |
|
||||
| {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
|
||||
| {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
|
||||
| {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
|
||||
| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
|
||||
| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
|
||||
| {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave |
|
||||
| {@link ng.directive:ngClass#animations ngClass / {{class}​}} | add and remove |
|
||||
| {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
|
||||
| {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
|
||||
| {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
|
||||
| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
|
||||
| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
|
||||
| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
|
||||
| {@link ng.directive:ngIf#animations ngIf} | enter and leave |
|
||||
| {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
|
||||
| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
|
||||
| {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
|
||||
| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
|
||||
| {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
|
||||
| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
|
||||
| {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
|
||||
| {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
|
||||
|
||||
(More information can be found by visiting the documentation associated with each directive.)
|
||||
|
||||
For a full breakdown of the steps involved during each animation event, refer to the
|
||||
{@link ng.$animate API docs}.
|
||||
{@link ng.$animate `$animate` API docs}.
|
||||
|
||||
## How do I use animations in my own directives?
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ components should follow a few simple conventions:
|
||||
}
|
||||
```
|
||||
|
||||
- **Components have a well-defined lifecycle**
|
||||
- **Components have a well-defined lifecycle:**
|
||||
Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life
|
||||
of the component. The following hook methods can be implemented:
|
||||
|
||||
|
||||
@@ -505,6 +505,36 @@ Note that values that have been passed through the `$interpolate` service within
|
||||
`URL` or `MEDIA_URL` will have already been sanitized, so you would not need to sanitize
|
||||
these values again.
|
||||
|
||||
<hr/>
|
||||
|
||||
Due to **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
|
||||
binding {@link ng.$sce#trustAs trustAs()} and the short versions
|
||||
({@link ng.$sce#trustAsResourceUrl trustAsResourceUrl()} et al.) to
|
||||
{@link ng.ngSrc}, {@link ng.ngSrcset}, and {@link ng.ngHref} will now raise an infinite digest error:
|
||||
|
||||
```js
|
||||
$scope.imgThumbFn = function(id) {
|
||||
return $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<img ng-src="{{imgThumbFn(imgId)}}">
|
||||
```
|
||||
This is because {@link ng.$interpolate} is now responsible for sanitizing
|
||||
the attribute value, and its watcher receives a new object from `trustAs()`
|
||||
on every digest.
|
||||
To migrate, compute the trusted value only when the input value changes:
|
||||
|
||||
```js
|
||||
$scope.$watch('imgId', function(id) {
|
||||
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
});
|
||||
```
|
||||
|
||||
```html
|
||||
<img ng-src="{{imgThumb}}">
|
||||
```
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ng-filters"></a>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
@description
|
||||
|
||||
# Including AngularJS scripts from the Google CDN
|
||||
The quickest way to get started is to point your html `<script>` tag to a
|
||||
[Google CDN](https://developers.google.com/speed/libraries/#angularjs) URL.
|
||||
The quickest way to get started is to point your html `<script>` tag to a Google CDN URL.
|
||||
This way, you don't have to download anything or maintain a local copy.
|
||||
|
||||
There are two types of AngularJS script URLs you can point to, one for development and one for
|
||||
|
||||
@@ -30,13 +30,21 @@ Any version branch not shown in the following table (e.g. 1.5.x) is no longer be
|
||||
Any version branch not shown in the following table (e.g. 1.6.x) is no longer being developed.
|
||||
|
||||
<table class="dev-status table table-bordered">
|
||||
<thead>
|
||||
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="security"><td><span>1.2.x</span></td><td>Long Term Support</td><td>Last version to provide IE 8 support</td></tr>
|
||||
<tr class="stable"><td><span>1.7.x</span></td><td>Long Term Support</td><td>See [Long Term Support](#long-term-support) section below.</td></tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="security">
|
||||
<td><span>1.2.x</span></td>
|
||||
<td>Long Term Support</td>
|
||||
<td>Last version to provide IE 8 support</td>
|
||||
</tr>
|
||||
<tr class="stable">
|
||||
<td><span>1.7.x</span></td>
|
||||
<td>Long Term Support</td>
|
||||
<td>See {@link version-support-status#long-term-support Long Term Support} section below.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Long Term Support
|
||||
@@ -51,4 +59,4 @@ At this time we will focus exclusively on providing fixes to bugs that satisfy a
|
||||
|
||||
### Blog Post
|
||||
|
||||
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
|
||||
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
|
||||
|
||||
@@ -107,12 +107,14 @@ module.exports = function(config, specificOptions) {
|
||||
'SL_iOS_10': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'iphone',
|
||||
platform: 'OS X 10.12',
|
||||
version: '10.3'
|
||||
},
|
||||
'SL_iOS_11': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'iphone',
|
||||
version: '11'
|
||||
platform: 'OS X 10.12',
|
||||
version: '11.2'
|
||||
},
|
||||
|
||||
'BS_Chrome': {
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ module.exports = {
|
||||
//returns the 32-bit mode force flags for java compiler if supported, this makes the build much faster
|
||||
java32flags: function() {
|
||||
if (process.platform === 'win32') return '';
|
||||
if (shell.exec('java -version -d32 2>&1', {silent: true}).code !== 0) return '';
|
||||
if (shell.exec('java -d32 -version 2>&1', {silent: true}).code !== 0) return '';
|
||||
return ' -d32 -client';
|
||||
},
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ set -e
|
||||
# Curl and run this script as part of your .travis.yml before_script section:
|
||||
# before_script:
|
||||
# - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash
|
||||
SC_VERSION="4.4.1"
|
||||
SC_VERSION="4.4.12"
|
||||
CONNECT_URL="https://saucelabs.com/downloads/sc-$SC_VERSION-linux.tar.gz"
|
||||
CONNECT_DIR="/tmp/sauce-connect-$RANDOM"
|
||||
CONNECT_DOWNLOAD="sc-$SC_VERSION-linux.tar.gz"
|
||||
|
||||
+8
-8
@@ -32,8 +32,8 @@
|
||||
"commitplease": "^2.7.10",
|
||||
"cross-spawn": "^4.0.0",
|
||||
"cz-conventional-changelog": "1.1.4",
|
||||
"dgeni": "^0.4.0",
|
||||
"dgeni-packages": "^0.16.4",
|
||||
"dgeni": "^0.4.9",
|
||||
"dgeni-packages": "^0.26.2",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"event-stream": "~3.1.0",
|
||||
"glob": "^6.0.1",
|
||||
@@ -63,18 +63,18 @@
|
||||
"jquery": "3.2.1",
|
||||
"jquery-2.1": "npm:jquery@2.1.4",
|
||||
"jquery-2.2": "npm:jquery@2.2.4",
|
||||
"karma": "^2.0.0",
|
||||
"karma-browserstack-launcher": "^1.2.0",
|
||||
"karma-chrome-launcher": "^2.1.1",
|
||||
"karma": "^2.0.4",
|
||||
"karma-browserstack-launcher": "^1.3.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-edge-launcher": "^0.4.2",
|
||||
"karma-firefox-launcher": "^1.0.1",
|
||||
"karma-firefox-launcher": "^1.1.0",
|
||||
"karma-ie-launcher": "^1.0.0",
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-junit-reporter": "^1.2.0",
|
||||
"karma-safari-launcher": "^1.0.0",
|
||||
"karma-sauce-launcher": "^1.2.0",
|
||||
"karma-script-launcher": "^1.0.0",
|
||||
"karma-spec-reporter": "^0.0.31",
|
||||
"karma-spec-reporter": "^0.0.32",
|
||||
"load-grunt-tasks": "^3.5.0",
|
||||
"lodash": "~2.4.1",
|
||||
"log4js": "^0.6.27",
|
||||
|
||||
@@ -171,9 +171,15 @@
|
||||
/* ng/q.js */
|
||||
"markQExceptionHandled": false,
|
||||
|
||||
/* sce.js */
|
||||
"SCE_CONTEXTS": false,
|
||||
|
||||
/* ng/directive/directives.js */
|
||||
"ngDirective": false,
|
||||
|
||||
/* ng/directive/ngEventDirs.js */
|
||||
"createEventDirective": false,
|
||||
|
||||
/* ng/directive/input.js */
|
||||
"VALID_CLASS": false,
|
||||
"INVALID_CLASS": false,
|
||||
|
||||
+7
-2
@@ -1695,8 +1695,13 @@ function angularInit(element, bootstrap) {
|
||||
});
|
||||
if (appElement) {
|
||||
if (!isAutoBootstrapAllowed) {
|
||||
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
|
||||
try {
|
||||
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
|
||||
'an extension, document.location.href does not match.');
|
||||
} catch (e) {
|
||||
// Support: Safari 11 w/ Webdriver
|
||||
// The console.error will throw and make the test fail
|
||||
}
|
||||
return;
|
||||
}
|
||||
config.strictDi = getNgAttribute(appElement, 'strict-di') !== null;
|
||||
@@ -1909,7 +1914,7 @@ function bindJQuery() {
|
||||
jqLite.cleanData = function(elems) {
|
||||
var events;
|
||||
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
|
||||
events = jqLite._data(elem).events;
|
||||
events = (jqLite._data(elem) || {}).events;
|
||||
if (events && events.$destroy) {
|
||||
jqLite(elem).triggerHandler('$destroy');
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
ngInitDirective,
|
||||
ngNonBindableDirective,
|
||||
ngPluralizeDirective,
|
||||
ngRefDirective,
|
||||
ngRepeatDirective,
|
||||
ngShowDirective,
|
||||
ngStyleDirective,
|
||||
@@ -69,6 +70,7 @@
|
||||
$FilterProvider,
|
||||
$$ForceReflowProvider,
|
||||
$InterpolateProvider,
|
||||
$$IntervalFactoryProvider,
|
||||
$IntervalProvider,
|
||||
$HttpProvider,
|
||||
$HttpParamSerializerProvider,
|
||||
@@ -87,6 +89,7 @@
|
||||
$SceProvider,
|
||||
$SceDelegateProvider,
|
||||
$SnifferProvider,
|
||||
$$TaskTrackerFactoryProvider,
|
||||
$TemplateCacheProvider,
|
||||
$TemplateRequestProvider,
|
||||
$$TestabilityProvider,
|
||||
@@ -194,6 +197,7 @@ function publishExternalAPI(angular) {
|
||||
ngInit: ngInitDirective,
|
||||
ngNonBindable: ngNonBindableDirective,
|
||||
ngPluralize: ngPluralizeDirective,
|
||||
ngRef: ngRefDirective,
|
||||
ngRepeat: ngRepeatDirective,
|
||||
ngShow: ngShowDirective,
|
||||
ngStyle: ngStyleDirective,
|
||||
@@ -239,6 +243,7 @@ function publishExternalAPI(angular) {
|
||||
$$forceReflow: $$ForceReflowProvider,
|
||||
$interpolate: $InterpolateProvider,
|
||||
$interval: $IntervalProvider,
|
||||
$$intervalFactory: $$IntervalFactoryProvider,
|
||||
$http: $HttpProvider,
|
||||
$httpParamSerializer: $HttpParamSerializerProvider,
|
||||
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
|
||||
@@ -254,6 +259,7 @@ function publishExternalAPI(angular) {
|
||||
$sce: $SceProvider,
|
||||
$sceDelegate: $SceDelegateProvider,
|
||||
$sniffer: $SnifferProvider,
|
||||
$$taskTrackerFactory: $$TaskTrackerFactoryProvider,
|
||||
$templateCache: $TemplateCacheProvider,
|
||||
$templateRequest: $TemplateRequestProvider,
|
||||
$$testability: $$TestabilityProvider,
|
||||
|
||||
+8
-4
@@ -45,11 +45,10 @@ function NgMapShim() {
|
||||
}
|
||||
NgMapShim.prototype = {
|
||||
_idx: function(key) {
|
||||
if (key === this._lastKey) {
|
||||
return this._lastIndex;
|
||||
if (key !== this._lastKey) {
|
||||
this._lastKey = key;
|
||||
this._lastIndex = this._keys.indexOf(key);
|
||||
}
|
||||
this._lastKey = key;
|
||||
this._lastIndex = this._keys.indexOf(key);
|
||||
return this._lastIndex;
|
||||
},
|
||||
_transformKey: function(key) {
|
||||
@@ -62,6 +61,11 @@ NgMapShim.prototype = {
|
||||
return this._values[idx];
|
||||
}
|
||||
},
|
||||
has: function(key) {
|
||||
key = this._transformKey(key);
|
||||
var idx = this._idx(key);
|
||||
return idx !== -1;
|
||||
},
|
||||
set: function(key, value) {
|
||||
key = this._transformKey(key);
|
||||
var idx = this._idx(key);
|
||||
|
||||
+16
-3
@@ -7,7 +7,8 @@
|
||||
*/
|
||||
|
||||
var minErrConfig = {
|
||||
objectMaxDepth: 5
|
||||
objectMaxDepth: 5,
|
||||
urlErrorParamsEnabled: true
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,12 +31,21 @@ var minErrConfig = {
|
||||
* * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
|
||||
* non-positive or non-numeric value, removes the max depth limit.
|
||||
* Default: 5
|
||||
*
|
||||
* * `urlErrorParamsEnabled` **{Boolean}** - Specifies wether the generated error url will
|
||||
* contain the parameters of the thrown error. Disabling the parameters can be useful if the
|
||||
* generated error url is very long.
|
||||
*
|
||||
* Default: true. When used without argument, it returns the current value.
|
||||
*/
|
||||
function errorHandlingConfig(config) {
|
||||
if (isObject(config)) {
|
||||
if (isDefined(config.objectMaxDepth)) {
|
||||
minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
|
||||
}
|
||||
if (isDefined(config.urlErrorParamsEnabled) && isBoolean(config.urlErrorParamsEnabled)) {
|
||||
minErrConfig.urlErrorParamsEnabled = config.urlErrorParamsEnabled;
|
||||
}
|
||||
} else {
|
||||
return minErrConfig;
|
||||
}
|
||||
@@ -50,6 +60,7 @@ function isValidObjectMaxDepth(maxDepth) {
|
||||
return isNumber(maxDepth) && maxDepth > 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
@@ -113,8 +124,10 @@ function minErr(module, ErrorConstructor) {
|
||||
|
||||
message += '\n' + url + (module ? module + '/' : '') + code;
|
||||
|
||||
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
|
||||
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
|
||||
if (minErrConfig.urlErrorParamsEnabled) {
|
||||
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
|
||||
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return new ErrorConstructor(message);
|
||||
|
||||
+28
-5
@@ -369,14 +369,39 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* **Note**: Generally, the events that are fired correspond 1:1 to `$animate` method names,
|
||||
* e.g. {@link ng.$animate#addClass addClass()} will fire `addClass`, and {@link ng.ngClass}
|
||||
* will fire `addClass` if classes are added, and `removeClass` if classes are removed.
|
||||
* However, there are two exceptions:
|
||||
*
|
||||
* <ul>
|
||||
* <li>if both an {@link ng.$animate#addClass addClass()} and a
|
||||
* {@link ng.$animate#removeClass removeClass()} action are performed during the same
|
||||
* animation, the event fired will be `setClass`. This is true even for `ngClass`.</li>
|
||||
* <li>an {@link ng.$animate#animate animate()} call that adds and removes classes will fire
|
||||
* the `setClass` event, but if it either removes or adds classes,
|
||||
* it will fire `animate` instead.</li>
|
||||
* </ul>
|
||||
*
|
||||
* </div>
|
||||
*
|
||||
* @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...)
|
||||
* @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself
|
||||
* as well as among its children
|
||||
* @param {Function} callback the callback function that will be fired when the listener is triggered
|
||||
* @param {Function} callback the callback function that will be fired when the listener is triggered.
|
||||
*
|
||||
* The arguments present in the callback function are:
|
||||
* * `element` - The captured DOM element that the animation was fired on.
|
||||
* * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends).
|
||||
* * `data` - an object with these properties:
|
||||
* * addClass - `{string|null}` - space-separated CSS classes to add to the element
|
||||
* * removeClass - `{string|null}` - space-separated CSS classes to remove from the element
|
||||
* * from - `{Object|null}` - CSS properties & values at the beginning of the animation
|
||||
* * to - `{Object|null}` - CSS properties & values at the end of the animation
|
||||
*
|
||||
* Note that the callback does not trigger a scope digest. Wrap your call into a
|
||||
* {@link $rootScope.Scope#$apply scope.$apply} to propagate changes to the scope.
|
||||
*/
|
||||
on: $$animateQueue.on,
|
||||
|
||||
@@ -644,9 +669,8 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* @param {object=} options an optional collection of options/styles that will be applied to the element.
|
||||
* The object can have the following properties:
|
||||
*
|
||||
* - **addClass** - `{string}` - space-separated CSS classes to add to element
|
||||
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Runner} animationRunner the animation runner
|
||||
@@ -676,7 +700,6 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
*
|
||||
* - **addClass** - `{string}` - space-separated CSS classes to add to element
|
||||
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Runner} the animation runner
|
||||
@@ -706,8 +729,8 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* The object can have the following properties:
|
||||
*
|
||||
* - **addClass** - `{string}` - space-separated CSS classes to add to element
|
||||
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
|
||||
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
|
||||
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
|
||||
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
|
||||
*
|
||||
* @return {Runner} the animation runner
|
||||
|
||||
+47
-64
@@ -1,5 +1,14 @@
|
||||
'use strict';
|
||||
/* global stripHash: true */
|
||||
/* global getHash: true, stripHash: false */
|
||||
|
||||
function getHash(url) {
|
||||
var index = url.indexOf('#');
|
||||
return index === -1 ? '' : url.substr(index);
|
||||
}
|
||||
|
||||
function trimEmptyHash(url) {
|
||||
return url.replace(/#$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* ! This is a private undocumented service !
|
||||
@@ -22,61 +31,27 @@
|
||||
* @param {object} $log window.console or an object with the same interface.
|
||||
* @param {object} $sniffer $sniffer service
|
||||
*/
|
||||
function Browser(window, document, $log, $sniffer) {
|
||||
function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
|
||||
var self = this,
|
||||
location = window.location,
|
||||
history = window.history,
|
||||
setTimeout = window.setTimeout,
|
||||
clearTimeout = window.clearTimeout,
|
||||
pendingDeferIds = {};
|
||||
pendingDeferIds = {},
|
||||
taskTracker = $$taskTrackerFactory($log);
|
||||
|
||||
self.isMock = false;
|
||||
|
||||
var outstandingRequestCount = 0;
|
||||
var outstandingRequestCallbacks = [];
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Task-tracking API
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO(vojta): remove this temporary api
|
||||
self.$$completeOutstandingRequest = completeOutstandingRequest;
|
||||
self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
|
||||
self.$$completeOutstandingRequest = taskTracker.completeTask;
|
||||
self.$$incOutstandingRequestCount = taskTracker.incTaskCount;
|
||||
|
||||
/**
|
||||
* Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks`
|
||||
* counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
|
||||
*/
|
||||
function completeOutstandingRequest(fn) {
|
||||
try {
|
||||
fn.apply(null, sliceArgs(arguments, 1));
|
||||
} finally {
|
||||
outstandingRequestCount--;
|
||||
if (outstandingRequestCount === 0) {
|
||||
while (outstandingRequestCallbacks.length) {
|
||||
try {
|
||||
outstandingRequestCallbacks.pop()();
|
||||
} catch (e) {
|
||||
$log.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHash(url) {
|
||||
var index = url.indexOf('#');
|
||||
return index === -1 ? '' : url.substr(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* TODO(vojta): prefix this method with $$ ?
|
||||
* @param {function()} callback Function that will be called when no outstanding request
|
||||
*/
|
||||
self.notifyWhenNoOutstandingRequests = function(callback) {
|
||||
if (outstandingRequestCount === 0) {
|
||||
callback();
|
||||
} else {
|
||||
outstandingRequestCallbacks.push(callback);
|
||||
}
|
||||
};
|
||||
// TODO(vojta): prefix this method with $$ ?
|
||||
self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks;
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// URL API
|
||||
@@ -101,20 +76,21 @@ function Browser(window, document, $log, $sniffer) {
|
||||
*
|
||||
* @description
|
||||
* GETTER:
|
||||
* Without any argument, this method just returns current value of location.href.
|
||||
* Without any argument, this method just returns current value of `location.href` (with a
|
||||
* trailing `#` stripped of if the hash is empty).
|
||||
*
|
||||
* SETTER:
|
||||
* With at least one argument, this method sets url to new value.
|
||||
* If html5 history api supported, pushState/replaceState is used, otherwise
|
||||
* location.href/location.replace is used.
|
||||
* Returns its own instance to allow chaining
|
||||
* If html5 history api supported, `pushState`/`replaceState` is used, otherwise
|
||||
* `location.href`/`location.replace` is used.
|
||||
* Returns its own instance to allow chaining.
|
||||
*
|
||||
* NOTE: this api is intended for use only by the $location service. Please use the
|
||||
* NOTE: this api is intended for use only by the `$location` service. Please use the
|
||||
* {@link ng.$location $location service} to change url.
|
||||
*
|
||||
* @param {string} url New url (when used as setter)
|
||||
* @param {boolean=} replace Should new url replace current history record?
|
||||
* @param {object=} state object to use with pushState/replaceState
|
||||
* @param {object=} state State object to use with `pushState`/`replaceState`
|
||||
*/
|
||||
self.url = function(url, replace, state) {
|
||||
// In modern browsers `history.state` is `null` by default; treating it separately
|
||||
@@ -172,7 +148,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).
|
||||
return pendingLocation || location.href;
|
||||
return trimEmptyHash(pendingLocation || location.href);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -307,7 +283,8 @@ function Browser(window, document, $log, $sniffer) {
|
||||
/**
|
||||
* @name $browser#defer
|
||||
* @param {function()} fn A function, who's execution should be deferred.
|
||||
* @param {number=} [delay=0] of milliseconds to defer the function execution.
|
||||
* @param {number=} [delay=0] Number of milliseconds to defer the function execution.
|
||||
* @param {string=} [taskType=DEFAULT_TASK_TYPE] The type of task that is deferred.
|
||||
* @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`.
|
||||
*
|
||||
* @description
|
||||
@@ -318,14 +295,19 @@ function Browser(window, document, $log, $sniffer) {
|
||||
* via `$browser.defer.flush()`.
|
||||
*
|
||||
*/
|
||||
self.defer = function(fn, delay) {
|
||||
self.defer = function(fn, delay, taskType) {
|
||||
var timeoutId;
|
||||
outstandingRequestCount++;
|
||||
|
||||
delay = delay || 0;
|
||||
taskType = taskType || taskTracker.DEFAULT_TASK_TYPE;
|
||||
|
||||
taskTracker.incTaskCount(taskType);
|
||||
timeoutId = setTimeout(function() {
|
||||
delete pendingDeferIds[timeoutId];
|
||||
completeOutstandingRequest(fn);
|
||||
}, delay || 0);
|
||||
pendingDeferIds[timeoutId] = true;
|
||||
taskTracker.completeTask(fn, taskType);
|
||||
}, delay);
|
||||
pendingDeferIds[timeoutId] = taskType;
|
||||
|
||||
return timeoutId;
|
||||
};
|
||||
|
||||
@@ -341,10 +323,11 @@ function Browser(window, document, $log, $sniffer) {
|
||||
* canceled.
|
||||
*/
|
||||
self.defer.cancel = function(deferId) {
|
||||
if (pendingDeferIds[deferId]) {
|
||||
if (pendingDeferIds.hasOwnProperty(deferId)) {
|
||||
var taskType = pendingDeferIds[deferId];
|
||||
delete pendingDeferIds[deferId];
|
||||
clearTimeout(deferId);
|
||||
completeOutstandingRequest(noop);
|
||||
taskTracker.completeTask(noop, taskType);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -354,8 +337,8 @@ function Browser(window, document, $log, $sniffer) {
|
||||
|
||||
/** @this */
|
||||
function $BrowserProvider() {
|
||||
this.$get = ['$window', '$log', '$sniffer', '$document',
|
||||
function($window, $log, $sniffer, $document) {
|
||||
return new Browser($window, $document, $log, $sniffer);
|
||||
}];
|
||||
this.$get = ['$window', '$log', '$sniffer', '$document', '$$taskTrackerFactory',
|
||||
function($window, $log, $sniffer, $document, $$taskTrackerFactory) {
|
||||
return new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory);
|
||||
}];
|
||||
}
|
||||
|
||||
+580
-85
@@ -1030,8 +1030,7 @@
|
||||
*
|
||||
* See issue [#2573](https://github.com/angular/angular.js/issues/2573).
|
||||
*
|
||||
* #### `transclude: element` in the replace template root can have
|
||||
* unexpected effects
|
||||
* #### `transclude: element` in the replace template root can have unexpected effects
|
||||
*
|
||||
* When the replace template has a directive at the root node that uses
|
||||
* {@link $compile#-transclude- `transclude: element`}, e.g.
|
||||
@@ -1045,6 +1044,325 @@
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngProp
|
||||
* @restrict A
|
||||
* @element ANY
|
||||
*
|
||||
* @usage
|
||||
*
|
||||
* ```html
|
||||
* <ANY ng-prop-propname="expression">
|
||||
* </ANY>
|
||||
* ```
|
||||
*
|
||||
* or with uppercase letters in property (e.g. "propName"):
|
||||
*
|
||||
*
|
||||
* ```html
|
||||
* <ANY ng-prop-prop_name="expression">
|
||||
* </ANY>
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @description
|
||||
* The `ngProp` directive binds an expression to a DOM element property.
|
||||
* `ngProp` allows writing to arbitrary properties by including
|
||||
* the property name in the attribute, e.g. `ng-prop-value="'my value'"` binds 'my value' to
|
||||
* the `value` property.
|
||||
*
|
||||
* Usually, it's not necessary to write to properties in AngularJS, as the built-in directives
|
||||
* handle the most common use cases (instead of the above example, you would use {@link ngValue}).
|
||||
*
|
||||
* However, [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements)
|
||||
* often use custom properties to hold data, and `ngProp` can be used to provide input to these
|
||||
* custom elements.
|
||||
*
|
||||
* ## Binding to camelCase properties
|
||||
*
|
||||
* Since HTML attributes are case-insensitive, camelCase properties like `innerHTML` must be escaped.
|
||||
* AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so
|
||||
* `innerHTML` must be written as `ng-prop-inner_h_t_m_l="expression"` (Note that this is just an
|
||||
* example, and for binding HTML {@link ngBindHtml} should be used.
|
||||
*
|
||||
* ## Security
|
||||
*
|
||||
* Binding expressions to arbitrary properties poses a security risk, as properties like `innerHTML`
|
||||
* can insert potentially dangerous HTML into the application, e.g. script tags that execute
|
||||
* malicious code.
|
||||
* For this reason, `ngProp` applies Strict Contextual Escaping with the {@link ng.$sce $sce service}.
|
||||
* This means vulnerable properties require their content to be "trusted", based on the
|
||||
* context of the property. For example, the `innerHTML` is in the `HTML` context, and the
|
||||
* `iframe.src` property is in the `RESOURCE_URL` context, which requires that values written to
|
||||
* this property are trusted as a `RESOURCE_URL`.
|
||||
*
|
||||
* This can be set explicitly by calling $sce.trustAs(type, value) on the value that is
|
||||
* trusted before passing it to the `ng-prop-*` directive. There are exist shorthand methods for
|
||||
* each context type in the form of {@link ng.$sce#trustAsResourceUrl $sce.trustAsResourceUrl()} et al.
|
||||
*
|
||||
* In some cases you can also rely upon automatic sanitization of untrusted values - see below.
|
||||
*
|
||||
* Based on the context, other options may exist to mark a value as trusted / configure the behavior
|
||||
* of {@link ng.$sce}. For example, to restrict the `RESOURCE_URL` context to specific origins, use
|
||||
* the {@link $sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist()}
|
||||
* and {@link $sceDelegateProvider#resourceUrlBlacklist resourceUrlBlacklist()}.
|
||||
*
|
||||
* {@link ng.$sce#what-trusted-context-types-are-supported- Find out more about the different context types}.
|
||||
*
|
||||
* ### HTML Sanitization
|
||||
*
|
||||
* By default, `$sce` will throw an error if it detects untrusted HTML content, and will not bind the
|
||||
* content.
|
||||
* However, if you include the {@link ngSanitize ngSanitize module}, it will try to sanitize the
|
||||
* potentially dangerous HTML, e.g. strip non-whitelisted tags and attributes when binding to
|
||||
* `innerHTML`.
|
||||
*
|
||||
* @example
|
||||
* ### Binding to different contexts
|
||||
*
|
||||
* <example name="ngProp" module="exampleNgProp">
|
||||
* <file name="app.js">
|
||||
* angular.module('exampleNgProp', [])
|
||||
* .component('main', {
|
||||
* templateUrl: 'main.html',
|
||||
* controller: function($sce) {
|
||||
* this.safeContent = '<strong>Safe content</strong>';
|
||||
* this.unsafeContent = '<button onclick="alert(\'Hello XSS!\')">Click for XSS</button>';
|
||||
* this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent);
|
||||
* }
|
||||
* });
|
||||
* </file>
|
||||
* <file name="main.html">
|
||||
* <div>
|
||||
* <div class="prop-unit">
|
||||
* Binding to a property without security context:
|
||||
* <div class="prop-binding" ng-prop-inner_text="$ctrl.safeContent"></div>
|
||||
* <span class="prop-note">innerText</span> (safeContent)
|
||||
* </div>
|
||||
*
|
||||
* <div class="prop-unit">
|
||||
* "Safe" content that requires a security context will throw because the contents could potentially be dangerous ...
|
||||
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.safeContent"></div>
|
||||
* <span class="prop-note">innerHTML</span> (safeContent)
|
||||
* </div>
|
||||
*
|
||||
* <div class="prop-unit">
|
||||
* ... so that actually dangerous content cannot be executed:
|
||||
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.unsafeContent"></div>
|
||||
* <span class="prop-note">innerHTML</span> (unsafeContent)
|
||||
* </div>
|
||||
*
|
||||
* <div class="prop-unit">
|
||||
* ... but unsafe Content that has been trusted explicitly works - only do this if you are 100% sure!
|
||||
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.trustedUnsafeContent"></div>
|
||||
* <span class="prop-note">innerHTML</span> (trustedUnsafeContent)
|
||||
* </div>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="index.html">
|
||||
* <main></main>
|
||||
* </file>
|
||||
* <file name="styles.css">
|
||||
* .prop-unit {
|
||||
* margin-bottom: 10px;
|
||||
* }
|
||||
*
|
||||
* .prop-binding {
|
||||
* min-height: 30px;
|
||||
* border: 1px solid blue;
|
||||
* }
|
||||
*
|
||||
* .prop-note {
|
||||
* font-family: Monospace;
|
||||
* }
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ### Binding to innerHTML with ngSanitize
|
||||
*
|
||||
* <example name="ngProp" module="exampleNgProp" deps="angular-sanitize.js">
|
||||
* <file name="app.js">
|
||||
* angular.module('exampleNgProp', ['ngSanitize'])
|
||||
* .component('main', {
|
||||
* templateUrl: 'main.html',
|
||||
* controller: function($sce) {
|
||||
* this.safeContent = '<strong>Safe content</strong>';
|
||||
* this.unsafeContent = '<button onclick="alert(\'Hello XSS!\')">Click for XSS</button>';
|
||||
* this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent);
|
||||
* }
|
||||
* });
|
||||
* </file>
|
||||
* <file name="main.html">
|
||||
* <div>
|
||||
* <div class="prop-unit">
|
||||
* "Safe" content will be sanitized ...
|
||||
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.safeContent"></div>
|
||||
* <span class="prop-note">innerHTML</span> (safeContent)
|
||||
* </div>
|
||||
*
|
||||
* <div class="prop-unit">
|
||||
* ... as will dangerous content:
|
||||
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.unsafeContent"></div>
|
||||
* <span class="prop-note">innerHTML</span> (unsafeContent)
|
||||
* </div>
|
||||
*
|
||||
* <div class="prop-unit">
|
||||
* ... and content that has been trusted explicitly works the same as without ngSanitize:
|
||||
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.trustedUnsafeContent"></div>
|
||||
* <span class="prop-note">innerHTML</span> (trustedUnsafeContent)
|
||||
* </div>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="index.html">
|
||||
* <main></main>
|
||||
* </file>
|
||||
* <file name="styles.css">
|
||||
* .prop-unit {
|
||||
* margin-bottom: 10px;
|
||||
* }
|
||||
*
|
||||
* .prop-binding {
|
||||
* min-height: 30px;
|
||||
* border: 1px solid blue;
|
||||
* }
|
||||
*
|
||||
* .prop-note {
|
||||
* font-family: Monospace;
|
||||
* }
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
*/
|
||||
|
||||
/** @ngdoc directive
|
||||
* @name ngOn
|
||||
* @restrict A
|
||||
* @element ANY
|
||||
*
|
||||
* @usage
|
||||
*
|
||||
* ```html
|
||||
* <ANY ng-on-eventname="expression">
|
||||
* </ANY>
|
||||
* ```
|
||||
*
|
||||
* or with uppercase letters in property (e.g. "eventName"):
|
||||
*
|
||||
*
|
||||
* ```html
|
||||
* <ANY ng-on-event_name="expression">
|
||||
* </ANY>
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
* The `ngOn` directive adds an event listener to a DOM element via
|
||||
* {@link angular.element angular.element().on()}, and evaluates an expression when the event is
|
||||
* fired.
|
||||
* `ngOn` allows adding listeners for arbitrary events by including
|
||||
* the event name in the attribute, e.g. `ng-on-drop="onDrop()"` executes the 'onDrop()' expression
|
||||
* when the `drop` event is fired.
|
||||
*
|
||||
* AngularJS provides specific directives for many events, such as {@link ngClick}, so in most
|
||||
* cases it is not necessary to use `ngOn`. However, AngularJS does not support all events
|
||||
* (e.g. the `drop` event in the example above), and new events might be introduced in later DOM
|
||||
* standards.
|
||||
*
|
||||
* Another use-case for `ngOn` is listening to
|
||||
* [custom events](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
|
||||
* fired by
|
||||
* [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements).
|
||||
*
|
||||
* ## Binding to camelCase properties
|
||||
*
|
||||
* Since HTML attributes are case-insensitive, camelCase properties like `myEvent` must be escaped.
|
||||
* AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so
|
||||
* `myEvent` must be written as `ng-on-my_event="expression"`.
|
||||
*
|
||||
* @example
|
||||
* ### Bind to built-in DOM events
|
||||
*
|
||||
* <example name="ngOn" module="exampleNgOn">
|
||||
* <file name="app.js">
|
||||
* angular.module('exampleNgOn', [])
|
||||
* .component('main', {
|
||||
* templateUrl: 'main.html',
|
||||
* controller: function() {
|
||||
* this.clickCount = 0;
|
||||
* this.mouseoverCount = 0;
|
||||
*
|
||||
* this.loadingState = 0;
|
||||
* }
|
||||
* });
|
||||
* </file>
|
||||
* <file name="main.html">
|
||||
* <div>
|
||||
* This is equivalent to `ngClick` and `ngMouseover`:<br>
|
||||
* <button
|
||||
* ng-on-click="$ctrl.clickCount = $ctrl.clickCount + 1"
|
||||
* ng-on-mouseover="$ctrl.mouseoverCount = $ctrl.mouseoverCount + 1">Click or mouseover</button><br>
|
||||
* clickCount: {{$ctrl.clickCount}}<br>
|
||||
* mouseover: {{$ctrl.mouseoverCount}}
|
||||
*
|
||||
* <hr>
|
||||
*
|
||||
* For the `error` and `load` event on images no built-in AngularJS directives exist:<br>
|
||||
* <img src="thisimagedoesnotexist.png" ng-on-error="$ctrl.loadingState = -1" ng-on-load="$ctrl.loadingState = 1"><br>
|
||||
* <div ng-switch="$ctrl.loadingState">
|
||||
* <span ng-switch-when="0">Image is loading</span>
|
||||
* <span ng-switch-when="-1">Image load error</span>
|
||||
* <span ng-switch-when="1">Image loaded successfully</span>
|
||||
* </div>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="index.html">
|
||||
* <main></main>
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ### Bind to custom DOM events
|
||||
*
|
||||
* <example name="ngOnCustom" module="exampleNgOn">
|
||||
* <file name="app.js">
|
||||
* angular.module('exampleNgOn', [])
|
||||
* .component('main', {
|
||||
* templateUrl: 'main.html',
|
||||
* controller: function() {
|
||||
* this.eventLog = '';
|
||||
*
|
||||
* this.listener = function($event) {
|
||||
* this.eventLog = 'Event with type "' + $event.type + '" fired at ' + $event.detail;
|
||||
* };
|
||||
* }
|
||||
* })
|
||||
* .component('childComponent', {
|
||||
* templateUrl: 'child.html',
|
||||
* controller: function($element) {
|
||||
* this.fireEvent = function() {
|
||||
* var event = new CustomEvent('customtype', { detail: new Date()});
|
||||
*
|
||||
* $element[0].dispatchEvent(event);
|
||||
* };
|
||||
* }
|
||||
* });
|
||||
* </file>
|
||||
* <file name="main.html">
|
||||
* <child-component ng-on-customtype="$ctrl.listener($event)"></child-component><br>
|
||||
* <span>Event log: {{$ctrl.eventLog}}</span>
|
||||
* </file>
|
||||
* <file name="child.html">
|
||||
<button ng-click="$ctrl.fireEvent()">Fire custom event</button>
|
||||
* </file>
|
||||
* <file name="index.html">
|
||||
* <main></main>
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
|
||||
var $compileMinErr = minErr('$compile');
|
||||
|
||||
function UNINITIALIZED_VALUE() {}
|
||||
@@ -1587,6 +1905,91 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
return cssClassDirectivesEnabledConfig;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The security context of DOM Properties.
|
||||
* @private
|
||||
*/
|
||||
var PROP_CONTEXTS = createMap();
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $compileProvider#addPropertySecurityContext
|
||||
* @description
|
||||
*
|
||||
* Defines the security context for DOM properties bound by ng-prop-*.
|
||||
*
|
||||
* @param {string} elementName The element name or '*' to match any element.
|
||||
* @param {string} propertyName The DOM property name.
|
||||
* @param {string} ctx The {@link $sce} security context in which this value is safe for use, e.g. `$sce.URL`
|
||||
* @returns {object} `this` for chaining
|
||||
*/
|
||||
this.addPropertySecurityContext = function(elementName, propertyName, ctx) {
|
||||
var key = (elementName.toLowerCase() + '|' + propertyName.toLowerCase());
|
||||
|
||||
if (key in PROP_CONTEXTS && PROP_CONTEXTS[key] !== ctx) {
|
||||
throw $compileMinErr('ctxoverride', 'Property context \'{0}.{1}\' already set to \'{2}\', cannot override to \'{3}\'.', elementName, propertyName, PROP_CONTEXTS[key], ctx);
|
||||
}
|
||||
|
||||
PROP_CONTEXTS[key] = ctx;
|
||||
return this;
|
||||
};
|
||||
|
||||
/* Default property contexts.
|
||||
*
|
||||
* Copy of https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58
|
||||
* Changing:
|
||||
* - SecurityContext.* => SCE_CONTEXTS/$sce.*
|
||||
* - STYLE => CSS
|
||||
* - various URL => MEDIA_URL
|
||||
* - *|formAction, form|action URL => RESOURCE_URL (like the attribute)
|
||||
*/
|
||||
(function registerNativePropertyContexts() {
|
||||
function registerContext(ctx, values) {
|
||||
forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; });
|
||||
}
|
||||
|
||||
registerContext(SCE_CONTEXTS.HTML, [
|
||||
'iframe|srcdoc',
|
||||
'*|innerHTML',
|
||||
'*|outerHTML'
|
||||
]);
|
||||
registerContext(SCE_CONTEXTS.CSS, ['*|style']);
|
||||
registerContext(SCE_CONTEXTS.URL, [
|
||||
'area|href', 'area|ping',
|
||||
'a|href', 'a|ping',
|
||||
'blockquote|cite',
|
||||
'body|background',
|
||||
'del|cite',
|
||||
'input|src',
|
||||
'ins|cite',
|
||||
'q|cite'
|
||||
]);
|
||||
registerContext(SCE_CONTEXTS.MEDIA_URL, [
|
||||
'audio|src',
|
||||
'img|src', 'img|srcset',
|
||||
'source|src', 'source|srcset',
|
||||
'track|src',
|
||||
'video|src', 'video|poster'
|
||||
]);
|
||||
registerContext(SCE_CONTEXTS.RESOURCE_URL, [
|
||||
'*|formAction',
|
||||
'applet|code', 'applet|codebase',
|
||||
'base|href',
|
||||
'embed|src',
|
||||
'frame|src',
|
||||
'form|action',
|
||||
'head|profile',
|
||||
'html|manifest',
|
||||
'iframe|src',
|
||||
'link|href',
|
||||
'media|src',
|
||||
'object|codebase', 'object|data',
|
||||
'script|src'
|
||||
]);
|
||||
})();
|
||||
|
||||
|
||||
this.$get = [
|
||||
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
|
||||
'$controller', '$rootScope', '$sce', '$animate',
|
||||
@@ -1632,6 +2035,57 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
|
||||
|
||||
function sanitizeSrcset(value, invokeType) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
if (!isString(value)) {
|
||||
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `{0}`: "{1}"', invokeType, 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
|
||||
var trimmedSrcset = trim(value);
|
||||
// ( 999x ,| 999w ,| ,|, )
|
||||
var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
|
||||
var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
|
||||
|
||||
// split srcset into tuple of uri and descriptor except for the last item
|
||||
var rawUris = trimmedSrcset.split(pattern);
|
||||
|
||||
// for each tuples
|
||||
var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
|
||||
for (var i = 0; i < nbrUrisWith2parts; i++) {
|
||||
var innerIdx = i * 2;
|
||||
// sanitize the uri
|
||||
result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
|
||||
// add the descriptor
|
||||
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 += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
|
||||
|
||||
// and add the last descriptor if any
|
||||
if (lastTuple.length === 2) {
|
||||
result += (' ' + trim(lastTuple[1]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function Attributes(element, attributesToCopy) {
|
||||
if (attributesToCopy) {
|
||||
var keys = Object.keys(attributesToCopy);
|
||||
@@ -1768,51 +2222,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
nodeName = nodeName_(this.$$element);
|
||||
|
||||
// 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
|
||||
var trimmedSrcset = trim(value);
|
||||
// ( 999x ,| 999w ,| ,|, )
|
||||
var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
|
||||
var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
|
||||
|
||||
// split srcset into tuple of uri and descriptor except for the last item
|
||||
var rawUris = trimmedSrcset.split(pattern);
|
||||
|
||||
// for each tuples
|
||||
var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
|
||||
for (var i = 0; i < nbrUrisWith2parts; i++) {
|
||||
var innerIdx = i * 2;
|
||||
// sanitize the uri
|
||||
result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
|
||||
// add the descriptor
|
||||
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 += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
|
||||
|
||||
// and add the last descriptor if any
|
||||
if (lastTuple.length === 2) {
|
||||
result += (' ' + trim(lastTuple[1]));
|
||||
}
|
||||
this[key] = value = result;
|
||||
if (nodeName === 'img' && key === 'srcset') {
|
||||
this[key] = value = sanitizeSrcset(value, '$set(\'srcset\', value)');
|
||||
}
|
||||
|
||||
if (writeAttr !== false) {
|
||||
@@ -1909,7 +2320,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
: function denormalizeTemplate(template) {
|
||||
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
|
||||
},
|
||||
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
|
||||
NG_PREFIX_BINDING = /^ng(Attr|Prop|On)([A-Z].*)$/;
|
||||
var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
|
||||
|
||||
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
|
||||
@@ -2245,43 +2656,66 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective);
|
||||
|
||||
// iterate over the attributes
|
||||
for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
|
||||
for (var attr, name, nName, value, ngPrefixMatch, nAttrs = node.attributes,
|
||||
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
|
||||
var attrStartName = false;
|
||||
var attrEndName = false;
|
||||
|
||||
var isNgAttr = false, isNgProp = false, isNgEvent = false;
|
||||
var multiElementMatch;
|
||||
|
||||
attr = nAttrs[j];
|
||||
name = attr.name;
|
||||
value = attr.value;
|
||||
|
||||
// support ngAttr attribute binding
|
||||
ngAttrName = directiveNormalize(name);
|
||||
isNgAttr = NG_ATTR_BINDING.test(ngAttrName);
|
||||
if (isNgAttr) {
|
||||
nName = directiveNormalize(name.toLowerCase());
|
||||
|
||||
// Support ng-attr-*, ng-prop-* and ng-on-*
|
||||
if ((ngPrefixMatch = nName.match(NG_PREFIX_BINDING))) {
|
||||
isNgAttr = ngPrefixMatch[1] === 'Attr';
|
||||
isNgProp = ngPrefixMatch[1] === 'Prop';
|
||||
isNgEvent = ngPrefixMatch[1] === 'On';
|
||||
|
||||
// Normalize the non-prefixed name
|
||||
name = name.replace(PREFIX_REGEXP, '')
|
||||
.substr(8).replace(/_(.)/g, function(match, letter) {
|
||||
.toLowerCase()
|
||||
.substr(4 + ngPrefixMatch[1].length).replace(/_(.)/g, function(match, letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
|
||||
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
|
||||
// Support *-start / *-end multi element directives
|
||||
} else if ((multiElementMatch = nName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) {
|
||||
attrStartName = name;
|
||||
attrEndName = name.substr(0, name.length - 5) + 'end';
|
||||
name = name.substr(0, name.length - 6);
|
||||
}
|
||||
|
||||
nName = directiveNormalize(name.toLowerCase());
|
||||
attrsMap[nName] = name;
|
||||
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
|
||||
if (isNgProp || isNgEvent) {
|
||||
attrs[nName] = value;
|
||||
attrsMap[nName] = attr.name;
|
||||
|
||||
if (isNgProp) {
|
||||
addPropertyDirective(node, directives, nName, name);
|
||||
} else {
|
||||
addEventDirective(directives, nName, name);
|
||||
}
|
||||
} else {
|
||||
// Update nName for cases where a prefix was removed
|
||||
// NOTE: the .toLowerCase() is unnecessary and causes https://github.com/angular/angular.js/issues/16624 for ng-attr-*
|
||||
nName = directiveNormalize(name.toLowerCase());
|
||||
attrsMap[nName] = name;
|
||||
|
||||
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
|
||||
attrs[nName] = value;
|
||||
if (getBooleanAttrName(node, nName)) {
|
||||
attrs[nName] = true; // presence means true
|
||||
}
|
||||
}
|
||||
|
||||
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
|
||||
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
|
||||
attrEndName);
|
||||
}
|
||||
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
|
||||
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
|
||||
attrEndName);
|
||||
}
|
||||
|
||||
if (nodeName === 'input' && node.getAttribute('type') === 'hidden') {
|
||||
@@ -2575,7 +3009,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
|
||||
// We have transclusion slots,
|
||||
// collect them up, compile them and store their transclusion functions
|
||||
$template = [];
|
||||
$template = window.document.createDocumentFragment();
|
||||
|
||||
var slotMap = createMap();
|
||||
var filledSlots = createMap();
|
||||
@@ -2603,10 +3037,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
var slotName = slotMap[directiveNormalize(nodeName_(node))];
|
||||
if (slotName) {
|
||||
filledSlots[slotName] = true;
|
||||
slots[slotName] = slots[slotName] || [];
|
||||
slots[slotName].push(node);
|
||||
slots[slotName] = slots[slotName] || window.document.createDocumentFragment();
|
||||
slots[slotName].appendChild(node);
|
||||
} else {
|
||||
$template.push(node);
|
||||
$template.appendChild(node);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2620,9 +3054,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
for (var slotName in slots) {
|
||||
if (slots[slotName]) {
|
||||
// Only define a transclusion function if the slot was filled
|
||||
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
|
||||
var slotCompileNodes = jqLite(slots[slotName].childNodes);
|
||||
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slotCompileNodes, transcludeFn);
|
||||
}
|
||||
}
|
||||
|
||||
$template = jqLite($template.childNodes);
|
||||
}
|
||||
|
||||
$compileNode.empty(); // clear contents
|
||||
@@ -2958,7 +3395,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
|
||||
if (!value) {
|
||||
var dataName = '$' + name + 'Controller';
|
||||
value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
|
||||
|
||||
if (inheritType === '^^' && $element[0] && $element[0].nodeType === NODE_TYPE_DOCUMENT) {
|
||||
// inheritedData() uses the documentElement when it finds the document, so we would
|
||||
// require from the element itself.
|
||||
value = null;
|
||||
} else {
|
||||
value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!value && !optional) {
|
||||
@@ -3315,42 +3759,95 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
|
||||
|
||||
function getTrustedContext(node, attrNormalizedName) {
|
||||
function getTrustedAttrContext(nodeName, attrNormalizedName) {
|
||||
if (attrNormalizedName === 'srcdoc') {
|
||||
return $sce.HTML;
|
||||
}
|
||||
var tag = nodeName_(node);
|
||||
// All tags with src attributes require a RESOURCE_URL value, except for
|
||||
// img and various html5 media tags, which require the MEDIA_URL context.
|
||||
// All nodes with src attributes require a RESOURCE_URL value, except for
|
||||
// img and various html5 media nodes, which require the MEDIA_URL context.
|
||||
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
|
||||
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
|
||||
if (['img', 'video', 'audio', 'source', 'track'].indexOf(nodeName) === -1) {
|
||||
return $sce.RESOURCE_URL;
|
||||
}
|
||||
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;
|
||||
if (nodeName === 'image') return $sce.MEDIA_URL;
|
||||
if (nodeName === 'a') return $sce.URL;
|
||||
return $sce.RESOURCE_URL;
|
||||
} else if (
|
||||
// Formaction
|
||||
(tag === 'form' && attrNormalizedName === 'action') ||
|
||||
(nodeName === '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') ||
|
||||
(nodeName === 'base' && attrNormalizedName === 'href') ||
|
||||
// links can be stylesheets or imports, which can run script in the current origin
|
||||
(tag === 'link' && attrNormalizedName === 'href')
|
||||
(nodeName === 'link' && attrNormalizedName === 'href')
|
||||
) {
|
||||
return $sce.RESOURCE_URL;
|
||||
} else if (tag === 'a' && (attrNormalizedName === 'href' ||
|
||||
} else if (nodeName === 'a' && (attrNormalizedName === 'href' ||
|
||||
attrNormalizedName === 'ngHref')) {
|
||||
return $sce.URL;
|
||||
}
|
||||
}
|
||||
|
||||
function getTrustedPropContext(nodeName, propNormalizedName) {
|
||||
var prop = propNormalizedName.toLowerCase();
|
||||
return PROP_CONTEXTS[nodeName + '|' + prop] || PROP_CONTEXTS['*|' + prop];
|
||||
}
|
||||
|
||||
function sanitizeSrcsetPropertyValue(value) {
|
||||
return sanitizeSrcset($sce.valueOf(value), 'ng-prop-srcset');
|
||||
}
|
||||
function addPropertyDirective(node, directives, attrName, propName) {
|
||||
if (EVENT_HANDLER_ATTR_REGEXP.test(propName)) {
|
||||
throw $compileMinErr('nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
}
|
||||
|
||||
var nodeName = nodeName_(node);
|
||||
var trustedContext = getTrustedPropContext(nodeName, propName);
|
||||
|
||||
var sanitizer = identity;
|
||||
// Sanitize img[srcset] + source[srcset] values.
|
||||
if (propName === 'srcset' && (nodeName === 'img' || nodeName === 'source')) {
|
||||
sanitizer = sanitizeSrcsetPropertyValue;
|
||||
} else if (trustedContext) {
|
||||
sanitizer = $sce.getTrusted.bind($sce, trustedContext);
|
||||
}
|
||||
|
||||
directives.push({
|
||||
priority: 100,
|
||||
compile: function ngPropCompileFn(_, attr) {
|
||||
var ngPropGetter = $parse(attr[attrName]);
|
||||
var ngPropWatch = $parse(attr[attrName], function sceValueOf(val) {
|
||||
// Unwrap the value to compare the actual inner safe value, not the wrapper object.
|
||||
return $sce.valueOf(val);
|
||||
});
|
||||
|
||||
return {
|
||||
pre: function ngPropPreLinkFn(scope, $element) {
|
||||
function applyPropValue() {
|
||||
var propValue = ngPropGetter(scope);
|
||||
$element.prop(propName, sanitizer(propValue));
|
||||
}
|
||||
|
||||
applyPropValue();
|
||||
scope.$watch(ngPropWatch, applyPropValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addEventDirective(directives, attrName, eventName) {
|
||||
directives.push(
|
||||
createEventDirective($parse, $rootScope, $exceptionHandler, attrName, eventName, /*forceAsync=*/false)
|
||||
);
|
||||
}
|
||||
|
||||
function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) {
|
||||
var trustedContext = getTrustedContext(node, name);
|
||||
var nodeName = nodeName_(node);
|
||||
var trustedContext = getTrustedAttrContext(nodeName, name);
|
||||
var mustHaveExpression = !isNgAttr;
|
||||
var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr;
|
||||
|
||||
@@ -3359,16 +3856,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
// no interpolation found -> ignore
|
||||
if (!interpolateFn) return;
|
||||
|
||||
if (name === 'multiple' && nodeName_(node) === 'select') {
|
||||
if (name === 'multiple' && nodeName === 'select') {
|
||||
throw $compileMinErr('selmulti',
|
||||
'Binding to the \'multiple\' attribute is not supported. Element: {0}',
|
||||
startingTag(node));
|
||||
}
|
||||
|
||||
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
|
||||
throw $compileMinErr('nodomevents',
|
||||
'Interpolations for HTML DOM event attributes are disallowed. Please use the ' +
|
||||
'ng- versions (such as ng-click instead of onclick) instead.');
|
||||
throw $compileMinErr('nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
}
|
||||
|
||||
directives.push({
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
var nullFormCtrl = {
|
||||
$addControl: noop,
|
||||
$getControls: valueFn([]),
|
||||
$$renameControl: nullFormRenameControl,
|
||||
$removeControl: noop,
|
||||
$setValidity: noop,
|
||||
@@ -159,6 +160,30 @@ FormController.prototype = {
|
||||
control.$$parentForm = this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name form.FormController#$getControls
|
||||
* @returns {Array} the controls that are currently part of this form
|
||||
*
|
||||
* @description
|
||||
* This method returns a **shallow copy** of the controls that are currently part of this form.
|
||||
* The controls can be instances of {@link form.FormController `FormController`}
|
||||
* ({@link ngForm "child-forms"}) and of {@link ngModel.NgModelController `NgModelController`}.
|
||||
* If you need access to the controls of child-forms, you have to call `$getControls()`
|
||||
* recursively on them.
|
||||
* This can be used for example to iterate over all controls to validate them.
|
||||
*
|
||||
* The controls can be accessed normally, but adding to, or removing controls from the array has
|
||||
* no effect on the form. Instead, use {@link form.FormController#$addControl `$addControl()`} and
|
||||
* {@link form.FormController#$removeControl `$removeControl()`} for this use-case.
|
||||
* Likewise, adding a control to, or removing a control from the form is not reflected
|
||||
* in the shallow copy. That means you should get a fresh copy from `$getControls()` every time
|
||||
* you need access to the controls.
|
||||
*/
|
||||
$getControls: function() {
|
||||
return shallowCopy(this.$$controls);
|
||||
},
|
||||
|
||||
// Private API: rename a form control
|
||||
$$renameControl: function(control, newName) {
|
||||
var oldName = control.$name;
|
||||
|
||||
@@ -255,6 +255,10 @@ var inputType = {
|
||||
* 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.
|
||||
*
|
||||
* The format of the displayed time can be adjusted with the
|
||||
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
|
||||
* and `timeStripZeroSeconds`.
|
||||
*
|
||||
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
||||
* @param {string=} name Property name of the form under which the control is published.
|
||||
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
|
||||
@@ -356,7 +360,12 @@ var inputType = {
|
||||
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
|
||||
*
|
||||
* 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.
|
||||
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions}. By default,
|
||||
* this is the timezone of the browser.
|
||||
*
|
||||
* The format of the displayed time can be adjusted with the
|
||||
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
|
||||
* and `timeStripZeroSeconds`.
|
||||
*
|
||||
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
||||
* @param {string=} name Property name of the form under which the control is published.
|
||||
@@ -1491,6 +1500,8 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
|
||||
badInputChecker(scope, element, attr, ctrl, type);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
var isTimeType = type === 'time' || type === 'datetimelocal';
|
||||
var previousDate;
|
||||
var previousTimezone;
|
||||
|
||||
@@ -1514,11 +1525,13 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
if (isValidDate(value)) {
|
||||
previousDate = value;
|
||||
var timezone = ctrl.$options.getOption('timezone');
|
||||
|
||||
if (timezone) {
|
||||
previousTimezone = timezone;
|
||||
previousDate = convertTimezoneToLocal(previousDate, timezone, true);
|
||||
}
|
||||
return $filter('date')(value, format, timezone);
|
||||
|
||||
return formatter(value, timezone);
|
||||
} else {
|
||||
previousDate = null;
|
||||
previousTimezone = null;
|
||||
@@ -1573,6 +1586,24 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
}
|
||||
return parsedDate;
|
||||
}
|
||||
|
||||
function formatter(value, timezone) {
|
||||
var targetFormat = format;
|
||||
|
||||
if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) {
|
||||
targetFormat = format
|
||||
.replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat'))
|
||||
.replace(/:$/, '');
|
||||
}
|
||||
|
||||
var formatted = $filter('date')(value, targetFormat, timezone);
|
||||
|
||||
if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) {
|
||||
formatted = formatted.replace(/(?::00)?(?:\.000)?$/, '');
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ function classDirective(name, selector) {
|
||||
* |----------------------------------|-------------------------------------|
|
||||
* | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
|
||||
* | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
|
||||
* | {@link ng.$animate#setClass setClass} | just before classes are added and classes are removed from the element at the same time |
|
||||
*
|
||||
* ### ngClass and pre-existing CSS3 Transitions/Animations
|
||||
The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
|
||||
|
||||
@@ -50,33 +50,44 @@ forEach(
|
||||
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
|
||||
function(eventName) {
|
||||
var directiveName = directiveNormalize('ng-' + eventName);
|
||||
ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function($element, attr) {
|
||||
// NOTE:
|
||||
// We expose the powerful `$event` object on the scope that provides access to the Window,
|
||||
// etc. This is OK, because expressions are not sandboxed any more (and the expression
|
||||
// sandbox was never meant to be a security feature anyway).
|
||||
var fn = $parse(attr[directiveName]);
|
||||
return function ngEventHandler(scope, element) {
|
||||
element.on(eventName, function(event) {
|
||||
var callback = function() {
|
||||
fn(scope, {$event: event});
|
||||
};
|
||||
if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
|
||||
scope.$evalAsync(callback);
|
||||
} else {
|
||||
scope.$apply(callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
ngEventDirectives[directiveName] = ['$parse', '$rootScope', '$exceptionHandler', function($parse, $rootScope, $exceptionHandler) {
|
||||
return createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsyncEvents[eventName]);
|
||||
}];
|
||||
}
|
||||
);
|
||||
|
||||
function createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsync) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function($element, attr) {
|
||||
// NOTE:
|
||||
// We expose the powerful `$event` object on the scope that provides access to the Window,
|
||||
// etc. This is OK, because expressions are not sandboxed any more (and the expression
|
||||
// sandbox was never meant to be a security feature anyway).
|
||||
var fn = $parse(attr[directiveName]);
|
||||
return function ngEventHandler(scope, element) {
|
||||
element.on(eventName, function(event) {
|
||||
var callback = function() {
|
||||
fn(scope, {$event: event});
|
||||
};
|
||||
|
||||
if (!$rootScope.$$phase) {
|
||||
scope.$apply(callback);
|
||||
} else if (forceAsync) {
|
||||
scope.$evalAsync(callback);
|
||||
} else {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
$exceptionHandler(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngDblclick
|
||||
|
||||
@@ -287,6 +287,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
|
||||
this.$$currentValidationRunId = 0;
|
||||
|
||||
this.$$scope = $scope;
|
||||
this.$$rootScope = $scope.$root;
|
||||
this.$$attr = $attr;
|
||||
this.$$element = $element;
|
||||
this.$$animate = $animate;
|
||||
@@ -864,7 +865,7 @@ NgModelController.prototype = {
|
||||
this.$$pendingDebounce = this.$$timeout(function() {
|
||||
that.$commitViewValue();
|
||||
}, debounceDelay);
|
||||
} else if (this.$$scope.$root.$$phase) {
|
||||
} else if (this.$$rootScope.$$phase) {
|
||||
this.$commitViewValue();
|
||||
} else {
|
||||
this.$$scope.$apply(function() {
|
||||
|
||||
@@ -41,7 +41,7 @@ ModelOptions.prototype = {
|
||||
options = extend({}, options);
|
||||
|
||||
// Inherit options from the parent if specified by the value `"$inherit"`
|
||||
forEach(options, /* @this */ function(option, key) {
|
||||
forEach(options, /** @this */ function(option, key) {
|
||||
if (option === '$inherit') {
|
||||
if (key === '*') {
|
||||
inheritAll = true;
|
||||
@@ -406,12 +406,6 @@ defaultModelOptions = new ModelOptions({
|
||||
* </example>
|
||||
*
|
||||
*
|
||||
* ## Specifying timezones
|
||||
*
|
||||
* You can specify the timezone that date/time input directives expect by providing its name in the
|
||||
* `timezone` property.
|
||||
*
|
||||
*
|
||||
* ## Programmatically changing options
|
||||
*
|
||||
* The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not
|
||||
@@ -423,8 +417,70 @@ defaultModelOptions = new ModelOptions({
|
||||
* Default events, extra triggers, and catch-all debounce values}.
|
||||
*
|
||||
*
|
||||
* ## Specifying timezones
|
||||
*
|
||||
* You can specify the timezone that date/time input directives expect by providing its name in the
|
||||
* `timezone` property.
|
||||
*
|
||||
*
|
||||
* ## Formatting the value of `time` and `datetime-local`
|
||||
*
|
||||
* With the options `timeSecondsFormat` and `timeStripZeroSeconds` it is possible to adjust the value
|
||||
* that is displayed in the control. Note that browsers may apply their own formatting
|
||||
* in the user interface.
|
||||
*
|
||||
<example name="ngModelOptions-time-format" module="timeExample">
|
||||
<file name="index.html">
|
||||
<time-example></time-example>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('timeExample', [])
|
||||
.component('timeExample', {
|
||||
templateUrl: 'timeExample.html',
|
||||
controller: function() {
|
||||
this.time = new Date(1970, 0, 1, 14, 57, 0);
|
||||
|
||||
this.options = {
|
||||
timeSecondsFormat: 'ss',
|
||||
timeStripZeroSeconds: true
|
||||
};
|
||||
|
||||
this.optionChange = function() {
|
||||
this.timeForm.timeFormatted.$overrideModelOptions(this.options);
|
||||
this.time = new Date(this.time);
|
||||
};
|
||||
}
|
||||
});
|
||||
</file>
|
||||
<file name="timeExample.html">
|
||||
<form name="$ctrl.timeForm">
|
||||
<strong>Default</strong>:
|
||||
<input type="time" ng-model="$ctrl.time" step="any" /><br>
|
||||
<strong>With options</strong>:
|
||||
<input type="time" name="timeFormatted" ng-model="$ctrl.time" step="any" ng-model-options="$ctrl.options" />
|
||||
<br>
|
||||
|
||||
Options:<br>
|
||||
<code>timeSecondsFormat</code>:
|
||||
<input
|
||||
type="text"
|
||||
ng-model="$ctrl.options.timeSecondsFormat"
|
||||
ng-change="$ctrl.optionChange()">
|
||||
<br>
|
||||
<code>timeStripZeroSeconds</code>:
|
||||
<input
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.options.timeStripZeroSeconds"
|
||||
ng-change="$ctrl.optionChange()">
|
||||
</form>
|
||||
</file>
|
||||
* </example>
|
||||
*
|
||||
* @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
|
||||
* and its descendents. Valid keys are:
|
||||
* and its descendents.
|
||||
*
|
||||
* **General options**:
|
||||
*
|
||||
* - `updateOn`: string specifying which event should the input be bound to. You can set several
|
||||
* events using an space delimited list. There is a special event called `default` that
|
||||
* matches the default events belonging to the control. These are the events that are bound to
|
||||
@@ -457,6 +513,10 @@ defaultModelOptions = new ModelOptions({
|
||||
* 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
|
||||
* `ngModel` as getters/setters.
|
||||
*
|
||||
*
|
||||
* **Input-type specific options**:
|
||||
*
|
||||
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
|
||||
* `<input type="date" />`, `<input type="time" />`, ... . It understands UTC/GMT and the
|
||||
* continental US time zone abbreviations, but for general use, use a time zone offset, for
|
||||
@@ -465,6 +525,24 @@ defaultModelOptions = new ModelOptions({
|
||||
* Note that changing the timezone will have no effect on the current date, and is only applied after
|
||||
* the next input / model change.
|
||||
*
|
||||
* - `timeSecondsFormat`: Defines if the `time` and `datetime-local` types should show seconds and
|
||||
* milliseconds. The option follows the format string of {@link date date filter}.
|
||||
* By default, the options is `undefined` which is equal to `'ss.sss'` (seconds and milliseconds).
|
||||
* The other options are `'ss'` (strips milliseconds), and `''` (empty string), which strips both
|
||||
* seconds and milliseconds.
|
||||
* Note that browsers that support `time` and `datetime-local` require the hour and minutes
|
||||
* part of the time string, and may show the value differently in the user interface.
|
||||
* {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
|
||||
*
|
||||
* - `timeStripZeroSeconds`: Defines if the `time` and `datetime-local` types should strip the
|
||||
* seconds and milliseconds from the formatted value if they are zero. This option is applied
|
||||
* after `timeSecondsFormat`.
|
||||
* This option can be used to make the formatting consistent over different browsers, as some
|
||||
* browsers with support for `time` will natively hide the milliseconds and
|
||||
* seconds if they are zero, but others won't, and browsers that don't implement these input
|
||||
* types will always show the full string.
|
||||
* {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
|
||||
*
|
||||
*/
|
||||
var ngModelOptionsDirective = function() {
|
||||
NgModelOptionsController.$inject = ['$attrs', '$scope'];
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngRef
|
||||
* @restrict A
|
||||
*
|
||||
* @description
|
||||
* The `ngRef` attribute tells AngularJS to assign the controller of a component (or a directive)
|
||||
* to the given property in the current scope. It is also possible to add the jqlite-wrapped DOM
|
||||
* element to the scope.
|
||||
*
|
||||
* If the element with `ngRef` is destroyed `null` is assigned to the property.
|
||||
*
|
||||
* Note that if you want to assign from a child into the parent scope, you must initialize the
|
||||
* target property on the parent scope, otherwise `ngRef` will assign on the child scope.
|
||||
* This commonly happens when assigning elements or components wrapped in {@link ngIf} or
|
||||
* {@link ngRepeat}. See the second example below.
|
||||
*
|
||||
*
|
||||
* @element ANY
|
||||
* @param {string} ngRef property name - A valid AngularJS expression identifier to which the
|
||||
* controller or jqlite-wrapped DOM element will be bound.
|
||||
* @param {string=} ngRefRead read value - The name of a directive (or component) on this element,
|
||||
* or the special string `$element`. If a name is provided, `ngRef` will
|
||||
* assign the matching controller. If `$element` is provided, the element
|
||||
* itself is assigned (even if a controller is available).
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ### Simple toggle
|
||||
* This example shows how the controller of the component toggle
|
||||
* is reused in the template through the scope to use its logic.
|
||||
* <example name="ng-ref-component" module="myApp">
|
||||
* <file name="index.html">
|
||||
* <my-toggle ng-ref="myToggle"></my-toggle>
|
||||
* <button ng-click="myToggle.toggle()">Toggle</button>
|
||||
* <div ng-show="myToggle.isOpen()">
|
||||
* You are using a component in the same template to show it.
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="index.js">
|
||||
* angular.module('myApp', [])
|
||||
* .component('myToggle', {
|
||||
* controller: function ToggleController() {
|
||||
* var opened = false;
|
||||
* this.isOpen = function() { return opened; };
|
||||
* this.toggle = function() { opened = !opened; };
|
||||
* }
|
||||
* });
|
||||
* </file>
|
||||
* <file name="protractor.js" type="protractor">
|
||||
* it('should publish the toggle into the scope', function() {
|
||||
* var toggle = element(by.buttonText('Toggle'));
|
||||
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(false);
|
||||
* toggle.click();
|
||||
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(true);
|
||||
* });
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
* @example
|
||||
* ### ngRef inside scopes
|
||||
* This example shows how `ngRef` works with child scopes. The `ngRepeat`-ed `myWrapper` components
|
||||
* are assigned to the scope of `myRoot`, because the `toggles` property has been initialized.
|
||||
* The repeated `myToggle` components are published to the child scopes created by `ngRepeat`.
|
||||
* `ngIf` behaves similarly - the assignment of `myToggle` happens in the `ngIf` child scope,
|
||||
* because the target property has not been initialized on the `myRoot` component controller.
|
||||
*
|
||||
* <example name="ng-ref-scopes" module="myApp">
|
||||
* <file name="index.html">
|
||||
* <my-root></my-root>
|
||||
* </file>
|
||||
* <file name="index.js">
|
||||
* angular.module('myApp', [])
|
||||
* .component('myRoot', {
|
||||
* templateUrl: 'root.html',
|
||||
* controller: function() {
|
||||
* this.wrappers = []; // initialize the array so that the wrappers are assigned into the parent scope
|
||||
* }
|
||||
* })
|
||||
* .component('myToggle', {
|
||||
* template: '<strong>myToggle</strong><button ng-click="$ctrl.toggle()" ng-transclude></button>',
|
||||
* transclude: true,
|
||||
* controller: function ToggleController() {
|
||||
* var opened = false;
|
||||
* this.isOpen = function() { return opened; };
|
||||
* this.toggle = function() { opened = !opened; };
|
||||
* }
|
||||
* })
|
||||
* .component('myWrapper', {
|
||||
* transclude: true,
|
||||
* template: '<strong>myWrapper</strong>' +
|
||||
* '<div>ngRepeatToggle.isOpen(): {{$ctrl.ngRepeatToggle.isOpen() | json}}</div>' +
|
||||
* '<my-toggle ng-ref="$ctrl.ngRepeatToggle"><ng-transclude></ng-transclude></my-toggle>'
|
||||
* });
|
||||
* </file>
|
||||
* <file name="root.html">
|
||||
* <strong>myRoot</strong>
|
||||
* <my-toggle ng-ref="$ctrl.outerToggle">Outer Toggle</my-toggle>
|
||||
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
|
||||
* <div><em>wrappers assigned to root</em><br>
|
||||
* <div ng-repeat="wrapper in $ctrl.wrappers">
|
||||
* wrapper.ngRepeatToggle.isOpen(): {{wrapper.ngRepeatToggle.isOpen() | json}}
|
||||
* </div>
|
||||
*
|
||||
* <ul>
|
||||
* <li ng-repeat="(index, value) in [1,2,3]">
|
||||
* <strong>ngRepeat</strong>
|
||||
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
|
||||
* <my-wrapper ng-ref="$ctrl.wrappers[index]">ngRepeat Toggle {{$index + 1}}</my-wrapper>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen()}} // This is always undefined because it's
|
||||
* assigned to the child scope created by ngIf.
|
||||
* </div>
|
||||
* <div ng-if="true">
|
||||
<strong>ngIf</strong>
|
||||
* <my-toggle ng-ref="ngIfToggle">ngIf Toggle</my-toggle>
|
||||
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}</div>
|
||||
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="styles.css">
|
||||
* ul {
|
||||
* list-style: none;
|
||||
* padding-left: 0;
|
||||
* }
|
||||
*
|
||||
* li[ng-repeat] {
|
||||
* background: lightgreen;
|
||||
* padding: 8px;
|
||||
* margin: 8px;
|
||||
* }
|
||||
*
|
||||
* [ng-if] {
|
||||
* background: lightgrey;
|
||||
* padding: 8px;
|
||||
* }
|
||||
*
|
||||
* my-root {
|
||||
* background: lightgoldenrodyellow;
|
||||
* padding: 8px;
|
||||
* display: block;
|
||||
* }
|
||||
*
|
||||
* my-wrapper {
|
||||
* background: lightsalmon;
|
||||
* padding: 8px;
|
||||
* display: block;
|
||||
* }
|
||||
*
|
||||
* my-toggle {
|
||||
* background: lightblue;
|
||||
* padding: 8px;
|
||||
* display: block;
|
||||
* }
|
||||
* </file>
|
||||
* <file name="protractor.js" type="protractor">
|
||||
* var OuterToggle = function() {
|
||||
* this.toggle = function() {
|
||||
* element(by.buttonText('Outer Toggle')).click();
|
||||
* };
|
||||
* this.isOpen = function() {
|
||||
* return element.all(by.binding('outerToggle.isOpen()')).first().getText();
|
||||
* };
|
||||
* };
|
||||
* var NgRepeatToggle = function(i) {
|
||||
* var parent = element.all(by.repeater('(index, value) in [1,2,3]')).get(i - 1);
|
||||
* this.toggle = function() {
|
||||
* element(by.buttonText('ngRepeat Toggle ' + i)).click();
|
||||
* };
|
||||
* this.isOpen = function() {
|
||||
* return parent.element(by.binding('ngRepeatToggle.isOpen() | json')).getText();
|
||||
* };
|
||||
* this.isOuterOpen = function() {
|
||||
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
|
||||
* };
|
||||
* };
|
||||
* var NgRepeatToggles = function() {
|
||||
* var toggles = [1,2,3].map(function(i) { return new NgRepeatToggle(i); });
|
||||
* this.forEach = function(fn) {
|
||||
* toggles.forEach(fn);
|
||||
* };
|
||||
* this.isOuterOpen = function(i) {
|
||||
* return toggles[i - 1].isOuterOpen();
|
||||
* };
|
||||
* };
|
||||
* var NgIfToggle = function() {
|
||||
* var parent = element(by.css('[ng-if]'));
|
||||
* this.toggle = function() {
|
||||
* element(by.buttonText('ngIf Toggle')).click();
|
||||
* };
|
||||
* this.isOpen = function() {
|
||||
* return by.binding('ngIfToggle.isOpen() | json').getText();
|
||||
* };
|
||||
* this.isOuterOpen = function() {
|
||||
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
|
||||
* };
|
||||
* };
|
||||
*
|
||||
* it('should toggle the outer toggle', function() {
|
||||
* var outerToggle = new OuterToggle();
|
||||
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
|
||||
* outerToggle.toggle();
|
||||
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
|
||||
* });
|
||||
*
|
||||
* it('should toggle all outer toggles', function() {
|
||||
* var outerToggle = new OuterToggle();
|
||||
* var repeatToggles = new NgRepeatToggles();
|
||||
* var ifToggle = new NgIfToggle();
|
||||
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
|
||||
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): false');
|
||||
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): false');
|
||||
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): false');
|
||||
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
|
||||
* outerToggle.toggle();
|
||||
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
|
||||
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): true');
|
||||
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): true');
|
||||
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): true');
|
||||
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): true');
|
||||
* });
|
||||
*
|
||||
* it('should toggle each repeat iteration separately', function() {
|
||||
* var repeatToggles = new NgRepeatToggles();
|
||||
*
|
||||
* repeatToggles.forEach(function(repeatToggle) {
|
||||
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): false');
|
||||
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
|
||||
* repeatToggle.toggle();
|
||||
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): true');
|
||||
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
|
||||
* });
|
||||
* });
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
*/
|
||||
|
||||
var ngRefMinErr = minErr('ngRef');
|
||||
|
||||
var ngRefDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
priority: -1, // Needed for compatibility with element transclusion on the same element
|
||||
restrict: 'A',
|
||||
compile: function(tElement, tAttrs) {
|
||||
// Get the expected controller name, converts <data-some-thing> into "someThing"
|
||||
var controllerName = directiveNormalize(nodeName_(tElement));
|
||||
|
||||
// Get the expression for value binding
|
||||
var getter = $parse(tAttrs.ngRef);
|
||||
var setter = getter.assign || function() {
|
||||
throw ngRefMinErr('nonassign', 'Expression in ngRef="{0}" is non-assignable!', tAttrs.ngRef);
|
||||
};
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
var refValue;
|
||||
|
||||
if (attrs.hasOwnProperty('ngRefRead')) {
|
||||
if (attrs.ngRefRead === '$element') {
|
||||
refValue = element;
|
||||
} else {
|
||||
refValue = element.data('$' + attrs.ngRefRead + 'Controller');
|
||||
|
||||
if (!refValue) {
|
||||
throw ngRefMinErr(
|
||||
'noctrl',
|
||||
'The controller for ngRefRead="{0}" could not be found on ngRef="{1}"',
|
||||
attrs.ngRefRead,
|
||||
tAttrs.ngRef
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
refValue = element.data('$' + controllerName + 'Controller');
|
||||
}
|
||||
|
||||
refValue = refValue || element;
|
||||
|
||||
setter(scope, refValue);
|
||||
|
||||
// when the element is removed, remove it (nullify it)
|
||||
element.on('$destroy', function() {
|
||||
// only remove it if value has not changed,
|
||||
// because animations (and other procedures) may duplicate elements
|
||||
if (getter(scope) === refValue) {
|
||||
setter(scope, null);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}];
|
||||
+2
-2
@@ -1054,7 +1054,7 @@ function $HttpProvider() {
|
||||
config.paramSerializer = isString(config.paramSerializer) ?
|
||||
$injector.get(config.paramSerializer) : config.paramSerializer;
|
||||
|
||||
$browser.$$incOutstandingRequestCount();
|
||||
$browser.$$incOutstandingRequestCount('$http');
|
||||
|
||||
var requestInterceptors = [];
|
||||
var responseInterceptors = [];
|
||||
@@ -1092,7 +1092,7 @@ function $HttpProvider() {
|
||||
}
|
||||
|
||||
function completeOutstandingRequest() {
|
||||
$browser.$$completeOutstandingRequest(noop);
|
||||
$browser.$$completeOutstandingRequest(noop, '$http');
|
||||
}
|
||||
|
||||
function executeHeaderFns(headers, config) {
|
||||
|
||||
+13
-48
@@ -4,10 +4,18 @@ var $intervalMinErr = minErr('$interval');
|
||||
|
||||
/** @this */
|
||||
function $IntervalProvider() {
|
||||
this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
|
||||
function($rootScope, $window, $q, $$q, $browser) {
|
||||
this.$get = ['$$intervalFactory', '$window',
|
||||
function($$intervalFactory, $window) {
|
||||
var intervals = {};
|
||||
|
||||
var setIntervalFn = function(tick, delay, deferred) {
|
||||
var id = $window.setInterval(tick, delay);
|
||||
intervals[id] = deferred;
|
||||
return id;
|
||||
};
|
||||
var clearIntervalFn = function(id) {
|
||||
$window.clearInterval(id);
|
||||
delete intervals[id];
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
@@ -135,49 +143,7 @@ function $IntervalProvider() {
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
function interval(fn, delay, count, invokeApply) {
|
||||
var hasParams = arguments.length > 4,
|
||||
args = hasParams ? sliceArgs(arguments, 4) : [],
|
||||
setInterval = $window.setInterval,
|
||||
clearInterval = $window.clearInterval,
|
||||
iteration = 0,
|
||||
skipApply = (isDefined(invokeApply) && !invokeApply),
|
||||
deferred = (skipApply ? $$q : $q).defer(),
|
||||
promise = deferred.promise;
|
||||
|
||||
count = isDefined(count) ? count : 0;
|
||||
|
||||
promise.$$intervalId = setInterval(function tick() {
|
||||
if (skipApply) {
|
||||
$browser.defer(callback);
|
||||
} else {
|
||||
$rootScope.$evalAsync(callback);
|
||||
}
|
||||
deferred.notify(iteration++);
|
||||
|
||||
if (count > 0 && iteration >= count) {
|
||||
deferred.resolve(iteration);
|
||||
clearInterval(promise.$$intervalId);
|
||||
delete intervals[promise.$$intervalId];
|
||||
}
|
||||
|
||||
if (!skipApply) $rootScope.$apply();
|
||||
|
||||
}, delay);
|
||||
|
||||
intervals[promise.$$intervalId] = deferred;
|
||||
|
||||
return promise;
|
||||
|
||||
function callback() {
|
||||
if (!hasParams) {
|
||||
fn(iteration);
|
||||
} else {
|
||||
fn.apply(null, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var interval = $$intervalFactory(setIntervalFn, clearIntervalFn);
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
@@ -205,8 +171,7 @@ function $IntervalProvider() {
|
||||
// Interval cancels should not report an unhandled promise.
|
||||
markQExceptionHandled(deferred.promise);
|
||||
deferred.reject('canceled');
|
||||
$window.clearInterval(id);
|
||||
delete intervals[id];
|
||||
clearIntervalFn(id);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
/** @this */
|
||||
function $$IntervalFactoryProvider() {
|
||||
this.$get = ['$browser', '$q', '$$q', '$rootScope',
|
||||
function($browser, $q, $$q, $rootScope) {
|
||||
return function intervalFactory(setIntervalFn, clearIntervalFn) {
|
||||
return function intervalFn(fn, delay, count, invokeApply) {
|
||||
var hasParams = arguments.length > 4,
|
||||
args = hasParams ? sliceArgs(arguments, 4) : [],
|
||||
iteration = 0,
|
||||
skipApply = isDefined(invokeApply) && !invokeApply,
|
||||
deferred = (skipApply ? $$q : $q).defer(),
|
||||
promise = deferred.promise;
|
||||
|
||||
count = isDefined(count) ? count : 0;
|
||||
|
||||
function callback() {
|
||||
if (!hasParams) {
|
||||
fn(iteration);
|
||||
} else {
|
||||
fn.apply(null, args);
|
||||
}
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (skipApply) {
|
||||
$browser.defer(callback);
|
||||
} else {
|
||||
$rootScope.$evalAsync(callback);
|
||||
}
|
||||
deferred.notify(iteration++);
|
||||
|
||||
if (count > 0 && iteration >= count) {
|
||||
deferred.resolve(iteration);
|
||||
clearIntervalFn(promise.$$intervalId);
|
||||
}
|
||||
|
||||
if (!skipApply) $rootScope.$apply();
|
||||
}
|
||||
|
||||
promise.$$intervalId = setIntervalFn(tick, delay, deferred, skipApply);
|
||||
|
||||
return promise;
|
||||
};
|
||||
};
|
||||
}];
|
||||
}
|
||||
+36
-44
@@ -1,4 +1,5 @@
|
||||
'use strict';
|
||||
/* global stripHash: true */
|
||||
|
||||
var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
|
||||
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
|
||||
@@ -38,6 +39,14 @@ function decodePath(path, html5Mode) {
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
function normalizePath(pathValue, searchValue, hashValue) {
|
||||
var search = toKeyValue(searchValue),
|
||||
hash = hashValue ? '#' + encodeUriSegment(hashValue) : '',
|
||||
path = encodePath(pathValue);
|
||||
|
||||
return path + (search ? '?' + search : '') + hash;
|
||||
}
|
||||
|
||||
function parseAbsoluteUrl(absoluteUrl, locationObj) {
|
||||
var parsedUrl = urlResolve(absoluteUrl);
|
||||
|
||||
@@ -86,17 +95,11 @@ function stripBaseUrl(base, url) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function stripHash(url) {
|
||||
var index = url.indexOf('#');
|
||||
return index === -1 ? url : url.substr(0, index);
|
||||
}
|
||||
|
||||
function trimEmptyHash(url) {
|
||||
return url.replace(/(#.+)|#$/, '$1');
|
||||
}
|
||||
|
||||
|
||||
function stripFile(url) {
|
||||
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
|
||||
}
|
||||
@@ -143,18 +146,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
|
||||
this.$$compose();
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose url and update `absUrl` property
|
||||
* @private
|
||||
*/
|
||||
this.$$compose = function() {
|
||||
var search = toKeyValue(this.$$search),
|
||||
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
|
||||
|
||||
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
|
||||
this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
|
||||
|
||||
this.$$urlUpdatedByLocation = true;
|
||||
this.$$normalizeUrl = function(url) {
|
||||
return appBaseNoFile + url.substr(1); // first char is always '/'
|
||||
};
|
||||
|
||||
this.$$parseLinkUrl = function(url, relHref) {
|
||||
@@ -278,18 +271,8 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose hashbang URL and update `absUrl` property
|
||||
* @private
|
||||
*/
|
||||
this.$$compose = function() {
|
||||
var search = toKeyValue(this.$$search),
|
||||
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
|
||||
|
||||
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
|
||||
this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
|
||||
|
||||
this.$$urlUpdatedByLocation = true;
|
||||
this.$$normalizeUrl = function(url) {
|
||||
return appBase + (url ? hashPrefix + url : '');
|
||||
};
|
||||
|
||||
this.$$parseLinkUrl = function(url, relHref) {
|
||||
@@ -340,17 +323,10 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
|
||||
return !!rewrittenUrl;
|
||||
};
|
||||
|
||||
this.$$compose = function() {
|
||||
var search = toKeyValue(this.$$search),
|
||||
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
|
||||
|
||||
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
|
||||
this.$$normalizeUrl = function(url) {
|
||||
// include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#'
|
||||
this.$$absUrl = appBase + hashPrefix + this.$$url;
|
||||
|
||||
this.$$urlUpdatedByLocation = true;
|
||||
return appBase + hashPrefix + url;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -374,6 +350,16 @@ var locationPrototype = {
|
||||
*/
|
||||
$$replace: false,
|
||||
|
||||
/**
|
||||
* Compose url and update `url` and `absUrl` property
|
||||
* @private
|
||||
*/
|
||||
$$compose: function() {
|
||||
this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash);
|
||||
this.$$absUrl = this.$$normalizeUrl(this.$$url);
|
||||
this.$$urlUpdatedByLocation = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $location#absUrl
|
||||
@@ -879,6 +865,13 @@ function $LocationProvider() {
|
||||
|
||||
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
|
||||
|
||||
// Determine if two URLs are equal despite potentially having different encoding/normalizing
|
||||
// such as $location.absUrl() vs $browser.url()
|
||||
// See https://github.com/angular/angular.js/issues/16592
|
||||
function urlsEqual(a, b) {
|
||||
return a === b || urlResolve(a).href === urlResolve(b).href;
|
||||
}
|
||||
|
||||
function setBrowserUrlWithFallback(url, replace, state) {
|
||||
var oldUrl = $location.url();
|
||||
var oldState = $location.$$state;
|
||||
@@ -945,7 +938,7 @@ function $LocationProvider() {
|
||||
|
||||
|
||||
// rewrite hashbang url <> html5 url
|
||||
if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) {
|
||||
if ($location.absUrl() !== initialUrl) {
|
||||
$browser.url($location.absUrl(), true);
|
||||
}
|
||||
|
||||
@@ -964,7 +957,6 @@ function $LocationProvider() {
|
||||
var oldUrl = $location.absUrl();
|
||||
var oldState = $location.$$state;
|
||||
var defaultPrevented;
|
||||
newUrl = trimEmptyHash(newUrl);
|
||||
$location.$$parse(newUrl);
|
||||
$location.$$state = newState;
|
||||
|
||||
@@ -992,11 +984,11 @@ function $LocationProvider() {
|
||||
if (initializing || $location.$$urlUpdatedByLocation) {
|
||||
$location.$$urlUpdatedByLocation = false;
|
||||
|
||||
var oldUrl = trimEmptyHash($browser.url());
|
||||
var newUrl = trimEmptyHash($location.absUrl());
|
||||
var oldUrl = $browser.url();
|
||||
var newUrl = $location.absUrl();
|
||||
var oldState = $browser.state();
|
||||
var currentReplace = $location.$$replace;
|
||||
var urlOrStateChanged = oldUrl !== newUrl ||
|
||||
var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) ||
|
||||
($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
|
||||
|
||||
if (initializing || urlOrStateChanged) {
|
||||
|
||||
+2
-2
@@ -1122,7 +1122,7 @@ function $RootScopeProvider() {
|
||||
if (asyncQueue.length) {
|
||||
$rootScope.$digest();
|
||||
}
|
||||
});
|
||||
}, null, '$evalAsync');
|
||||
}
|
||||
|
||||
asyncQueue.push({scope: this, fn: $parse(expr), locals: locals});
|
||||
@@ -1493,7 +1493,7 @@ function $RootScopeProvider() {
|
||||
if (applyAsyncId === null) {
|
||||
applyAsyncId = $browser.defer(function() {
|
||||
$rootScope.$apply(flushApplyAsync);
|
||||
});
|
||||
}, null, '$applyAsync');
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
+1
-1
@@ -623,7 +623,7 @@ function $SceDelegateProvider() {
|
||||
* | `$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.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. <br><br> The {@link $sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider#resourceUrlWhitelist()} and {@link $sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider#resourceUrlBlacklist()} can be used to restrict trusted origins for `RESOURCE_URL` |
|
||||
* | `$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. |
|
||||
*
|
||||
*
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ! This is a private undocumented service !
|
||||
*
|
||||
* @name $$taskTrackerFactory
|
||||
* @description
|
||||
* A function to create `TaskTracker` instances.
|
||||
*
|
||||
* A `TaskTracker` can keep track of pending tasks (grouped by type) and can notify interested
|
||||
* parties when all pending tasks (or tasks of a specific type) have been completed.
|
||||
*
|
||||
* @param {$log} log - A logger instance (such as `$log`). Used to log error during callback
|
||||
* execution.
|
||||
*
|
||||
* @this
|
||||
*/
|
||||
function $$TaskTrackerFactoryProvider() {
|
||||
this.$get = valueFn(function(log) { return new TaskTracker(log); });
|
||||
}
|
||||
|
||||
function TaskTracker(log) {
|
||||
var self = this;
|
||||
var taskCounts = {};
|
||||
var taskCallbacks = [];
|
||||
|
||||
var ALL_TASKS_TYPE = self.ALL_TASKS_TYPE = '$$all$$';
|
||||
var DEFAULT_TASK_TYPE = self.DEFAULT_TASK_TYPE = '$$default$$';
|
||||
|
||||
/**
|
||||
* Execute the specified function and decrement the appropriate `taskCounts` counter.
|
||||
* If the counter reaches 0, all corresponding `taskCallbacks` are executed.
|
||||
*
|
||||
* @param {Function} fn - The function to execute.
|
||||
* @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task that is being completed.
|
||||
*/
|
||||
self.completeTask = completeTask;
|
||||
|
||||
/**
|
||||
* Increase the task count for the specified task type (or the default task type if non is
|
||||
* specified).
|
||||
*
|
||||
* @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task whose count will be increased.
|
||||
*/
|
||||
self.incTaskCount = incTaskCount;
|
||||
|
||||
/**
|
||||
* Execute the specified callback when all pending tasks have been completed.
|
||||
*
|
||||
* If there are no pending tasks, the callback is executed immediately. You can optionally limit
|
||||
* the tasks that will be waited for to a specific type, by passing a `taskType`.
|
||||
*
|
||||
* @param {function} callback - The function to call when there are no pending tasks.
|
||||
* @param {string=} [taskType=ALL_TASKS_TYPE] - The type of tasks that will be waited for.
|
||||
*/
|
||||
self.notifyWhenNoPendingTasks = notifyWhenNoPendingTasks;
|
||||
|
||||
function completeTask(fn, taskType) {
|
||||
taskType = taskType || DEFAULT_TASK_TYPE;
|
||||
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
decTaskCount(taskType);
|
||||
|
||||
var countForType = taskCounts[taskType];
|
||||
var countForAll = taskCounts[ALL_TASKS_TYPE];
|
||||
|
||||
// If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks.
|
||||
if (!countForAll || !countForType) {
|
||||
var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType;
|
||||
var nextCb;
|
||||
|
||||
while ((nextCb = getNextCallback(taskType))) {
|
||||
try {
|
||||
nextCb();
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decTaskCount(taskType) {
|
||||
taskType = taskType || DEFAULT_TASK_TYPE;
|
||||
if (taskCounts[taskType]) {
|
||||
taskCounts[taskType]--;
|
||||
taskCounts[ALL_TASKS_TYPE]--;
|
||||
}
|
||||
}
|
||||
|
||||
function getLastCallback() {
|
||||
var cbInfo = taskCallbacks.pop();
|
||||
return cbInfo && cbInfo.cb;
|
||||
}
|
||||
|
||||
function getLastCallbackForType(taskType) {
|
||||
for (var i = taskCallbacks.length - 1; i >= 0; --i) {
|
||||
var cbInfo = taskCallbacks[i];
|
||||
if (cbInfo.type === taskType) {
|
||||
taskCallbacks.splice(i, 1);
|
||||
return cbInfo.cb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function incTaskCount(taskType) {
|
||||
taskType = taskType || DEFAULT_TASK_TYPE;
|
||||
taskCounts[taskType] = (taskCounts[taskType] || 0) + 1;
|
||||
taskCounts[ALL_TASKS_TYPE] = (taskCounts[ALL_TASKS_TYPE] || 0) + 1;
|
||||
}
|
||||
|
||||
function notifyWhenNoPendingTasks(callback, taskType) {
|
||||
taskType = taskType || ALL_TASKS_TYPE;
|
||||
if (!taskCounts[taskType]) {
|
||||
callback();
|
||||
} else {
|
||||
taskCallbacks.push({type: taskType, cb: callback});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,15 @@ function $$TestabilityProvider() {
|
||||
* @name $$testability#whenStable
|
||||
*
|
||||
* @description
|
||||
* Calls the callback when $timeout and $http requests are completed.
|
||||
* Calls the callback when all pending tasks are completed.
|
||||
*
|
||||
* Types of tasks waited for include:
|
||||
* - Pending timeouts (via {@link $timeout}).
|
||||
* - Pending HTTP requests (via {@link $http}).
|
||||
* - In-progress route transitions (via {@link $route}).
|
||||
* - Pending tasks scheduled via {@link $rootScope#$applyAsync}.
|
||||
* - Pending tasks scheduled via {@link $rootScope#$evalAsync}.
|
||||
* These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
|
||||
*
|
||||
* @param {function} callback
|
||||
*/
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ function $TimeoutProvider() {
|
||||
}
|
||||
|
||||
if (!skipApply) $rootScope.$apply();
|
||||
}, delay);
|
||||
}, delay, '$timeout');
|
||||
|
||||
promise.$$timeoutId = timeoutId;
|
||||
deferreds[timeoutId] = deferred;
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
/* ngAnimate directives/services */
|
||||
"ngAnimateSwapDirective": true,
|
||||
"$$rAFSchedulerFactory": true,
|
||||
"$$AnimateCacheProvider": true,
|
||||
"$$AnimateChildrenDirective": true,
|
||||
"$$AnimateQueueProvider": true,
|
||||
"$$AnimationProvider": true,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
/** @this */
|
||||
var $$AnimateCacheProvider = function() {
|
||||
|
||||
var KEY = '$$ngAnimateParentKey';
|
||||
var parentCounter = 0;
|
||||
var cache = Object.create(null);
|
||||
|
||||
this.$get = [function() {
|
||||
return {
|
||||
cacheKey: function(node, method, addClass, removeClass) {
|
||||
var parentNode = node.parentNode;
|
||||
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
|
||||
var parts = [parentID, method, node.getAttribute('class')];
|
||||
if (addClass) {
|
||||
parts.push(addClass);
|
||||
}
|
||||
if (removeClass) {
|
||||
parts.push(removeClass);
|
||||
}
|
||||
return parts.join(' ');
|
||||
},
|
||||
|
||||
containsCachedAnimationWithoutDuration: function(key) {
|
||||
var entry = cache[key];
|
||||
|
||||
// nothing cached, so go ahead and animate
|
||||
// otherwise it should be a valid animation
|
||||
return (entry && !entry.isValid) || false;
|
||||
},
|
||||
|
||||
flush: function() {
|
||||
cache = Object.create(null);
|
||||
},
|
||||
|
||||
count: function(key) {
|
||||
var entry = cache[key];
|
||||
return entry ? entry.total : 0;
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
var entry = cache[key];
|
||||
return entry && entry.value;
|
||||
},
|
||||
|
||||
put: function(key, value, isValid) {
|
||||
if (!cache[key]) {
|
||||
cache[key] = { total: 1, value: value, isValid: isValid };
|
||||
} else {
|
||||
cache[key].total++;
|
||||
cache[key].value = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
};
|
||||
+34
-58
@@ -304,33 +304,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
|
||||
return [style, value];
|
||||
}
|
||||
|
||||
function createLocalCacheLookup() {
|
||||
var cache = Object.create(null);
|
||||
return {
|
||||
flush: function() {
|
||||
cache = Object.create(null);
|
||||
},
|
||||
|
||||
count: function(key) {
|
||||
var entry = cache[key];
|
||||
return entry ? entry.total : 0;
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
var entry = cache[key];
|
||||
return entry && entry.value;
|
||||
},
|
||||
|
||||
put: function(key, value) {
|
||||
if (!cache[key]) {
|
||||
cache[key] = { total: 1, value: value };
|
||||
} else {
|
||||
cache[key].total++;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// we do not reassign an already present style value since
|
||||
// if we detect the style property value again we may be
|
||||
// detecting styles that were added via the `from` styles.
|
||||
@@ -349,26 +322,16 @@ function registerRestorableStyles(backup, node, properties) {
|
||||
}
|
||||
|
||||
var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) {
|
||||
var gcsLookup = createLocalCacheLookup();
|
||||
var gcsStaggerLookup = createLocalCacheLookup();
|
||||
|
||||
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
|
||||
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$animateCache',
|
||||
'$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue',
|
||||
function($window, $$jqLite, $$AnimateRunner, $timeout,
|
||||
function($window, $$jqLite, $$AnimateRunner, $timeout, $$animateCache,
|
||||
$$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) {
|
||||
|
||||
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
|
||||
|
||||
var parentCounter = 0;
|
||||
function gcsHashFn(node, extraClasses) {
|
||||
var KEY = '$$ngAnimateParentKey';
|
||||
var parentNode = node.parentNode;
|
||||
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
|
||||
return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
|
||||
}
|
||||
|
||||
function computeCachedCssStyles(node, className, cacheKey, properties) {
|
||||
var timings = gcsLookup.get(cacheKey);
|
||||
function computeCachedCssStyles(node, className, cacheKey, allowNoDuration, properties) {
|
||||
var timings = $$animateCache.get(cacheKey);
|
||||
|
||||
if (!timings) {
|
||||
timings = computeCssStyles($window, node, properties);
|
||||
@@ -377,20 +340,26 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
}
|
||||
}
|
||||
|
||||
// if a css animation has no duration we
|
||||
// should mark that so that repeated addClass/removeClass calls are skipped
|
||||
var hasDuration = allowNoDuration || (timings.transitionDuration > 0 || timings.animationDuration > 0);
|
||||
|
||||
// we keep putting this in multiple times even though the value and the cacheKey are the same
|
||||
// because we're keeping an internal tally of how many duplicate animations are detected.
|
||||
gcsLookup.put(cacheKey, timings);
|
||||
$$animateCache.put(cacheKey, timings, hasDuration);
|
||||
|
||||
return timings;
|
||||
}
|
||||
|
||||
function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
|
||||
var stagger;
|
||||
var staggerCacheKey = 'stagger-' + cacheKey;
|
||||
|
||||
// if we have one or more existing matches of matching elements
|
||||
// containing the same parent + CSS styles (which is how cacheKey works)
|
||||
// then staggering is possible
|
||||
if (gcsLookup.count(cacheKey) > 0) {
|
||||
stagger = gcsStaggerLookup.get(cacheKey);
|
||||
if ($$animateCache.count(cacheKey) > 0) {
|
||||
stagger = $$animateCache.get(staggerCacheKey);
|
||||
|
||||
if (!stagger) {
|
||||
var staggerClassName = pendClasses(className, '-stagger');
|
||||
@@ -405,7 +374,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
|
||||
$$jqLite.removeClass(node, staggerClassName);
|
||||
|
||||
gcsStaggerLookup.put(cacheKey, stagger);
|
||||
$$animateCache.put(staggerCacheKey, stagger, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,8 +385,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
function waitUntilQuiet(callback) {
|
||||
rafWaitQueue.push(callback);
|
||||
$$rAFScheduler.waitUntilQuiet(function() {
|
||||
gcsLookup.flush();
|
||||
gcsStaggerLookup.flush();
|
||||
$$animateCache.flush();
|
||||
|
||||
// DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
|
||||
// PLEASE EXAMINE THE `$$forceReflow` service to understand why.
|
||||
@@ -432,8 +400,8 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
});
|
||||
}
|
||||
|
||||
function computeTimings(node, className, cacheKey) {
|
||||
var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
|
||||
function computeTimings(node, className, cacheKey, allowNoDuration) {
|
||||
var timings = computeCachedCssStyles(node, className, cacheKey, allowNoDuration, DETECT_CSS_PROPERTIES);
|
||||
var aD = timings.animationDelay;
|
||||
var tD = timings.transitionDelay;
|
||||
timings.maxDelay = aD && tD
|
||||
@@ -520,7 +488,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
|
||||
var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
|
||||
var fullClassName = classes + ' ' + preparationClasses;
|
||||
var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
|
||||
var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
|
||||
var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;
|
||||
|
||||
@@ -533,7 +500,12 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
return closeAndReturnNoopAnimator();
|
||||
}
|
||||
|
||||
var cacheKey, stagger;
|
||||
var stagger, cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
|
||||
if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
|
||||
preparationClasses = null;
|
||||
return closeAndReturnNoopAnimator();
|
||||
}
|
||||
|
||||
if (options.stagger > 0) {
|
||||
var staggerVal = parseFloat(options.stagger);
|
||||
stagger = {
|
||||
@@ -543,7 +515,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
animationDuration: 0
|
||||
};
|
||||
} else {
|
||||
cacheKey = gcsHashFn(node, fullClassName);
|
||||
stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
|
||||
}
|
||||
|
||||
@@ -577,7 +548,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
var itemIndex = stagger
|
||||
? options.staggerIndex >= 0
|
||||
? options.staggerIndex
|
||||
: gcsLookup.count(cacheKey)
|
||||
: $$animateCache.count(cacheKey)
|
||||
: 0;
|
||||
|
||||
var isFirst = itemIndex === 0;
|
||||
@@ -592,7 +563,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
|
||||
}
|
||||
|
||||
var timings = computeTimings(node, fullClassName, cacheKey);
|
||||
var timings = computeTimings(node, fullClassName, cacheKey, !isStructural);
|
||||
var relativeDelay = timings.maxDelay;
|
||||
maxDelay = Math.max(relativeDelay, 0);
|
||||
maxDuration = timings.maxDuration;
|
||||
@@ -630,6 +601,8 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
return closeAndReturnNoopAnimator();
|
||||
}
|
||||
|
||||
var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
|
||||
|
||||
if (options.delay != null) {
|
||||
var delayStyle;
|
||||
if (typeof options.delay !== 'boolean') {
|
||||
@@ -717,10 +690,13 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
animationClosed = true;
|
||||
animationPaused = false;
|
||||
|
||||
if (!options.$$skipPreparationClasses) {
|
||||
if (preparationClasses && !options.$$skipPreparationClasses) {
|
||||
$$jqLite.removeClass(element, preparationClasses);
|
||||
}
|
||||
$$jqLite.removeClass(element, activeClasses);
|
||||
|
||||
if (activeClasses) {
|
||||
$$jqLite.removeClass(element, activeClasses);
|
||||
}
|
||||
|
||||
blockKeyframeAnimations(node, false);
|
||||
blockTransitions(node, false);
|
||||
@@ -904,9 +880,9 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
|
||||
if (flags.recalculateTimingStyles) {
|
||||
fullClassName = node.getAttribute('class') + ' ' + preparationClasses;
|
||||
cacheKey = gcsHashFn(node, fullClassName);
|
||||
cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
|
||||
|
||||
timings = computeTimings(node, fullClassName, cacheKey);
|
||||
timings = computeTimings(node, fullClassName, cacheKey, false);
|
||||
relativeDelay = timings.maxDelay;
|
||||
maxDelay = Math.max(relativeDelay, 0);
|
||||
maxDuration = timings.maxDuration;
|
||||
|
||||
@@ -13,6 +13,15 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
join: []
|
||||
};
|
||||
|
||||
function getEventData(options) {
|
||||
return {
|
||||
addClass: options.addClass,
|
||||
removeClass: options.removeClass,
|
||||
from: options.from,
|
||||
to: options.to
|
||||
};
|
||||
}
|
||||
|
||||
function makeTruthyCssClassMap(classString) {
|
||||
if (!classString) {
|
||||
return null;
|
||||
@@ -111,6 +120,10 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
var disabledElementsLookup = new $$Map();
|
||||
var animationsEnabled = null;
|
||||
|
||||
function removeFromDisabledElementsLookup(evt) {
|
||||
disabledElementsLookup.delete(evt.target);
|
||||
}
|
||||
|
||||
function postDigestTaskFactory() {
|
||||
var postDigestCalled = false;
|
||||
return function(fn) {
|
||||
@@ -294,6 +307,11 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
bool = !disabledElementsLookup.get(node);
|
||||
} else {
|
||||
// (element, bool) - Element setter
|
||||
if (!disabledElementsLookup.has(node)) {
|
||||
// The element is added to the map for the first time.
|
||||
// Create a listener to remove it on `$destroy` (to avoid memory leak).
|
||||
jqLite(element).on('$destroy', removeFromDisabledElementsLookup);
|
||||
}
|
||||
disabledElementsLookup.set(node, !bool);
|
||||
}
|
||||
}
|
||||
@@ -379,9 +397,9 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
|
||||
if (skipAnimations) {
|
||||
// Callbacks should fire even if the document is hidden (regression fix for issue #14120)
|
||||
if (documentHidden) notifyProgress(runner, event, 'start');
|
||||
if (documentHidden) notifyProgress(runner, event, 'start', getEventData(options));
|
||||
close();
|
||||
if (documentHidden) notifyProgress(runner, event, 'close');
|
||||
if (documentHidden) notifyProgress(runner, event, 'close', getEventData(options));
|
||||
return runner;
|
||||
}
|
||||
|
||||
@@ -438,7 +456,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
if (existingAnimation.state === RUNNING_STATE) {
|
||||
normalizeAnimationDetails(element, newAnimation);
|
||||
} else {
|
||||
applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
|
||||
applyGeneratedPreparationClasses($$jqLite, element, isStructural ? event : null, options);
|
||||
|
||||
event = newAnimation.event = existingAnimation.event;
|
||||
options = mergeAnimationDetails(element, existingAnimation, newAnimation);
|
||||
@@ -543,7 +561,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
// this will update the runner's flow-control events based on
|
||||
// the `realRunner` object.
|
||||
runner.setHost(realRunner);
|
||||
notifyProgress(runner, event, 'start', {});
|
||||
notifyProgress(runner, event, 'start', getEventData(options));
|
||||
|
||||
realRunner.done(function(status) {
|
||||
close(!status);
|
||||
@@ -551,7 +569,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
|
||||
if (animationDetails && animationDetails.counter === counter) {
|
||||
clearElementAnimationState(node);
|
||||
}
|
||||
notifyProgress(runner, event, 'close', {});
|
||||
notifyProgress(runner, event, 'close', getEventData(options));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+57
-17
@@ -8,6 +8,7 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
var drivers = this.drivers = [];
|
||||
|
||||
var RUNNER_STORAGE_KEY = '$$animationRunner';
|
||||
var PREPARE_CLASSES_KEY = '$$animatePrepareClasses';
|
||||
|
||||
function setRunner(element, runner) {
|
||||
element.data(RUNNER_STORAGE_KEY, runner);
|
||||
@@ -21,8 +22,8 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
return element.data(RUNNER_STORAGE_KEY);
|
||||
}
|
||||
|
||||
this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler',
|
||||
function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler) {
|
||||
this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler', '$$animateCache',
|
||||
function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler, $$animateCache) {
|
||||
|
||||
var animationQueue = [];
|
||||
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
|
||||
@@ -37,6 +38,7 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
var animation = animations[i];
|
||||
lookup.set(animation.domNode, animations[i] = {
|
||||
domNode: animation.domNode,
|
||||
element: animation.element,
|
||||
fn: animation.fn,
|
||||
children: []
|
||||
});
|
||||
@@ -93,7 +95,7 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
result.push(row);
|
||||
row = [];
|
||||
}
|
||||
row.push(entry.fn);
|
||||
row.push(entry);
|
||||
entry.children.forEach(function(childEntry) {
|
||||
nextLevelEntries++;
|
||||
queue.push(childEntry);
|
||||
@@ -111,6 +113,8 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
|
||||
// TODO(matsko): document the signature in a better way
|
||||
return function(element, event, options) {
|
||||
var node = getDomNode(element);
|
||||
|
||||
options = prepareAnimationOptions(options);
|
||||
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
|
||||
|
||||
@@ -128,8 +132,6 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
return runner;
|
||||
}
|
||||
|
||||
setRunner(element, runner);
|
||||
|
||||
var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass));
|
||||
var tempClasses = options.tempClasses;
|
||||
if (tempClasses) {
|
||||
@@ -137,12 +139,12 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
options.tempClasses = null;
|
||||
}
|
||||
|
||||
var prepareClassName;
|
||||
if (isStructural) {
|
||||
prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX;
|
||||
$$jqLite.addClass(element, prepareClassName);
|
||||
element.data(PREPARE_CLASSES_KEY, 'ng-' + event + PREPARE_CLASS_SUFFIX);
|
||||
}
|
||||
|
||||
setRunner(element, runner);
|
||||
|
||||
animationQueue.push({
|
||||
// this data is used by the postDigest code and passed into
|
||||
// the driver step function
|
||||
@@ -182,16 +184,30 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
var toBeSortedAnimations = [];
|
||||
|
||||
forEach(groupedAnimations, function(animationEntry) {
|
||||
var element = animationEntry.from ? animationEntry.from.element : animationEntry.element;
|
||||
var extraClasses = options.addClass;
|
||||
extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
|
||||
var cacheKey = $$animateCache.cacheKey(node, event, extraClasses, options.removeClass);
|
||||
|
||||
toBeSortedAnimations.push({
|
||||
domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element),
|
||||
element: element,
|
||||
domNode: getDomNode(element),
|
||||
fn: function triggerAnimationStart() {
|
||||
var startAnimationFn, closeFn = animationEntry.close;
|
||||
|
||||
// in the event that we've cached the animation status for this element
|
||||
// and it's in fact an invalid animation (something that has duration = 0)
|
||||
// then we should skip all the heavy work from here on
|
||||
if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
|
||||
closeFn();
|
||||
return;
|
||||
}
|
||||
|
||||
// it's important that we apply the `ng-animate` CSS class and the
|
||||
// temporary classes before we do any driver invoking since these
|
||||
// CSS classes may be required for proper CSS detection.
|
||||
animationEntry.beforeStart();
|
||||
|
||||
var startAnimationFn, closeFn = animationEntry.close;
|
||||
|
||||
// in the event that the element was removed before the digest runs or
|
||||
// during the RAF sequencing then we should not trigger the animation.
|
||||
var targetElement = animationEntry.anchors
|
||||
@@ -221,7 +237,32 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
// we need to sort each of the animations in order of parent to child
|
||||
// relationships. This ensures that the child classes are applied at the
|
||||
// right time.
|
||||
$$rAFScheduler(sortAnimations(toBeSortedAnimations));
|
||||
var finalAnimations = sortAnimations(toBeSortedAnimations);
|
||||
for (var i = 0; i < finalAnimations.length; i++) {
|
||||
var innerArray = finalAnimations[i];
|
||||
for (var j = 0; j < innerArray.length; j++) {
|
||||
var entry = innerArray[j];
|
||||
var element = entry.element;
|
||||
|
||||
// the RAFScheduler code only uses functions
|
||||
finalAnimations[i][j] = entry.fn;
|
||||
|
||||
// the first row of elements shouldn't have a prepare-class added to them
|
||||
// since the elements are at the top of the animation hierarchy and they
|
||||
// will be applied without a RAF having to pass...
|
||||
if (i === 0) {
|
||||
element.removeData(PREPARE_CLASSES_KEY);
|
||||
continue;
|
||||
}
|
||||
|
||||
var prepareClassName = element.data(PREPARE_CLASSES_KEY);
|
||||
if (prepareClassName) {
|
||||
$$jqLite.addClass(element, prepareClassName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$$rAFScheduler(finalAnimations);
|
||||
});
|
||||
|
||||
return runner;
|
||||
@@ -359,10 +400,10 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
}
|
||||
|
||||
function beforeStart() {
|
||||
element.addClass(NG_ANIMATE_CLASSNAME);
|
||||
if (tempClasses) {
|
||||
$$jqLite.addClass(element, tempClasses);
|
||||
}
|
||||
tempClasses = (tempClasses ? (tempClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
|
||||
$$jqLite.addClass(element, tempClasses);
|
||||
|
||||
var prepareClassName = element.data(PREPARE_CLASSES_KEY);
|
||||
if (prepareClassName) {
|
||||
$$jqLite.removeClass(element, prepareClassName);
|
||||
prepareClassName = null;
|
||||
@@ -402,7 +443,6 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
$$jqLite.removeClass(element, tempClasses);
|
||||
}
|
||||
|
||||
element.removeClass(NG_ANIMATE_CLASSNAME);
|
||||
runner.complete(!rejected);
|
||||
}
|
||||
};
|
||||
|
||||
+36
-14
@@ -17,20 +17,28 @@
|
||||
* ## Directive Support
|
||||
* The following directives are "animation aware":
|
||||
*
|
||||
* | Directive | Supported Animations |
|
||||
* |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
|
||||
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
|
||||
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
|
||||
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
|
||||
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
|
||||
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
|
||||
* | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
|
||||
* | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
|
||||
* | {@link ng.directive:form#animations form} & {@link ng.directive:ngModel#animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
|
||||
* | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
|
||||
* | {@link module:ngMessages#animations ngMessage} | enter and leave |
|
||||
* | Directive | Supported Animations |
|
||||
* |-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
|
||||
* | {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
|
||||
* | {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave |
|
||||
* | {@link ng.directive:ngClass#animations ngClass / {{class}​}} | add and remove |
|
||||
* | {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
|
||||
* | {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
|
||||
* | {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
|
||||
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
|
||||
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
|
||||
* | {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
|
||||
* | {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
|
||||
* | {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
|
||||
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
|
||||
* | {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
|
||||
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
|
||||
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
|
||||
*
|
||||
* (More information can be found by visiting each the documentation associated with each directive.)
|
||||
* (More information can be found by visiting the documentation associated with each directive.)
|
||||
*
|
||||
* For a full breakdown of the steps involved during each animation event, refer to the
|
||||
* {@link ng.$animate `$animate` API docs}.
|
||||
*
|
||||
* ## CSS-based Animations
|
||||
*
|
||||
@@ -267,9 +275,22 @@
|
||||
* .message.ng-enter-prepare {
|
||||
* opacity: 0;
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* ### Animating between value changes
|
||||
*
|
||||
* Sometimes you need to animate between different expression states, whose values
|
||||
* don't necessary need to be known or referenced in CSS styles.
|
||||
* Unless possible with another {@link ngAnimate#directive-support "animation aware" directive},
|
||||
* that specific use case can always be covered with {@link ngAnimate.directive:ngAnimateSwap} as
|
||||
* can be seen in {@link ngAnimate.directive:ngAnimateSwap#examples this example}.
|
||||
*
|
||||
* Note that {@link ngAnimate.directive:ngAnimateSwap} is a *structural directive*, which means it
|
||||
* creates a new instance of the element (including any other/child directives it may have) and
|
||||
* links it to a new scope every time *swap* happens. In some cases this might not be desirable
|
||||
* (e.g. for performance reasons, or when you wish to retain internal state on the original
|
||||
* element instance).
|
||||
*
|
||||
* ## JavaScript-based Animations
|
||||
*
|
||||
* ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
|
||||
@@ -765,6 +786,7 @@ angular.module('ngAnimate', [], function initAngularHelpers() {
|
||||
.factory('$$rAFScheduler', $$rAFSchedulerFactory)
|
||||
|
||||
.provider('$$animateQueue', $$AnimateQueueProvider)
|
||||
.provider('$$animateCache', $$AnimateCacheProvider)
|
||||
.provider('$$animation', $$AnimationProvider)
|
||||
|
||||
.provider('$animateCss', $AnimateCssProvider)
|
||||
|
||||
@@ -301,7 +301,7 @@ function getDomNode(element) {
|
||||
return (element instanceof jqLite) ? element[0] : element;
|
||||
}
|
||||
|
||||
function applyGeneratedPreparationClasses(element, event, options) {
|
||||
function applyGeneratedPreparationClasses($$jqLite, element, event, options) {
|
||||
var classes = '';
|
||||
if (event) {
|
||||
classes = pendClasses(event, EVENT_CLASS_PREFIX, true);
|
||||
|
||||
+24
-5
@@ -14,8 +14,8 @@
|
||||
*
|
||||
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
|
||||
* directives are supported:
|
||||
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
|
||||
* `ngDblClick`, and `ngMessages`.
|
||||
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`,
|
||||
* `ngClick`, `ngDblClick`, and `ngMessages`.
|
||||
*
|
||||
* Below is a more detailed breakdown of the attributes handled by ngAria:
|
||||
*
|
||||
@@ -46,11 +46,17 @@
|
||||
* <md-checkbox ng-disabled="disabled" aria-disabled="true">
|
||||
* ```
|
||||
*
|
||||
* ## Disabling Attributes
|
||||
* It's possible to disable individual attributes added by ngAria with the
|
||||
* ## Disabling Specific Attributes
|
||||
* It is possible to disable individual attributes added by ngAria with the
|
||||
* {@link ngAria.$ariaProvider#config config} method. For more details, see the
|
||||
* {@link guide/accessibility Developer Guide}.
|
||||
*
|
||||
* ## Disabling `ngAria` on Specific Elements
|
||||
* It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable`
|
||||
* attribute on it. Note that only the element itself (and not its child elements) will be ignored.
|
||||
*/
|
||||
var ARIA_DISABLE_ATTR = 'ngAriaDisable';
|
||||
|
||||
var ngAriaModule = angular.module('ngAria', ['ng']).
|
||||
info({ angularVersion: '"NG_VERSION_FULL"' }).
|
||||
provider('$aria', $AriaProvider);
|
||||
@@ -132,6 +138,8 @@ function $AriaProvider() {
|
||||
|
||||
function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
|
||||
return function(scope, elem, attr) {
|
||||
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
||||
|
||||
var ariaCamelName = attr.$normalize(ariaAttr);
|
||||
if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
|
||||
scope.$watch(attr[attrName], function(boolVal) {
|
||||
@@ -251,6 +259,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
require: 'ngModel',
|
||||
priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
|
||||
compile: function(elem, attr) {
|
||||
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
||||
|
||||
var shape = getShape(attr, elem);
|
||||
|
||||
return {
|
||||
@@ -347,6 +357,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
restrict: 'A',
|
||||
require: '?ngMessages',
|
||||
link: function(scope, elem, attr, ngMessages) {
|
||||
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
||||
|
||||
if (!elem.attr('aria-live')) {
|
||||
elem.attr('aria-live', 'assertive');
|
||||
}
|
||||
@@ -357,6 +369,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function(elem, attr) {
|
||||
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
||||
|
||||
var fn = $parse(attr.ngClick);
|
||||
return function(scope, elem, attr) {
|
||||
|
||||
@@ -373,7 +387,10 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
if ($aria.config('bindKeydown') && !attr.ngKeydown && !attr.ngKeypress && !attr.ngKeyup) {
|
||||
elem.on('keydown', function(event) {
|
||||
var keyCode = event.which || event.keyCode;
|
||||
if (keyCode === 32 || keyCode === 13) {
|
||||
|
||||
if (keyCode === 13 || keyCode === 32) {
|
||||
// Prevent the default browser behavior (e.g. scrolling when pressing spacebar).
|
||||
event.preventDefault();
|
||||
scope.$apply(callback);
|
||||
}
|
||||
|
||||
@@ -389,6 +406,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
}])
|
||||
.directive('ngDblclick', ['$aria', function($aria) {
|
||||
return function(scope, elem, attr) {
|
||||
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
||||
|
||||
if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) {
|
||||
elem.attr('tabindex', 0);
|
||||
}
|
||||
|
||||
+128
-46
@@ -18,7 +18,7 @@ var jqLite;
|
||||
* sequencing based on the order of how the messages are defined in the template.
|
||||
*
|
||||
* Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude`
|
||||
* `ngMessage` and `ngMessageExp` directives.
|
||||
* `ngMessage`, `ngMessageExp` and `ngMessageDefault` directives.
|
||||
*
|
||||
* ## Usage
|
||||
* The `ngMessages` directive allows keys in a key/value collection to be associated with a child element
|
||||
@@ -257,7 +257,26 @@ var jqLite;
|
||||
* .some-message.ng-leave.ng-leave-active {}
|
||||
* ```
|
||||
*
|
||||
* {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
|
||||
* {@link ngAnimate See the ngAnimate docs} to learn how to use JavaScript animations or to learn
|
||||
* more about ngAnimate.
|
||||
*
|
||||
* ## Displaying a default message
|
||||
* If the ngMessages renders no inner ngMessage directive (i.e. when none of the truthy
|
||||
* keys are matched by a defined message), then it will render a default message
|
||||
* using the {@link ngMessageDefault} directive.
|
||||
* Note that matched messages will always take precedence over unmatched messages. That means
|
||||
* the default message will not be displayed when another message is matched. This is also
|
||||
* true for `ng-messages-multiple`.
|
||||
*
|
||||
* ```html
|
||||
* <div ng-messages="myForm.myField.$error" role="alert">
|
||||
* <div ng-message="required">This field is required</div>
|
||||
* <div ng-message="minlength">This field is too short</div>
|
||||
* <div ng-message-default>This field has an input error</div>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
|
||||
*/
|
||||
angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
// Access helpers from AngularJS core.
|
||||
@@ -286,8 +305,11 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
* at a time and this depends on the prioritization of the messages within the template. (This can
|
||||
* be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.)
|
||||
*
|
||||
* A remote template can also be used to promote message reusability and messages can also be
|
||||
* overridden.
|
||||
* A remote template can also be used (With {@link ngMessagesInclude}) to promote message
|
||||
* reusability and messages can also be overridden.
|
||||
*
|
||||
* A default message can also be displayed when no `ngMessage` directive is inserted, using the
|
||||
* {@link ngMessageDefault} directive.
|
||||
*
|
||||
* {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`.
|
||||
*
|
||||
@@ -298,6 +320,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
* <ANY ng-message="stringValue">...</ANY>
|
||||
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
|
||||
* <ANY ng-message-exp="expressionValue">...</ANY>
|
||||
* <ANY ng-message-default>...</ANY>
|
||||
* </ANY>
|
||||
*
|
||||
* <!-- or by using element directives -->
|
||||
@@ -305,6 +328,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
* <ng-message when="stringValue">...</ng-message>
|
||||
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
|
||||
* <ng-message when-exp="expressionValue">...</ng-message>
|
||||
* <ng-message-default>...</ng-message-default>
|
||||
* </ng-messages>
|
||||
* ```
|
||||
*
|
||||
@@ -333,6 +357,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
* <div ng-message="required">You did not enter a field</div>
|
||||
* <div ng-message="minlength">Your field is too short</div>
|
||||
* <div ng-message="maxlength">Your field is too long</div>
|
||||
* <div ng-message-default>This field has an input error</div>
|
||||
* </div>
|
||||
* </form>
|
||||
* </file>
|
||||
@@ -370,6 +395,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
|
||||
var unmatchedMessages = [];
|
||||
var matchedKeys = {};
|
||||
var truthyKeys = 0;
|
||||
var messageItem = ctrl.head;
|
||||
var messageFound = false;
|
||||
var totalMessages = 0;
|
||||
@@ -382,13 +408,17 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
var messageUsed = false;
|
||||
if (!messageFound) {
|
||||
forEach(collection, function(value, key) {
|
||||
if (!messageUsed && truthy(value) && messageCtrl.test(key)) {
|
||||
// this is to prevent the same error name from showing up twice
|
||||
if (matchedKeys[key]) return;
|
||||
matchedKeys[key] = true;
|
||||
if (truthy(value) && !messageUsed) {
|
||||
truthyKeys++;
|
||||
|
||||
messageUsed = true;
|
||||
messageCtrl.attach();
|
||||
if (messageCtrl.test(key)) {
|
||||
// this is to prevent the same error name from showing up twice
|
||||
if (matchedKeys[key]) return;
|
||||
matchedKeys[key] = true;
|
||||
|
||||
messageUsed = true;
|
||||
messageCtrl.attach();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -408,7 +438,16 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
messageCtrl.detach();
|
||||
});
|
||||
|
||||
if (unmatchedMessages.length !== totalMessages) {
|
||||
var messageMatched = unmatchedMessages.length !== totalMessages;
|
||||
var attachDefault = ctrl.default && !messageMatched && truthyKeys > 0;
|
||||
|
||||
if (attachDefault) {
|
||||
ctrl.default.attach();
|
||||
} else if (ctrl.default) {
|
||||
ctrl.default.detach();
|
||||
}
|
||||
|
||||
if (messageMatched || attachDefault) {
|
||||
$animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
|
||||
} else {
|
||||
$animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
|
||||
@@ -428,23 +467,31 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
}
|
||||
};
|
||||
|
||||
this.register = function(comment, messageCtrl) {
|
||||
var nextKey = latestKey.toString();
|
||||
messages[nextKey] = {
|
||||
message: messageCtrl
|
||||
};
|
||||
insertMessageNode($element[0], comment, nextKey);
|
||||
comment.$$ngMessageNode = nextKey;
|
||||
latestKey++;
|
||||
this.register = function(comment, messageCtrl, isDefault) {
|
||||
if (isDefault) {
|
||||
ctrl.default = messageCtrl;
|
||||
} else {
|
||||
var nextKey = latestKey.toString();
|
||||
messages[nextKey] = {
|
||||
message: messageCtrl
|
||||
};
|
||||
insertMessageNode($element[0], comment, nextKey);
|
||||
comment.$$ngMessageNode = nextKey;
|
||||
latestKey++;
|
||||
}
|
||||
|
||||
ctrl.reRender();
|
||||
};
|
||||
|
||||
this.deregister = function(comment) {
|
||||
var key = comment.$$ngMessageNode;
|
||||
delete comment.$$ngMessageNode;
|
||||
removeMessageNode($element[0], comment, key);
|
||||
delete messages[key];
|
||||
this.deregister = function(comment, isDefault) {
|
||||
if (isDefault) {
|
||||
delete ctrl.default;
|
||||
} else {
|
||||
var key = comment.$$ngMessageNode;
|
||||
delete comment.$$ngMessageNode;
|
||||
removeMessageNode($element[0], comment, key);
|
||||
delete messages[key];
|
||||
}
|
||||
ctrl.reRender();
|
||||
};
|
||||
|
||||
@@ -647,9 +694,41 @@ angular.module('ngMessages', [], function initAngularHelpers() {
|
||||
*
|
||||
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
|
||||
*/
|
||||
.directive('ngMessageExp', ngMessageDirectiveFactory());
|
||||
.directive('ngMessageExp', ngMessageDirectiveFactory())
|
||||
|
||||
function ngMessageDirectiveFactory() {
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngMessageDefault
|
||||
* @restrict AE
|
||||
* @scope
|
||||
*
|
||||
* @description
|
||||
* `ngMessageDefault` is a directive with the purpose to show and hide a default message for
|
||||
* {@link directive:ngMessages}, when none of provided messages matches.
|
||||
*
|
||||
* More information about using `ngMessageDefault` can be found in the
|
||||
* {@link module:ngMessages `ngMessages` module documentation}.
|
||||
*
|
||||
* @usage
|
||||
* ```html
|
||||
* <!-- using attribute directives -->
|
||||
* <ANY ng-messages="expression" role="alert">
|
||||
* <ANY ng-message="stringValue">...</ANY>
|
||||
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
|
||||
* <ANY ng-message-default>...</ANY>
|
||||
* </ANY>
|
||||
*
|
||||
* <!-- or by using element directives -->
|
||||
* <ng-messages for="expression" role="alert">
|
||||
* <ng-message when="stringValue">...</ng-message>
|
||||
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
|
||||
* <ng-message-default>...</ng-message-default>
|
||||
* </ng-messages>
|
||||
*
|
||||
*/
|
||||
.directive('ngMessageDefault', ngMessageDirectiveFactory(true));
|
||||
|
||||
function ngMessageDirectiveFactory(isDefault) {
|
||||
return ['$animate', function($animate) {
|
||||
return {
|
||||
restrict: 'AE',
|
||||
@@ -658,25 +737,28 @@ function ngMessageDirectiveFactory() {
|
||||
terminal: true,
|
||||
require: '^^ngMessages',
|
||||
link: function(scope, element, attrs, ngMessagesCtrl, $transclude) {
|
||||
var commentNode = element[0];
|
||||
var commentNode, records, staticExp, dynamicExp;
|
||||
|
||||
var records;
|
||||
var staticExp = attrs.ngMessage || attrs.when;
|
||||
var dynamicExp = attrs.ngMessageExp || attrs.whenExp;
|
||||
var assignRecords = function(items) {
|
||||
records = items
|
||||
? (isArray(items)
|
||||
? items
|
||||
: items.split(/[\s,]+/))
|
||||
: null;
|
||||
ngMessagesCtrl.reRender();
|
||||
};
|
||||
if (!isDefault) {
|
||||
commentNode = element[0];
|
||||
staticExp = attrs.ngMessage || attrs.when;
|
||||
dynamicExp = attrs.ngMessageExp || attrs.whenExp;
|
||||
|
||||
if (dynamicExp) {
|
||||
assignRecords(scope.$eval(dynamicExp));
|
||||
scope.$watchCollection(dynamicExp, assignRecords);
|
||||
} else {
|
||||
assignRecords(staticExp);
|
||||
var assignRecords = function(items) {
|
||||
records = items
|
||||
? (isArray(items)
|
||||
? items
|
||||
: items.split(/[\s,]+/))
|
||||
: null;
|
||||
ngMessagesCtrl.reRender();
|
||||
};
|
||||
|
||||
if (dynamicExp) {
|
||||
assignRecords(scope.$eval(dynamicExp));
|
||||
scope.$watchCollection(dynamicExp, assignRecords);
|
||||
} else {
|
||||
assignRecords(staticExp);
|
||||
}
|
||||
}
|
||||
|
||||
var currentElement, messageCtrl;
|
||||
@@ -701,7 +783,7 @@ function ngMessageDirectiveFactory() {
|
||||
// If the message element was removed via a call to `detach` then `currentElement` will be null
|
||||
// So this handler only handles cases where something else removed the message element.
|
||||
if (currentElement && currentElement.$$attachId === $$attachId) {
|
||||
ngMessagesCtrl.deregister(commentNode);
|
||||
ngMessagesCtrl.deregister(commentNode, isDefault);
|
||||
messageCtrl.detach();
|
||||
}
|
||||
newScope.$destroy();
|
||||
@@ -716,14 +798,14 @@ function ngMessageDirectiveFactory() {
|
||||
$animate.leave(elm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, isDefault);
|
||||
|
||||
// We need to ensure that this directive deregisters itself when it no longer exists
|
||||
// Normally this is done when the attached element is destroyed; but if this directive
|
||||
// gets removed before we attach the message to the DOM there is nothing to watch
|
||||
// in which case we must deregister when the containing scope is destroyed.
|
||||
scope.$on('$destroy', function() {
|
||||
ngMessagesCtrl.deregister(commentNode);
|
||||
ngMessagesCtrl.deregister(commentNode, isDefault);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Vendored
+370
-196
@@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* global routeToRegExp: false */
|
||||
|
||||
/**
|
||||
* @ngdoc object
|
||||
* @name angular.mock
|
||||
@@ -24,43 +26,27 @@ angular.mock = {};
|
||||
* that there are several helper methods available which can be used in tests.
|
||||
*/
|
||||
angular.mock.$BrowserProvider = function() {
|
||||
this.$get = function() {
|
||||
return new angular.mock.$Browser();
|
||||
};
|
||||
this.$get = [
|
||||
'$log', '$$taskTrackerFactory',
|
||||
function($log, $$taskTrackerFactory) {
|
||||
return new angular.mock.$Browser($log, $$taskTrackerFactory);
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
angular.mock.$Browser = function() {
|
||||
angular.mock.$Browser = function($log, $$taskTrackerFactory) {
|
||||
var self = this;
|
||||
var taskTracker = $$taskTrackerFactory($log);
|
||||
|
||||
this.isMock = true;
|
||||
self.$$url = 'http://server/';
|
||||
self.$$lastUrl = self.$$url; // used by url polling fn
|
||||
self.pollFns = [];
|
||||
|
||||
// Testability API
|
||||
|
||||
var outstandingRequestCount = 0;
|
||||
var outstandingRequestCallbacks = [];
|
||||
self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
|
||||
self.$$completeOutstandingRequest = function(fn) {
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
outstandingRequestCount--;
|
||||
if (!outstandingRequestCount) {
|
||||
while (outstandingRequestCallbacks.length) {
|
||||
outstandingRequestCallbacks.pop()();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
self.notifyWhenNoOutstandingRequests = function(callback) {
|
||||
if (outstandingRequestCount) {
|
||||
outstandingRequestCallbacks.push(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
// Task-tracking API
|
||||
self.$$completeOutstandingRequest = taskTracker.completeTask;
|
||||
self.$$incOutstandingRequestCount = taskTracker.incTaskCount;
|
||||
self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks;
|
||||
|
||||
// register url polling fn
|
||||
|
||||
@@ -84,13 +70,22 @@ angular.mock.$Browser = function() {
|
||||
self.deferredFns = [];
|
||||
self.deferredNextId = 0;
|
||||
|
||||
self.defer = function(fn, delay) {
|
||||
// Note that we do not use `$$incOutstandingRequestCount` or `$$completeOutstandingRequest`
|
||||
// in this mock implementation.
|
||||
self.defer = function(fn, delay, taskType) {
|
||||
var timeoutId = self.deferredNextId++;
|
||||
|
||||
delay = delay || 0;
|
||||
self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
|
||||
self.deferredFns.sort(function(a, b) { return a.time - b.time;});
|
||||
return self.deferredNextId++;
|
||||
taskType = taskType || taskTracker.DEFAULT_TASK_TYPE;
|
||||
|
||||
taskTracker.incTaskCount(taskType);
|
||||
self.deferredFns.push({
|
||||
id: timeoutId,
|
||||
type: taskType,
|
||||
time: (self.defer.now + delay),
|
||||
fn: fn
|
||||
});
|
||||
self.deferredFns.sort(function(a, b) { return a.time - b.time; });
|
||||
|
||||
return timeoutId;
|
||||
};
|
||||
|
||||
|
||||
@@ -104,14 +99,15 @@ angular.mock.$Browser = function() {
|
||||
|
||||
|
||||
self.defer.cancel = function(deferId) {
|
||||
var fnIndex;
|
||||
var taskIndex;
|
||||
|
||||
angular.forEach(self.deferredFns, function(fn, index) {
|
||||
if (fn.id === deferId) fnIndex = index;
|
||||
angular.forEach(self.deferredFns, function(task, index) {
|
||||
if (task.id === deferId) taskIndex = index;
|
||||
});
|
||||
|
||||
if (angular.isDefined(fnIndex)) {
|
||||
self.deferredFns.splice(fnIndex, 1);
|
||||
if (angular.isDefined(taskIndex)) {
|
||||
var task = self.deferredFns.splice(taskIndex, 1)[0];
|
||||
taskTracker.completeTask(angular.noop, task.type);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -125,6 +121,8 @@ angular.mock.$Browser = function() {
|
||||
* @description
|
||||
* Flushes all pending requests and executes the defer callbacks.
|
||||
*
|
||||
* See {@link ngMock.$flushPendingsTasks} for more info.
|
||||
*
|
||||
* @param {number=} number of milliseconds to flush. See {@link #defer.now}
|
||||
*/
|
||||
self.defer.flush = function(delay) {
|
||||
@@ -133,26 +131,76 @@ angular.mock.$Browser = function() {
|
||||
if (angular.isDefined(delay)) {
|
||||
// A delay was passed so compute the next time
|
||||
nextTime = self.defer.now + delay;
|
||||
} else if (self.deferredFns.length) {
|
||||
// No delay was passed so set the next time so that it clears the deferred queue
|
||||
nextTime = self.deferredFns[self.deferredFns.length - 1].time;
|
||||
} else {
|
||||
if (self.deferredFns.length) {
|
||||
// No delay was passed so set the next time so that it clears the deferred queue
|
||||
nextTime = self.deferredFns[self.deferredFns.length - 1].time;
|
||||
} else {
|
||||
// No delay passed, but there are no deferred tasks so flush - indicates an error!
|
||||
throw new Error('No deferred tasks to be flushed');
|
||||
}
|
||||
// No delay passed, but there are no deferred tasks so flush - indicates an error!
|
||||
throw new Error('No deferred tasks to be flushed');
|
||||
}
|
||||
|
||||
while (self.deferredFns.length && self.deferredFns[0].time <= nextTime) {
|
||||
// Increment the time and call the next deferred function
|
||||
self.defer.now = self.deferredFns[0].time;
|
||||
self.deferredFns.shift().fn();
|
||||
var task = self.deferredFns.shift();
|
||||
taskTracker.completeTask(task.fn, task.type);
|
||||
}
|
||||
|
||||
// Ensure that the current time is correct
|
||||
self.defer.now = nextTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* @name $browser#defer.getPendingTasks
|
||||
*
|
||||
* @description
|
||||
* Returns the currently pending tasks that need to be flushed.
|
||||
* You can request a specific type of tasks only, by specifying a `taskType`.
|
||||
*
|
||||
* @param {string=} taskType - The type tasks to return.
|
||||
*/
|
||||
self.defer.getPendingTasks = function(taskType) {
|
||||
return !taskType
|
||||
? self.deferredFns
|
||||
: self.deferredFns.filter(function(task) { return task.type === taskType; });
|
||||
};
|
||||
|
||||
/**
|
||||
* @name $browser#defer.formatPendingTasks
|
||||
*
|
||||
* @description
|
||||
* Formats each task in a list of pending tasks as a string, suitable for use in error messages.
|
||||
*
|
||||
* @param {Array<Object>} pendingTasks - A list of task objects.
|
||||
* @return {Array<string>} A list of stringified tasks.
|
||||
*/
|
||||
self.defer.formatPendingTasks = function(pendingTasks) {
|
||||
return pendingTasks.map(function(task) {
|
||||
return '{id: ' + task.id + ', type: ' + task.type + ', time: ' + task.time + '}';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @name $browser#defer.verifyNoPendingTasks
|
||||
*
|
||||
* @description
|
||||
* Verifies that there are no pending tasks that need to be flushed.
|
||||
* You can check for a specific type of tasks only, by specifying a `taskType`.
|
||||
*
|
||||
* See {@link $verifyNoPendingTasks} for more info.
|
||||
*
|
||||
* @param {string=} taskType - The type tasks to check for.
|
||||
*/
|
||||
self.defer.verifyNoPendingTasks = function(taskType) {
|
||||
var pendingTasks = self.defer.getPendingTasks(taskType);
|
||||
|
||||
if (pendingTasks.length) {
|
||||
var formattedTasks = self.defer.formatPendingTasks(pendingTasks).join('\n ');
|
||||
throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' +
|
||||
formattedTasks);
|
||||
}
|
||||
};
|
||||
|
||||
self.$$baseHref = '/';
|
||||
self.baseHref = function() {
|
||||
return this.$$baseHref;
|
||||
@@ -177,7 +225,8 @@ angular.mock.$Browser.prototype = {
|
||||
state = null;
|
||||
}
|
||||
if (url) {
|
||||
this.$$url = url;
|
||||
// The `$browser` service trims empty hashes; simulate it.
|
||||
this.$$url = url.replace(/#$/, '');
|
||||
// Native pushState serializes & copies the object; simulate it.
|
||||
this.$$state = angular.copy(state);
|
||||
return this;
|
||||
@@ -191,6 +240,82 @@ angular.mock.$Browser.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $flushPendingTasks
|
||||
*
|
||||
* @description
|
||||
* Flushes all currently pending tasks and executes the corresponding callbacks.
|
||||
*
|
||||
* Optionally, you can also pass a `delay` argument to only flush tasks that are scheduled to be
|
||||
* executed within `delay` milliseconds. Currently, `delay` only applies to timeouts, since all
|
||||
* other tasks have a delay of 0 (i.e. they are scheduled to be executed as soon as possible, but
|
||||
* still asynchronously).
|
||||
*
|
||||
* If no delay is specified, it uses a delay such that all currently pending tasks are flushed.
|
||||
*
|
||||
* The types of tasks that are flushed include:
|
||||
*
|
||||
* - Pending timeouts (via {@link $timeout}).
|
||||
* - Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}.
|
||||
* - Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
|
||||
* These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
|
||||
*
|
||||
* <div class="alert alert-info">
|
||||
* Periodic tasks scheduled via {@link $interval} use a different queue and are not flushed by
|
||||
* `$flushPendingTasks()`. Use {@link ngMock.$interval#flush $interval.flush(millis)} instead.
|
||||
* </div>
|
||||
*
|
||||
* @param {number=} delay - The number of milliseconds to flush.
|
||||
*/
|
||||
angular.mock.$FlushPendingTasksProvider = function() {
|
||||
this.$get = [
|
||||
'$browser',
|
||||
function($browser) {
|
||||
return function $flushPendingTasks(delay) {
|
||||
return $browser.defer.flush(delay);
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $verifyNoPendingTasks
|
||||
*
|
||||
* @description
|
||||
* Verifies that there are no pending tasks that need to be flushed. It throws an error if there are
|
||||
* still pending tasks.
|
||||
*
|
||||
* You can check for a specific type of tasks only, by specifying a `taskType`.
|
||||
*
|
||||
* Available task types:
|
||||
*
|
||||
* - `$timeout`: Pending timeouts (via {@link $timeout}).
|
||||
* - `$http`: Pending HTTP requests (via {@link $http}).
|
||||
* - `$route`: In-progress route transitions (via {@link $route}).
|
||||
* - `$applyAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}.
|
||||
* - `$evalAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
|
||||
* These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
|
||||
*
|
||||
* <div class="alert alert-info">
|
||||
* Periodic tasks scheduled via {@link $interval} use a different queue and are not taken into
|
||||
* account by `$verifyNoPendingTasks()`. There is currently no way to verify that there are no
|
||||
* pending {@link $interval} tasks.
|
||||
* </div>
|
||||
*
|
||||
* @param {string=} taskType - The type of tasks to check for.
|
||||
*/
|
||||
angular.mock.$VerifyNoPendingTasksProvider = function() {
|
||||
this.$get = [
|
||||
'$browser',
|
||||
function($browser) {
|
||||
return function $verifyNoPendingTasks(taskType) {
|
||||
return $browser.defer.verifyNoPendingTasks(taskType);
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
@@ -459,62 +584,40 @@ angular.mock.$LogProvider = function() {
|
||||
* @returns {promise} A promise which will be notified on each iteration.
|
||||
*/
|
||||
angular.mock.$IntervalProvider = function() {
|
||||
this.$get = ['$browser', '$rootScope', '$q', '$$q',
|
||||
function($browser, $rootScope, $q, $$q) {
|
||||
this.$get = ['$browser', '$$intervalFactory',
|
||||
function($browser, $$intervalFactory) {
|
||||
var repeatFns = [],
|
||||
nextRepeatId = 0,
|
||||
now = 0;
|
||||
now = 0,
|
||||
setIntervalFn = function(tick, delay, deferred, skipApply) {
|
||||
var id = nextRepeatId++;
|
||||
var fn = !skipApply ? tick : function() {
|
||||
tick();
|
||||
$browser.defer.flush();
|
||||
};
|
||||
|
||||
var $interval = function(fn, delay, count, invokeApply) {
|
||||
var hasParams = arguments.length > 4,
|
||||
args = hasParams ? Array.prototype.slice.call(arguments, 4) : [],
|
||||
iteration = 0,
|
||||
skipApply = (angular.isDefined(invokeApply) && !invokeApply),
|
||||
deferred = (skipApply ? $$q : $q).defer(),
|
||||
promise = deferred.promise;
|
||||
|
||||
count = (angular.isDefined(count)) ? count : 0;
|
||||
promise.then(null, function() {}, (!hasParams) ? fn : function() {
|
||||
fn.apply(null, args);
|
||||
});
|
||||
|
||||
promise.$$intervalId = nextRepeatId;
|
||||
|
||||
function tick() {
|
||||
deferred.notify(iteration++);
|
||||
|
||||
if (count > 0 && iteration >= count) {
|
||||
var fnIndex;
|
||||
deferred.resolve(iteration);
|
||||
|
||||
angular.forEach(repeatFns, function(fn, index) {
|
||||
if (fn.id === promise.$$intervalId) fnIndex = index;
|
||||
repeatFns.push({
|
||||
nextTime: (now + (delay || 0)),
|
||||
delay: delay || 1,
|
||||
fn: fn,
|
||||
id: id,
|
||||
deferred: deferred
|
||||
});
|
||||
repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime; });
|
||||
|
||||
if (angular.isDefined(fnIndex)) {
|
||||
repeatFns.splice(fnIndex, 1);
|
||||
return id;
|
||||
},
|
||||
clearIntervalFn = function(id) {
|
||||
for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) {
|
||||
if (repeatFns[fnIndex].id === id) {
|
||||
repeatFns.splice(fnIndex, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (skipApply) {
|
||||
$browser.defer.flush();
|
||||
} else {
|
||||
$rootScope.$apply();
|
||||
}
|
||||
}
|
||||
var $interval = $$intervalFactory(setIntervalFn, clearIntervalFn);
|
||||
|
||||
repeatFns.push({
|
||||
nextTime: (now + (delay || 0)),
|
||||
delay: delay || 1,
|
||||
fn: tick,
|
||||
id: nextRepeatId,
|
||||
deferred: deferred
|
||||
});
|
||||
repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
|
||||
|
||||
nextRepeatId++;
|
||||
return promise;
|
||||
};
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $interval#cancel
|
||||
@@ -527,17 +630,15 @@ angular.mock.$IntervalProvider = function() {
|
||||
*/
|
||||
$interval.cancel = function(promise) {
|
||||
if (!promise) return false;
|
||||
var fnIndex;
|
||||
|
||||
angular.forEach(repeatFns, function(fn, index) {
|
||||
if (fn.id === promise.$$intervalId) fnIndex = index;
|
||||
});
|
||||
|
||||
if (angular.isDefined(fnIndex)) {
|
||||
repeatFns[fnIndex].deferred.promise.then(undefined, function() {});
|
||||
repeatFns[fnIndex].deferred.reject('canceled');
|
||||
repeatFns.splice(fnIndex, 1);
|
||||
return true;
|
||||
for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) {
|
||||
if (repeatFns[fnIndex].id === promise.$$intervalId) {
|
||||
var deferred = repeatFns[fnIndex].deferred;
|
||||
deferred.promise.then(undefined, function() {});
|
||||
deferred.reject('canceled');
|
||||
repeatFns.splice(fnIndex, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -550,7 +651,7 @@ angular.mock.$IntervalProvider = function() {
|
||||
*
|
||||
* Runs interval tasks scheduled to be run in the next `millis` milliseconds.
|
||||
*
|
||||
* @param {number=} millis maximum timeout amount to flush up until.
|
||||
* @param {number} millis maximum timeout amount to flush up until.
|
||||
*
|
||||
* @return {number} The amount of time moved forward.
|
||||
*/
|
||||
@@ -1282,7 +1383,7 @@ angular.mock.dump = function(object) {
|
||||
* ## Matching route requests
|
||||
*
|
||||
* For extra convenience, `whenRoute` and `expectRoute` shortcuts are available. These methods offer colon
|
||||
* delimited matching of the url path, ignoring the query string. This allows declarations
|
||||
* delimited matching of the url path, ignoring the query string and trailing slashes. This allows declarations
|
||||
* similar to how application routes are configured with `$routeProvider`. Because these methods convert
|
||||
* the definition url to regex, declaration order is important. Combined with query parameter parsing,
|
||||
* the following is possible:
|
||||
@@ -1410,16 +1511,25 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
}
|
||||
}
|
||||
|
||||
function createFatalError(message) {
|
||||
var error = new Error(message);
|
||||
// In addition to being converted to a rejection, these errors also need to be passed to
|
||||
// the $exceptionHandler and be rethrown (so that the test fails).
|
||||
error.$$passToExceptionHandler = true;
|
||||
return error;
|
||||
}
|
||||
|
||||
if (expectation && expectation.match(method, url)) {
|
||||
if (!expectation.matchData(data)) {
|
||||
throw new Error('Expected ' + expectation + ' with different data\n' +
|
||||
'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
|
||||
throw createFatalError('Expected ' + expectation + ' with different data\n' +
|
||||
'EXPECTED: ' + prettyPrint(expectation.data) + '\n' +
|
||||
'GOT: ' + data);
|
||||
}
|
||||
|
||||
if (!expectation.matchHeaders(headers)) {
|
||||
throw new Error('Expected ' + expectation + ' with different headers\n' +
|
||||
'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
|
||||
prettyPrint(headers));
|
||||
throw createFatalError('Expected ' + expectation + ' with different headers\n' +
|
||||
'EXPECTED: ' + prettyPrint(expectation.headers) + '\n' +
|
||||
'GOT: ' + prettyPrint(headers));
|
||||
}
|
||||
|
||||
expectations.shift();
|
||||
@@ -1440,20 +1550,17 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
|
||||
} else if (definition.passThrough) {
|
||||
originalHttpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers);
|
||||
} else throw new Error('No response defined !');
|
||||
} else throw createFatalError('No response defined !');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var error = wasExpected ?
|
||||
new Error('No response defined !') :
|
||||
new Error('Unexpected request: ' + method + ' ' + url + '\n' +
|
||||
(expectation ? 'Expected ' + expectation : 'No more request expected'));
|
||||
|
||||
// In addition to be being converted to a rejection, this error also needs to be passed to
|
||||
// the $exceptionHandler and be rethrown (so that the test fails).
|
||||
error.$$passToExceptionHandler = true;
|
||||
if (wasExpected) {
|
||||
throw createFatalError('No response defined !');
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw createFatalError('Unexpected request: ' + method + ' ' + url + '\n' +
|
||||
(expectation ? 'Expected ' + expectation : 'No more request expected'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1481,8 +1588,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* ```
|
||||
* – The respond method takes a set of static data to be returned or a function that can
|
||||
* return an array containing response status (number), response data (Array|Object|string),
|
||||
* response headers (Object), and the text for the status (string). The respond method returns
|
||||
* the `requestHandler` object for possible overrides.
|
||||
* response headers (Object), HTTP status text (string), and XMLHttpRequest status (string:
|
||||
* `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler`
|
||||
* object for possible overrides.
|
||||
*/
|
||||
$httpBackend.when = function(method, url, data, headers, keys) {
|
||||
|
||||
@@ -1558,7 +1666,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current definition.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1573,7 +1682,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current definition.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1588,7 +1698,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current definition.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1605,7 +1716,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
|
||||
* data string and returns true if the data is as expected.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current definition.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1622,7 +1734,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
|
||||
* data string and returns true if the data is as expected.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current definition.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1654,43 +1767,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string} url HTTP url string that supports colon param matching.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled. See #when for more info.
|
||||
* order to change how a matched request is handled.
|
||||
* See {@link ngMock.$httpBackend#when `when`} for more info.
|
||||
*/
|
||||
$httpBackend.whenRoute = function(method, url) {
|
||||
var pathObj = parseRoute(url);
|
||||
var pathObj = routeToRegExp(url, {caseInsensitiveMatch: true, ignoreTrailingSlashes: true});
|
||||
return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys);
|
||||
};
|
||||
|
||||
function parseRoute(url) {
|
||||
var ret = {
|
||||
regexp: url
|
||||
},
|
||||
keys = ret.keys = [];
|
||||
|
||||
if (!url || !angular.isString(url)) return ret;
|
||||
|
||||
url = url
|
||||
.replace(/([().])/g, '\\$1')
|
||||
.replace(/(\/)?:(\w+)([?*])?/g, function(_, slash, key, option) {
|
||||
var optional = option === '?' ? option : null;
|
||||
var star = option === '*' ? option : null;
|
||||
keys.push({ name: key, optional: !!optional });
|
||||
slash = slash || '';
|
||||
return ''
|
||||
+ (optional ? '' : slash)
|
||||
+ '(?:'
|
||||
+ (optional ? slash : '')
|
||||
+ (star && '(.+?)' || '([^/]+)')
|
||||
+ (optional || '')
|
||||
+ ')'
|
||||
+ (optional || '');
|
||||
})
|
||||
.replace(/([/$*])/g, '\\$1');
|
||||
|
||||
ret.regexp = new RegExp('^' + url, 'i');
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $httpBackend#expect
|
||||
@@ -1711,14 +1795,15 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* order to change how a matched request is handled.
|
||||
*
|
||||
* - respond –
|
||||
* ```
|
||||
* { function([status,] data[, headers, statusText])
|
||||
* | function(function(method, url, data, headers, params)}
|
||||
* ```
|
||||
* ```js
|
||||
* {function([status,] data[, headers, statusText])
|
||||
* | function(function(method, url, data, headers, params)}
|
||||
* ```
|
||||
* – The respond method takes a set of static data to be returned or a function that can
|
||||
* return an array containing response status (number), response data (Array|Object|string),
|
||||
* response headers (Object), and the text for the status (string). The respond method returns
|
||||
* the `requestHandler` object for possible overrides.
|
||||
* response headers (Object), HTTP status text (string), and XMLHttpRequest status (string:
|
||||
* `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler`
|
||||
* object for possible overrides.
|
||||
*/
|
||||
$httpBackend.expect = function(method, url, data, headers, keys) {
|
||||
|
||||
@@ -1743,8 +1828,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* Creates a new request expectation for GET requests. For more info see `expect()`.
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* and returns true if the url matches the current expectation.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current expectation.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1758,8 +1844,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* Creates a new request expectation for HEAD requests. For more info see `expect()`.
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* and returns true if the url matches the current expectation.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current expectation.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1773,8 +1860,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* Creates a new request expectation for DELETE requests. For more info see `expect()`.
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* and returns true if the url matches the current expectation.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current expectation.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1788,11 +1876,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* Creates a new request expectation for POST requests. For more info see `expect()`.
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* and returns true if the url matches the current expectation.
|
||||
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
|
||||
* receives data string and returns true if the data is as expected, or Object if request body
|
||||
* is in JSON format.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current expectation.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1806,11 +1895,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* Creates a new request expectation for PUT requests. For more info see `expect()`.
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* and returns true if the url matches the current expectation.
|
||||
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
|
||||
* receives data string and returns true if the data is as expected, or Object if request body
|
||||
* is in JSON format.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current expectation.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1824,11 +1914,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* Creates a new request expectation for PATCH requests. For more info see `expect()`.
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
|
||||
* and returns true if the url matches the current definition.
|
||||
* and returns true if the url matches the current expectation.
|
||||
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
|
||||
* receives data string and returns true if the data is as expected, or Object if request body
|
||||
* is in JSON format.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current expectation.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1842,7 +1933,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* Creates a new request expectation for JSONP requests. For more info see `expect()`.
|
||||
*
|
||||
* @param {string|RegExp|function(string)=} url HTTP url or function that receives an url
|
||||
* and returns true if the url matches the current definition.
|
||||
* and returns true if the url matches the current expectation.
|
||||
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
@@ -1860,10 +1951,11 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string} url HTTP url string that supports colon param matching.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled. See #expect for more info.
|
||||
* order to change how a matched request is handled.
|
||||
* See {@link ngMock.$httpBackend#expect `expect`} for more info.
|
||||
*/
|
||||
$httpBackend.expectRoute = function(method, url) {
|
||||
var pathObj = parseRoute(url);
|
||||
var pathObj = routeToRegExp(url, {caseInsensitiveMatch: true, ignoreTrailingSlashes: true});
|
||||
return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys);
|
||||
};
|
||||
|
||||
@@ -2193,39 +2285,86 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $timeout#flush
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.7.3"
|
||||
*
|
||||
* This method flushes all types of tasks (not only timeouts), which is unintuitive.
|
||||
* It is recommended to use {@link ngMock.$flushPendingTasks} instead.
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Flushes the queue of pending tasks.
|
||||
*
|
||||
* _This method is essentially an alias of {@link ngMock.$flushPendingTasks}._
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* For historical reasons, this method will also flush non-`$timeout` pending tasks, such as
|
||||
* {@link $q} promises and tasks scheduled via
|
||||
* {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and
|
||||
* {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
|
||||
* </div>
|
||||
*
|
||||
* @param {number=} delay maximum timeout amount to flush up until
|
||||
*/
|
||||
$delegate.flush = function(delay) {
|
||||
// For historical reasons, `$timeout.flush()` flushes all types of pending tasks.
|
||||
// Keep the same behavior for backwards compatibility (and because it doesn't make sense to
|
||||
// selectively flush scheduled events out of order).
|
||||
$browser.defer.flush(delay);
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $timeout#verifyNoPendingTasks
|
||||
*
|
||||
* @deprecated
|
||||
* sinceVersion="1.7.3"
|
||||
*
|
||||
* This method takes all types of tasks (not only timeouts) into account, which is unintuitive.
|
||||
* It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally
|
||||
* allows checking for timeouts only (with `$verifyNoPendingTasks('$timeout')`).
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Verifies that there are no pending tasks that need to be flushed.
|
||||
* Verifies that there are no pending tasks that need to be flushed. It throws an error if there
|
||||
* are still pending tasks.
|
||||
*
|
||||
* _This method is essentially an alias of {@link ngMock.$verifyNoPendingTasks} (called with no
|
||||
* arguments)._
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* <p>
|
||||
* For historical reasons, this method will also verify non-`$timeout` pending tasks, such as
|
||||
* pending {@link $http} requests, in-progress {@link $route} transitions, unresolved
|
||||
* {@link $q} promises and tasks scheduled via
|
||||
* {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and
|
||||
* {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
|
||||
* </p>
|
||||
* <p>
|
||||
* It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally
|
||||
* supports verifying a specific type of tasks. For example, you can verify there are no
|
||||
* pending timeouts with `$verifyNoPendingTasks('$timeout')`.
|
||||
* </p>
|
||||
* </div>
|
||||
*/
|
||||
$delegate.verifyNoPendingTasks = function() {
|
||||
if ($browser.deferredFns.length) {
|
||||
throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' +
|
||||
formatPendingTasksAsString($browser.deferredFns));
|
||||
// For historical reasons, `$timeout.verifyNoPendingTasks()` takes all types of pending tasks
|
||||
// into account. Keep the same behavior for backwards compatibility.
|
||||
var pendingTasks = $browser.defer.getPendingTasks();
|
||||
|
||||
if (pendingTasks.length) {
|
||||
var formattedTasks = $browser.defer.formatPendingTasks(pendingTasks).join('\n ');
|
||||
var hasPendingTimeout = pendingTasks.some(function(task) { return task.type === '$timeout'; });
|
||||
var extraMessage = hasPendingTimeout ? '' : '\n\nNone of the pending tasks are timeouts. ' +
|
||||
'If you only want to verify pending timeouts, use ' +
|
||||
'`$verifyNoPendingTasks(\'$timeout\')` instead.';
|
||||
|
||||
throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' +
|
||||
formattedTasks + extraMessage);
|
||||
}
|
||||
};
|
||||
|
||||
function formatPendingTasksAsString(tasks) {
|
||||
var result = [];
|
||||
angular.forEach(tasks, function(task) {
|
||||
result.push('{id: ' + task.id + ', time: ' + task.time + '}');
|
||||
});
|
||||
|
||||
return result.join(', ');
|
||||
}
|
||||
|
||||
return $delegate;
|
||||
}];
|
||||
|
||||
@@ -2447,7 +2586,9 @@ angular.module('ngMock', ['ng']).provider({
|
||||
$log: angular.mock.$LogProvider,
|
||||
$interval: angular.mock.$IntervalProvider,
|
||||
$rootElement: angular.mock.$RootElementProvider,
|
||||
$componentController: angular.mock.$ComponentControllerProvider
|
||||
$componentController: angular.mock.$ComponentControllerProvider,
|
||||
$flushPendingTasks: angular.mock.$FlushPendingTasksProvider,
|
||||
$verifyNoPendingTasks: angular.mock.$VerifyNoPendingTasksProvider
|
||||
}).config(['$provide', '$compileProvider', function($provide, $compileProvider) {
|
||||
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
|
||||
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
|
||||
@@ -2756,6 +2897,39 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
|
||||
* control how a matched request is handled. You can save this object for later use and invoke
|
||||
* `respond` or `passThrough` again in order to change how a matched request is handled.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $httpBackend#matchLatestDefinition
|
||||
* @module ngMockE2E
|
||||
* @description
|
||||
* This method can be used to change which mocked responses `$httpBackend` returns, when defining
|
||||
* them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
|
||||
* By default, `$httpBackend` returns the first definition that matches. When setting
|
||||
* `$http.matchLatestDefinition(true)`, it will use the last response that matches, i.e. the
|
||||
* one that was added last.
|
||||
*
|
||||
* ```js
|
||||
* hb.when('GET', '/url1').respond(200, 'content', {});
|
||||
* hb.when('GET', '/url1').respond(201, 'another', {});
|
||||
* hb('GET', '/url1'); // receives "content"
|
||||
*
|
||||
* $http.matchLatestDefinition(true)
|
||||
* hb('GET', '/url1'); // receives "another"
|
||||
*
|
||||
* hb.when('GET', '/url1').respond(201, 'onemore', {});
|
||||
* hb('GET', '/url1'); // receives "onemore"
|
||||
* ```
|
||||
*
|
||||
* This is useful if a you have a default response that is overriden inside specific tests.
|
||||
*
|
||||
* Note that different from config methods on providers, `matchLatestDefinition()` can be changed
|
||||
* even when the application is already running.
|
||||
*
|
||||
* @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
|
||||
* If omitted, it will return the current value.
|
||||
* @return {$httpBackend|Boolean} self when used as a setter, and the current value when used
|
||||
* as a getter
|
||||
*/
|
||||
angular.mock.e2e = {};
|
||||
angular.mock.e2e.$httpBackendDecorator =
|
||||
['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock];
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
* - `charcode`: [charCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/charcode)
|
||||
* for keyboard events (keydown, keypress, and keyup).
|
||||
*
|
||||
* - `data`: [data](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/data) for
|
||||
* [CompositionEvents](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent).
|
||||
*
|
||||
* - `elapsedTime`: the elapsedTime for
|
||||
* [TransitionEvent](https://developer.mozilla.org/docs/Web/API/TransitionEvent)
|
||||
* and [AnimationEvent](https://developer.mozilla.org/docs/Web/API/AnimationEvent).
|
||||
|
||||
+48
-52
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
/* global routeToRegExp: false */
|
||||
/* global shallowCopy: false */
|
||||
|
||||
// `isArray` and `isObject` are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
|
||||
@@ -183,11 +184,22 @@ function $RouteProvider() {
|
||||
* `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same
|
||||
* route definition, will cause the latter to be ignored.
|
||||
*
|
||||
* - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes
|
||||
* (including the path) even if the new URL maps to the same route.
|
||||
*
|
||||
* If the option is set to `false` and the URL in the browser changes, but the new URL maps
|
||||
* to the same route, then a `$routeUpdate` event is broadcasted on the root scope (without
|
||||
* reloading the route).
|
||||
*
|
||||
* - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()`
|
||||
* or `$location.hash()` changes.
|
||||
*
|
||||
* If the option is set to `false` and url in the browser changes, then
|
||||
* `$routeUpdate` event is broadcasted on the root scope.
|
||||
* If the option is set to `false` and the URL in the browser changes, then a `$routeUpdate`
|
||||
* event is broadcasted on the root scope (without reloading the route).
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* **Note:** This option has no effect if `reloadOnUrl` is set to `false`.
|
||||
* </div>
|
||||
*
|
||||
* - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive
|
||||
*
|
||||
@@ -202,6 +214,9 @@ function $RouteProvider() {
|
||||
this.when = function(path, route) {
|
||||
//copy original route object to preserve params inherited from proto chain
|
||||
var routeCopy = shallowCopy(route);
|
||||
if (angular.isUndefined(routeCopy.reloadOnUrl)) {
|
||||
routeCopy.reloadOnUrl = true;
|
||||
}
|
||||
if (angular.isUndefined(routeCopy.reloadOnSearch)) {
|
||||
routeCopy.reloadOnSearch = true;
|
||||
}
|
||||
@@ -210,7 +225,7 @@ function $RouteProvider() {
|
||||
}
|
||||
routes[path] = angular.extend(
|
||||
routeCopy,
|
||||
path && pathRegExp(path, routeCopy)
|
||||
path && routeToRegExp(path, routeCopy)
|
||||
);
|
||||
|
||||
// create redirection for trailing slashes
|
||||
@@ -221,7 +236,7 @@ function $RouteProvider() {
|
||||
|
||||
routes[redirectPath] = angular.extend(
|
||||
{redirectTo: path},
|
||||
pathRegExp(redirectPath, routeCopy)
|
||||
routeToRegExp(redirectPath, routeCopy)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,47 +254,6 @@ function $RouteProvider() {
|
||||
*/
|
||||
this.caseInsensitiveMatch = false;
|
||||
|
||||
/**
|
||||
* @param path {string} path
|
||||
* @param opts {Object} options
|
||||
* @return {?Object}
|
||||
*
|
||||
* @description
|
||||
* Normalizes the given path, returning a regular expression
|
||||
* and the original path.
|
||||
*
|
||||
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
|
||||
*/
|
||||
function pathRegExp(path, opts) {
|
||||
var insensitive = opts.caseInsensitiveMatch,
|
||||
ret = {
|
||||
originalPath: path,
|
||||
regexp: path
|
||||
},
|
||||
keys = ret.keys = [];
|
||||
|
||||
path = path
|
||||
.replace(/([().])/g, '\\$1')
|
||||
.replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) {
|
||||
var optional = (option === '?' || option === '*?') ? '?' : null;
|
||||
var star = (option === '*' || option === '*?') ? '*' : null;
|
||||
keys.push({ name: key, optional: !!optional });
|
||||
slash = slash || '';
|
||||
return ''
|
||||
+ (optional ? '' : slash)
|
||||
+ '(?:'
|
||||
+ (optional ? slash : '')
|
||||
+ (star && '(.+?)' || '([^/]+)')
|
||||
+ (optional || '')
|
||||
+ ')'
|
||||
+ (optional || '');
|
||||
})
|
||||
.replace(/([/$*])/g, '\\$1');
|
||||
|
||||
ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $routeProvider#otherwise
|
||||
@@ -544,8 +518,9 @@ function $RouteProvider() {
|
||||
* @name $route#$routeUpdate
|
||||
* @eventType broadcast on root scope
|
||||
* @description
|
||||
* The `reloadOnSearch` property has been set to false, and we are reusing the same
|
||||
* instance of the Controller.
|
||||
* Broadcasted if the same instance of a route (including template, controller instance,
|
||||
* resolved dependencies, etc.) is being reused. This can happen if either `reloadOnSearch` or
|
||||
* `reloadOnUrl` has been set to `false`.
|
||||
*
|
||||
* @param {Object} angularEvent Synthetic event object
|
||||
* @param {Route} current Current/previous route information.
|
||||
@@ -653,9 +628,7 @@ function $RouteProvider() {
|
||||
var lastRoute = $route.current;
|
||||
|
||||
preparedRoute = parseRoute();
|
||||
preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
|
||||
&& angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
|
||||
&& !preparedRoute.reloadOnSearch && !forceReload;
|
||||
preparedRouteIsUpdateOnly = isNavigationUpdateOnly(preparedRoute, lastRoute);
|
||||
|
||||
if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
|
||||
if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
|
||||
@@ -680,7 +653,7 @@ function $RouteProvider() {
|
||||
|
||||
var nextRoutePromise = $q.resolve(nextRoute);
|
||||
|
||||
$browser.$$incOutstandingRequestCount();
|
||||
$browser.$$incOutstandingRequestCount('$route');
|
||||
|
||||
nextRoutePromise.
|
||||
then(getRedirectionData).
|
||||
@@ -708,7 +681,7 @@ function $RouteProvider() {
|
||||
// `outstandingRequestCount` to hit zero. This is important in case we are redirecting
|
||||
// to a new route which also requires some asynchronous work.
|
||||
|
||||
$browser.$$completeOutstandingRequest(noop);
|
||||
$browser.$$completeOutstandingRequest(noop, '$route');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -835,6 +808,29 @@ function $RouteProvider() {
|
||||
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} newRoute - The new route configuration (as returned by `parseRoute()`).
|
||||
* @param {Object} oldRoute - The previous route configuration (as returned by `parseRoute()`).
|
||||
* @returns {boolean} Whether this is an "update-only" navigation, i.e. the URL maps to the same
|
||||
* route and it can be reused (based on the config and the type of change).
|
||||
*/
|
||||
function isNavigationUpdateOnly(newRoute, oldRoute) {
|
||||
// IF this is not a forced reload
|
||||
return !forceReload
|
||||
// AND both `newRoute`/`oldRoute` are defined
|
||||
&& newRoute && oldRoute
|
||||
// AND they map to the same Route Definition Object
|
||||
&& (newRoute.$$route === oldRoute.$$route)
|
||||
// AND `reloadOnUrl` is disabled
|
||||
&& (!newRoute.reloadOnUrl
|
||||
// OR `reloadOnSearch` is disabled
|
||||
|| (!newRoute.reloadOnSearch
|
||||
// AND both routes have the same path params
|
||||
&& angular.equals(newRoute.pathParams, oldRoute.pathParams)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} interpolation of the redirect path with the parameters
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
/* global routeToRegExp: true */
|
||||
|
||||
/**
|
||||
* @param path {string} path
|
||||
* @param opts {Object} options
|
||||
* @return {?Object}
|
||||
*
|
||||
* @description
|
||||
* Normalizes the given path, returning a regular expression
|
||||
* and the original path.
|
||||
*
|
||||
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
|
||||
*/
|
||||
function routeToRegExp(path, opts) {
|
||||
var keys = [];
|
||||
|
||||
var pattern = path
|
||||
.replace(/([().])/g, '\\$1')
|
||||
.replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) {
|
||||
var optional = option === '?' || option === '*?';
|
||||
var star = option === '*' || option === '*?';
|
||||
keys.push({ name: key, optional: optional });
|
||||
slash = slash || '';
|
||||
return (
|
||||
(optional ? '(?:' + slash : slash + '(?:') +
|
||||
(star ? '([^?#]+?)' : '([^/?#]+)') +
|
||||
(optional ? '?)?' : ')')
|
||||
);
|
||||
})
|
||||
.replace(/([/$*])/g, '\\$1');
|
||||
|
||||
if (opts.ignoreTrailingSlashes) {
|
||||
pattern = pattern.replace(/\/+$/, '') + '/*';
|
||||
}
|
||||
|
||||
return {
|
||||
originalPath: path,
|
||||
keys: keys,
|
||||
regexp: new RegExp(
|
||||
'^' + pattern + '(?:[?#]|$)',
|
||||
opts.caseInsensitiveMatch ? 'i' : ''
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -89,6 +89,34 @@ describe('api', function() {
|
||||
expect(map.get(keys[2])).toBe(values[2]);
|
||||
});
|
||||
|
||||
it('should return if a key exists or not', function() {
|
||||
var map = new NgMapShim();
|
||||
var keys = ['foo', {}];
|
||||
|
||||
expect(map.has(keys[0])).toBe(false);
|
||||
expect(map.has(keys[1])).toBe(false);
|
||||
|
||||
map.set(keys[0], 'bar');
|
||||
expect(map.has(keys[0])).toBe(true);
|
||||
expect(map.has(keys[1])).toBe(false);
|
||||
|
||||
map.set(keys[1], 'baz');
|
||||
expect(map.has(keys[0])).toBe(true);
|
||||
expect(map.has(keys[1])).toBe(true);
|
||||
|
||||
map.delete(keys[0]);
|
||||
expect(map.has(keys[0])).toBe(false);
|
||||
expect(map.has(keys[1])).toBe(true);
|
||||
|
||||
map.delete(keys[1]);
|
||||
expect(map.has(keys[0])).toBe(false);
|
||||
expect(map.has(keys[1])).toBe(false);
|
||||
|
||||
map.set(keys[1], 'qux');
|
||||
expect(map.has(keys[0])).toBe(false);
|
||||
expect(map.has(keys[1])).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to deal with `NaN` keys', function() {
|
||||
var map = new NgMapShim();
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="test" require-directive require-target-directive>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script src="angular.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
angular.
|
||||
module('test', []).
|
||||
provider('$exceptionHandler', /** @this */ function() {
|
||||
this.$get = [function() {
|
||||
return function(error) {
|
||||
window.document.querySelector('#container').textContent = error && error.message;
|
||||
};
|
||||
}];
|
||||
}).
|
||||
|
||||
directive('requireDirective', function() {
|
||||
return {
|
||||
require: '^^requireTargetDirective',
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
window.document.querySelector('#container').textContent = ctrl.content;
|
||||
}
|
||||
};
|
||||
}).
|
||||
directive('requireTargetDirective', function() {
|
||||
return {
|
||||
controller: function() {
|
||||
this.content = 'requiredContent';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
describe('require parent controller on html element', function() {
|
||||
it('should not use the html element as the parent element', function() {
|
||||
|
||||
loadFixture('directive-require-html');
|
||||
|
||||
expect(element(by.id('container')).getText()).toContain('Controller \'requireTargetDirective\', required by directive \'requireDirective\', can\'t be found!');
|
||||
});
|
||||
});
|
||||
@@ -313,6 +313,7 @@ beforeEach(function() {
|
||||
|
||||
function generateCompare(isNot) {
|
||||
return function(actual, namespace, code, content) {
|
||||
|
||||
var matcher = new MinErrMatcher(isNot, namespace, code, content, {
|
||||
inputType: 'error',
|
||||
expectedAction: 'equal',
|
||||
|
||||
@@ -501,6 +501,12 @@ describe('jqLite', function() {
|
||||
expect(jqLite(c).data('prop')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not break on cleanData(), if element has no data', function() {
|
||||
var selected = jqLite([a, b, c]);
|
||||
spyOn(jqLite, '_data').and.returnValue(undefined);
|
||||
expect(function() { jqLite.cleanData(selected); }).not.toThrow();
|
||||
});
|
||||
|
||||
|
||||
it('should add and remove data on SVGs', function() {
|
||||
var svg = jqLite('<svg><rect></rect></svg>');
|
||||
|
||||
+49
-17
@@ -2,32 +2,57 @@
|
||||
|
||||
describe('errors', function() {
|
||||
var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth;
|
||||
var originalUrlErrorParamsEnabled = minErrConfig.urlErrorParamsEnabled;
|
||||
|
||||
afterEach(function() {
|
||||
minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage;
|
||||
minErrConfig.urlErrorParamsEnabled = originalUrlErrorParamsEnabled;
|
||||
});
|
||||
|
||||
describe('errorHandlingConfig', function() {
|
||||
it('should get default objectMaxDepth', function() {
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
|
||||
describe('objectMaxDepth',function() {
|
||||
it('should get default objectMaxDepth', function() {
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
|
||||
});
|
||||
|
||||
it('should set objectMaxDepth', function() {
|
||||
errorHandlingConfig({objectMaxDepth: 3});
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(3);
|
||||
});
|
||||
|
||||
it('should not change objectMaxDepth when undefined is supplied', function() {
|
||||
errorHandlingConfig({objectMaxDepth: undefined});
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage);
|
||||
});
|
||||
|
||||
they('should set objectMaxDepth to NaN when $prop is supplied',
|
||||
[NaN, null, true, false, -1, 0], function(maxDepth) {
|
||||
errorHandlingConfig({objectMaxDepth: maxDepth});
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should set objectMaxDepth', function() {
|
||||
errorHandlingConfig({objectMaxDepth: 3});
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(3);
|
||||
|
||||
describe('urlErrorParamsEnabled',function() {
|
||||
|
||||
it('should get default urlErrorParamsEnabled', function() {
|
||||
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should set urlErrorParamsEnabled', function() {
|
||||
errorHandlingConfig({urlErrorParamsEnabled: false});
|
||||
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(false);
|
||||
errorHandlingConfig({urlErrorParamsEnabled: true});
|
||||
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should not change its value when non-boolean is supplied', function() {
|
||||
errorHandlingConfig({urlErrorParamsEnabled: 123});
|
||||
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(originalUrlErrorParamsEnabled);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not change objectMaxDepth when undefined is supplied', function() {
|
||||
errorHandlingConfig({objectMaxDepth: undefined});
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage);
|
||||
});
|
||||
|
||||
they('should set objectMaxDepth to NaN when $prop is supplied',
|
||||
[NaN, null, true, false, -1, 0], function(maxDepth) {
|
||||
errorHandlingConfig({objectMaxDepth: maxDepth});
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('minErr', function() {
|
||||
@@ -165,7 +190,6 @@ describe('errors', function() {
|
||||
.toMatch(/^[\s\S]*\?p0=a&p1=b&p2=value%20with%20space$/);
|
||||
});
|
||||
|
||||
|
||||
it('should strip error reference urls from the error message parameters', function() {
|
||||
var firstError = testError('firstcode', 'longer string and so on');
|
||||
|
||||
@@ -177,5 +201,13 @@ describe('errors', function() {
|
||||
'%3A%2F%2Ferrors.angularjs.org%2F%22NG_VERSION_FULL%22%2Ftest%2Ffirstcode');
|
||||
});
|
||||
|
||||
it('should not generate URL query parameters when urlErrorParamsEnabled is false', function() {
|
||||
|
||||
errorHandlingConfig({urlErrorParamsEnabled: false});
|
||||
|
||||
expect(testError('acode', 'aproblem', 'a', 'b', 'c').message).toBe('[test:acode] aproblem\n' +
|
||||
'https://errors.angularjs.org/"NG_VERSION_FULL"/test/acode');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
+172
-25
@@ -34,9 +34,9 @@ function MockWindow(options) {
|
||||
timeouts[id] = noop;
|
||||
};
|
||||
|
||||
this.setTimeout.flush = function() {
|
||||
var length = timeouts.length;
|
||||
while (length-- > 0) timeouts.shift()();
|
||||
this.setTimeout.flush = function(count) {
|
||||
count = count || timeouts.length;
|
||||
while (count-- > 0) timeouts.shift()();
|
||||
};
|
||||
|
||||
this.addEventListener = function(name, listener) {
|
||||
@@ -143,24 +143,26 @@ function MockDocument() {
|
||||
}
|
||||
|
||||
describe('browser', function() {
|
||||
/* global Browser: false */
|
||||
var browser, fakeWindow, fakeDocument, fakeLog, logs, scripts, removedScripts;
|
||||
/* global Browser: false, TaskTracker: false */
|
||||
var browser, fakeWindow, fakeDocument, fakeLog, logs, taskTrackerFactory;
|
||||
|
||||
beforeEach(function() {
|
||||
scripts = [];
|
||||
removedScripts = [];
|
||||
sniffer = {history: true};
|
||||
fakeWindow = new MockWindow();
|
||||
fakeDocument = new MockDocument();
|
||||
taskTrackerFactory = function(log) { return new TaskTracker(log); };
|
||||
|
||||
logs = {log:[], warn:[], info:[], error:[]};
|
||||
|
||||
fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
|
||||
warn: function() { logs.warn.push(slice.call(arguments)); },
|
||||
info: function() { logs.info.push(slice.call(arguments)); },
|
||||
error: function() { logs.error.push(slice.call(arguments)); }};
|
||||
fakeLog = {
|
||||
log: function() { logs.log.push(slice.call(arguments)); },
|
||||
warn: function() { logs.warn.push(slice.call(arguments)); },
|
||||
info: function() { logs.info.push(slice.call(arguments)); },
|
||||
error: function() { logs.error.push(slice.call(arguments)); }
|
||||
};
|
||||
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
|
||||
});
|
||||
|
||||
describe('MockBrowser', function() {
|
||||
@@ -200,7 +202,7 @@ describe('browser', function() {
|
||||
|
||||
fakeWindow = new MockWindow({msie: msie});
|
||||
fakeWindow.location.state = {prop: 'val'};
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
|
||||
|
||||
browser.url(fakeWindow.location.href, false, {prop: 'val'});
|
||||
if (msie) {
|
||||
@@ -214,12 +216,66 @@ describe('browser', function() {
|
||||
}
|
||||
});
|
||||
|
||||
describe('outstanding requests', function() {
|
||||
it('should process callbacks immediately with no outstanding requests', function() {
|
||||
|
||||
describe('notifyWhenNoOutstandingRequests', function() {
|
||||
it('should invoke callbacks immediately if there are no pending tasks', function() {
|
||||
var callback = jasmine.createSpy('callback');
|
||||
browser.notifyWhenNoOutstandingRequests(callback);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should invoke callbacks immediately if there are no pending tasks (for specific task-type)',
|
||||
function() {
|
||||
var callbackAll = jasmine.createSpy('callbackAll');
|
||||
var callbackFoo = jasmine.createSpy('callbackFoo');
|
||||
|
||||
browser.$$incOutstandingRequestCount();
|
||||
browser.notifyWhenNoOutstandingRequests(callbackAll);
|
||||
browser.notifyWhenNoOutstandingRequests(callbackFoo, 'foo');
|
||||
|
||||
expect(callbackAll).not.toHaveBeenCalled();
|
||||
expect(callbackFoo).toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should invoke callbacks as soon as there are no pending tasks', function() {
|
||||
var callback = jasmine.createSpy('callback');
|
||||
|
||||
browser.$$incOutstandingRequestCount();
|
||||
browser.notifyWhenNoOutstandingRequests(callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
browser.$$completeOutstandingRequest(noop);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should invoke callbacks as soon as there are no pending tasks (for specific task-type)',
|
||||
function() {
|
||||
var callbackAll = jasmine.createSpy('callbackAll');
|
||||
var callbackFoo = jasmine.createSpy('callbackFoo');
|
||||
|
||||
browser.$$incOutstandingRequestCount();
|
||||
browser.$$incOutstandingRequestCount('foo');
|
||||
browser.notifyWhenNoOutstandingRequests(callbackAll);
|
||||
browser.notifyWhenNoOutstandingRequests(callbackFoo, 'foo');
|
||||
|
||||
expect(callbackAll).not.toHaveBeenCalled();
|
||||
expect(callbackFoo).not.toHaveBeenCalled();
|
||||
|
||||
browser.$$completeOutstandingRequest(noop, 'foo');
|
||||
|
||||
expect(callbackAll).not.toHaveBeenCalled();
|
||||
expect(callbackFoo).toHaveBeenCalledOnce();
|
||||
|
||||
browser.$$completeOutstandingRequest(noop);
|
||||
|
||||
expect(callbackAll).toHaveBeenCalledOnce();
|
||||
expect(callbackFoo).toHaveBeenCalledOnce();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -236,13 +292,36 @@ describe('browser', function() {
|
||||
|
||||
|
||||
it('should update outstandingRequests counter', function() {
|
||||
var callback = jasmine.createSpy('deferred');
|
||||
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
|
||||
|
||||
browser.defer(callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
browser.defer(noop);
|
||||
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
|
||||
expect(noPendingTasksSpy).not.toHaveBeenCalled();
|
||||
|
||||
fakeWindow.setTimeout.flush();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
it('should update outstandingRequests counter (for specific task-type)', function() {
|
||||
var noPendingFooTasksSpy = jasmine.createSpy('noPendingFooTasks');
|
||||
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
|
||||
|
||||
browser.defer(noop, 0, 'foo');
|
||||
browser.defer(noop, 0, 'bar');
|
||||
|
||||
browser.notifyWhenNoOutstandingRequests(noPendingFooTasksSpy, 'foo');
|
||||
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
|
||||
expect(noPendingFooTasksSpy).not.toHaveBeenCalled();
|
||||
expect(noPendingTasksSpy).not.toHaveBeenCalled();
|
||||
|
||||
fakeWindow.setTimeout.flush(1);
|
||||
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
|
||||
expect(noPendingTasksSpy).not.toHaveBeenCalled();
|
||||
|
||||
fakeWindow.setTimeout.flush(1);
|
||||
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
|
||||
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
@@ -270,6 +349,40 @@ describe('browser', function() {
|
||||
expect(log).toEqual(['ok']);
|
||||
expect(browser.defer.cancel(deferId2)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should update outstandingRequests counter', function() {
|
||||
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
|
||||
var deferId = browser.defer(noop);
|
||||
|
||||
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
|
||||
expect(noPendingTasksSpy).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.cancel(deferId);
|
||||
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
it('should update outstandingRequests counter (for specific task-type)', function() {
|
||||
var noPendingFooTasksSpy = jasmine.createSpy('noPendingFooTasks');
|
||||
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
|
||||
|
||||
var deferId1 = browser.defer(noop, 0, 'foo');
|
||||
var deferId2 = browser.defer(noop, 0, 'bar');
|
||||
|
||||
browser.notifyWhenNoOutstandingRequests(noPendingFooTasksSpy, 'foo');
|
||||
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
|
||||
expect(noPendingFooTasksSpy).not.toHaveBeenCalled();
|
||||
expect(noPendingTasksSpy).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.cancel(deferId1);
|
||||
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
|
||||
expect(noPendingTasksSpy).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.cancel(deferId2);
|
||||
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
|
||||
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -291,6 +404,14 @@ describe('browser', function() {
|
||||
expect(browser.url()).toEqual('https://another.com');
|
||||
});
|
||||
|
||||
it('should strip an empty hash fragment', function() {
|
||||
fakeWindow.location.href = 'http://test.com#';
|
||||
expect(browser.url()).toEqual('http://test.com');
|
||||
|
||||
fakeWindow.location.href = 'https://another.com#foo';
|
||||
expect(browser.url()).toEqual('https://another.com#foo');
|
||||
});
|
||||
|
||||
it('should use history.pushState when available', function() {
|
||||
sniffer.history = true;
|
||||
browser.url('http://new.org');
|
||||
@@ -462,7 +583,7 @@ describe('browser', function() {
|
||||
// the initial URL contains a lengthy oauth token in the hash
|
||||
var initialUrl = 'http://test.com/oauthcallback#state=xxx%3D¬-before-policy=0';
|
||||
fakeWindow.location.href = initialUrl;
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
|
||||
|
||||
// somehow, $location gets a version of this url where the = is no longer escaped, and tells the browser:
|
||||
var initialUrlFixedByLocation = initialUrl.replace('%3D', '=');
|
||||
@@ -497,7 +618,7 @@ describe('browser', function() {
|
||||
replaceState = spyOn(fakeWindow.history, 'replaceState').and.callThrough();
|
||||
locationReplace = spyOn(fakeWindow.location, 'replace').and.callThrough();
|
||||
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
|
||||
browser.onUrlChange(function() {});
|
||||
});
|
||||
|
||||
@@ -596,7 +717,7 @@ describe('browser', function() {
|
||||
}
|
||||
});
|
||||
|
||||
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer);
|
||||
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer, taskTrackerFactory);
|
||||
|
||||
expect(historyStateAccessed).toBe(false);
|
||||
});
|
||||
@@ -609,7 +730,7 @@ describe('browser', function() {
|
||||
return function() {
|
||||
beforeEach(function() {
|
||||
fakeWindow = new MockWindow({msie: options.msie});
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
|
||||
});
|
||||
|
||||
it('should return history.state', function() {
|
||||
@@ -712,7 +833,7 @@ describe('browser', function() {
|
||||
return function() {
|
||||
beforeEach(function() {
|
||||
fakeWindow = new MockWindow({msie: options.msie});
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
|
||||
});
|
||||
|
||||
it('should fire onUrlChange listeners only once if both popstate and hashchange triggered', function() {
|
||||
@@ -781,7 +902,7 @@ describe('browser', function() {
|
||||
|
||||
function setup(options) {
|
||||
fakeWindow = new MockWindow(options);
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
|
||||
|
||||
module(function($provide, $locationProvider) {
|
||||
|
||||
@@ -934,6 +1055,32 @@ describe('browser', function() {
|
||||
expect($location.absUrl()).toEqual('http://server/#otherHash');
|
||||
});
|
||||
});
|
||||
|
||||
// issue #16632
|
||||
it('should not trigger `$locationChangeStart` more than once due to trailing `#`', function() {
|
||||
setup({
|
||||
history: true,
|
||||
html5Mode: true
|
||||
});
|
||||
|
||||
inject(function($flushPendingTasks, $location, $rootScope) {
|
||||
$rootScope.$digest();
|
||||
|
||||
var spy = jasmine.createSpy('$locationChangeStart');
|
||||
$rootScope.$on('$locationChangeStart', spy);
|
||||
|
||||
$rootScope.$evalAsync(function() {
|
||||
fakeWindow.location.href += '#';
|
||||
});
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(fakeWindow.location.href).toBe('http://server/#');
|
||||
expect($location.absUrl()).toBe('http://server/');
|
||||
|
||||
expect(spy.calls.count()).toBe(0);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test with $rootScope', function() {
|
||||
|
||||
+192
-8
@@ -8843,6 +8843,50 @@ describe('$compile', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should correctly handle multi-element directives', function() {
|
||||
module(function() {
|
||||
directive('foo', valueFn({
|
||||
template: '[<div ng-transclude></div>]',
|
||||
transclude: true
|
||||
}));
|
||||
directive('bar', valueFn({
|
||||
template: '[<div ng-transclude="header"></div>|<div ng-transclude="footer"></div>]',
|
||||
transclude: {
|
||||
header: 'header',
|
||||
footer: 'footer'
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var tmplWithFoo =
|
||||
'<foo>' +
|
||||
'<div ng-if-start="true">Hello, </div>' +
|
||||
'<div ng-if-end>world!</div>' +
|
||||
'</foo>';
|
||||
var tmplWithBar =
|
||||
'<bar>' +
|
||||
'<header ng-if-start="true">This is a </header>' +
|
||||
'<header ng-if-end>header!</header>' +
|
||||
'<footer ng-if-start="true">This is a </footer>' +
|
||||
'<footer ng-if-end>footer!</footer>' +
|
||||
'</bar>';
|
||||
|
||||
var elem1 = $compile(tmplWithFoo)($rootScope);
|
||||
var elem2 = $compile(tmplWithBar)($rootScope);
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(elem1.text()).toBe('[Hello, world!]');
|
||||
expect(elem2.text()).toBe('[This is a header!|This is a footer!]');
|
||||
|
||||
dealoc(elem1);
|
||||
dealoc(elem2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//see issue https://github.com/angular/angular.js/issues/12936
|
||||
it('should use the proper scope when it is on the root element of a replaced directive template', function() {
|
||||
module(function() {
|
||||
@@ -11436,7 +11480,7 @@ describe('$compile', function() {
|
||||
expect(element.attr('srcset')).toEqual('http://example.com');
|
||||
}));
|
||||
|
||||
it('does not work with trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
|
||||
// Use trustAsHtml and ng-bind-html to work around this.
|
||||
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
|
||||
@@ -11661,18 +11705,19 @@ describe('$compile', function() {
|
||||
expect(function() {
|
||||
$compile('<button onclick="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
|
||||
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ONCLICK="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
|
||||
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-attr-onclick="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
|
||||
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
}));
|
||||
|
||||
it('should pass through arbitrary values on onXYZ event attributes that contain a hyphen', inject(function($compile, $rootScope) {
|
||||
@@ -11789,7 +11834,7 @@ describe('$compile', function() {
|
||||
}));
|
||||
|
||||
|
||||
it('should pass through $sce.trustAs() values in action attribute', inject(function($compile, $rootScope, $sce) {
|
||||
it('should pass through $sce.trustAsResourceUrl() values in action attribute', inject(function($compile, $rootScope, $sce) {
|
||||
element = $compile('<form action="{{testUrl}}"></form>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
|
||||
$rootScope.$apply();
|
||||
@@ -11982,6 +12027,39 @@ describe('$compile', function() {
|
||||
expect(element.attr('test3')).toBe('Misko');
|
||||
}));
|
||||
|
||||
it('should use the non-prefixed name in $attr mappings', function() {
|
||||
var attrs;
|
||||
module(function() {
|
||||
directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-attr-title="12" ng-attr-super-title="34" ng-attr-my-camel_title="56">')($rootScope);
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(attrs.title).toBe('12');
|
||||
expect(attrs.$attr.title).toBe('title');
|
||||
expect(attrs.ngAttrTitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrTitle).toBeUndefined();
|
||||
|
||||
expect(attrs.superTitle).toBe('34');
|
||||
expect(attrs.$attr.superTitle).toBe('super-title');
|
||||
expect(attrs.ngAttrSuperTitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrSuperTitle).toBeUndefined();
|
||||
|
||||
// Note the casing is incorrect: https://github.com/angular/angular.js/issues/16624
|
||||
expect(attrs.myCameltitle).toBe('56');
|
||||
expect(attrs.$attr.myCameltitle).toBe('my-camelTitle');
|
||||
expect(attrs.ngAttrMyCameltitle).toBeUndefined();
|
||||
expect(attrs.ngAttrMyCamelTitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrMyCameltitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrMyCamelTitle).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with the "href" attribute', inject(function() {
|
||||
$rootScope.value = 'test';
|
||||
element = $compile('<a ng-attr-href="test/{{value}}"></a>')($rootScope);
|
||||
@@ -12068,6 +12146,112 @@ describe('$compile', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('addPropertySecurityContext', function() {
|
||||
function testProvider(provider) {
|
||||
module(provider);
|
||||
inject(function($compile) { /* done! */ });
|
||||
}
|
||||
|
||||
it('should allow adding new properties', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
$compileProvider.addPropertySecurityContext('*', 'my-prop', 'resourceUrl');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow different sce types of a property on different element types', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
$compileProvider.addPropertySecurityContext('span', 'title', 'css');
|
||||
$compileProvider.addPropertySecurityContext('*', 'title', 'resourceUrl');
|
||||
$compileProvider.addPropertySecurityContext('article', 'title', 'html');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw \'ctxoverride\' when changing an existing context', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
|
||||
expect(function() {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'resourceUrl');
|
||||
})
|
||||
.toThrowMinErr('$compile', 'ctxoverride', 'Property context \'div.title\' already set to \'mediaUrl\', cannot override to \'resourceUrl\'.');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow setting the same property/element to the same value', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
});
|
||||
});
|
||||
|
||||
it('should enforce the specified sce type for properties added for specific elements', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
|
||||
|
||||
$rootScope.bar = 'untrusted:test1';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
|
||||
|
||||
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
|
||||
|
||||
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('untrusted:test3');
|
||||
});
|
||||
});
|
||||
|
||||
it('should enforce the specified sce type for properties added for all elements (*)', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('*', 'foo', 'mediaUrl');
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
|
||||
|
||||
$rootScope.bar = 'untrusted:test1';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
|
||||
|
||||
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
|
||||
|
||||
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('untrusted:test3');
|
||||
});
|
||||
});
|
||||
|
||||
it('should enforce the specific sce type when both an element specific and generic exist', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('*', 'foo', 'css');
|
||||
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
|
||||
|
||||
$rootScope.bar = 'untrusted:test1';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
|
||||
|
||||
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
|
||||
|
||||
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('untrusted:test3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('when an attribute has an underscore-separated name', function() {
|
||||
|
||||
it('should work with different prefixes', inject(function($compile, $rootScope) {
|
||||
|
||||
@@ -1200,6 +1200,52 @@ describe('form', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('$getControls', function() {
|
||||
it('should return an empty array if the controller has no controls', function() {
|
||||
doc = $compile('<form name="testForm"></form>')(scope);
|
||||
|
||||
scope.$digest();
|
||||
|
||||
var formCtrl = scope.testForm;
|
||||
|
||||
expect(formCtrl.$getControls()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return a shallow copy of the form controls', function() {
|
||||
doc = $compile(
|
||||
'<form name="testForm">' +
|
||||
'<input ng-model="named" name="foo">' +
|
||||
'<div ng-form>' +
|
||||
'<input ng-model="named" name="foo">' +
|
||||
'</div>' +
|
||||
'</form>')(scope);
|
||||
|
||||
scope.$digest();
|
||||
|
||||
var form = doc,
|
||||
formCtrl = scope.testForm,
|
||||
formInput = form.children('input').eq(0),
|
||||
formInputCtrl = formInput.controller('ngModel'),
|
||||
nestedForm = form.find('div'),
|
||||
nestedFormCtrl = nestedForm.controller('form'),
|
||||
nestedInput = nestedForm.children('input').eq(0),
|
||||
nestedInputCtrl = nestedInput.controller('ngModel');
|
||||
|
||||
var controls = formCtrl.$getControls();
|
||||
|
||||
expect(controls).not.toBe(formCtrl.$$controls);
|
||||
|
||||
controls.push('something');
|
||||
expect(formCtrl.$$controls).not.toContain('something');
|
||||
|
||||
expect(controls[0]).toBe(formInputCtrl);
|
||||
expect(controls[1]).toBe(nestedFormCtrl);
|
||||
|
||||
var nestedControls = controls[1].$getControls();
|
||||
|
||||
expect(nestedControls[0]).toBe(nestedInputCtrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('should rename nested form controls when interpolated name changes', function() {
|
||||
scope.idA = 'A';
|
||||
|
||||
@@ -1384,6 +1384,88 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.datetimelocal).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should use the timeSecondsFormat specified in ngModelOptions', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="datetime-local" ng-model-options="{timeSecondsFormat: \'\'}" ng-model="time"/>'
|
||||
);
|
||||
|
||||
var ctrl = inputElm.controller('ngModel');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 0, 500);
|
||||
});
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 500);
|
||||
});
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41');
|
||||
|
||||
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss'});
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 5, 500);
|
||||
});
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41:05');
|
||||
|
||||
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss.sss'});
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 50);
|
||||
});
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41:50.050');
|
||||
});
|
||||
|
||||
|
||||
it('should strip empty milliseconds and seconds if specified in ngModelOptions', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="datetime-local" ng-model-options="{timeStripZeroSeconds: true}" ng-model="threeFortyOnePm"/>'
|
||||
);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41:50.500');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41:00.500');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 0);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41:50');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 0);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41');
|
||||
});
|
||||
|
||||
|
||||
it('should apply timeStripZeroSeconds after timeSecondsFormat', function() {
|
||||
var inputElm = helper.compileInput('<input type="datetime-local"' +
|
||||
' ng-model-options="{timeSecondsFormat: \'ss\', timeStripZeroSeconds: true}"' +
|
||||
' ng-model="threeFortyOnePm"/>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41:50');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('1970-01-01T15:41');
|
||||
});
|
||||
|
||||
describe('min', function() {
|
||||
var inputElm;
|
||||
beforeEach(function() {
|
||||
@@ -1593,7 +1675,7 @@ describe('input', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should set the view if the model if a valid Date object.', function() {
|
||||
it('should set the view if the model is a valid Date object.', function() {
|
||||
var inputElm = helper.compileInput('<input type="time" ng-model="threeFortyOnePm"/>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
@@ -1604,7 +1686,7 @@ describe('input', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should set the model undefined if the view is invalid', function() {
|
||||
it('should set the model to undefined if the view is invalid', function() {
|
||||
var inputElm = helper.compileInput('<input type="time" ng-model="breakMe"/>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
@@ -1623,7 +1705,7 @@ describe('input', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should render as blank if null', function() {
|
||||
it('should set blank if null', function() {
|
||||
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
|
||||
|
||||
$rootScope.$apply('test = null');
|
||||
@@ -1633,7 +1715,7 @@ describe('input', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should come up blank when no value specified', function() {
|
||||
it('should set blank when no value specified', function() {
|
||||
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
|
||||
|
||||
expect(inputElm.val()).toBe('');
|
||||
@@ -1644,6 +1726,88 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('');
|
||||
});
|
||||
|
||||
it('should use the timeSecondsFormat specified in ngModelOptions', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="time" ng-model-options="{timeSecondsFormat: \'\'}" ng-model="time"/>'
|
||||
);
|
||||
|
||||
var ctrl = inputElm.controller('ngModel');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 0, 500);
|
||||
});
|
||||
expect(inputElm.val()).toBe('15:41');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 500);
|
||||
});
|
||||
expect(inputElm.val()).toBe('15:41');
|
||||
|
||||
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss'});
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 5, 500);
|
||||
});
|
||||
expect(inputElm.val()).toBe('15:41:05');
|
||||
|
||||
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss.sss'});
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 50);
|
||||
});
|
||||
expect(inputElm.val()).toBe('15:41:50.050');
|
||||
});
|
||||
|
||||
|
||||
it('should strip empty milliseconds and seconds if specified in ngModelOptions', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="time" ng-model-options="{timeStripZeroSeconds: true}" ng-model="threeFortyOnePm"/>'
|
||||
);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('15:41:50.500');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('15:41:00.500');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 0);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('15:41:50');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 0);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('15:41');
|
||||
});
|
||||
|
||||
|
||||
it('should apply timeStripZeroSeconds after timeSecondsFormat', function() {
|
||||
var inputElm = helper.compileInput('<input type="time"' +
|
||||
' ng-model-options="{timeSecondsFormat: \'ss\', timeStripZeroSeconds: true}"' +
|
||||
' ng-model="threeFortyOnePm"/>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('15:41:50');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
|
||||
});
|
||||
|
||||
expect(inputElm.val()).toBe('15:41');
|
||||
});
|
||||
|
||||
|
||||
it('should parse empty string to null', function() {
|
||||
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
|
||||
|
||||
@@ -148,6 +148,133 @@ describe('event directives', function() {
|
||||
expect($rootScope.blur).toHaveBeenCalledOnce();
|
||||
expect(element.val()).toBe('newValue');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should call the listener synchronously if the event is triggered inside of a digest',
|
||||
inject(function($rootScope, $compile) {
|
||||
var watchedVal;
|
||||
|
||||
element = $compile('<button type="button" ng-click="click()">Button</button>')($rootScope);
|
||||
$rootScope.$watch('value', function(newValue) {
|
||||
watchedVal = newValue;
|
||||
});
|
||||
$rootScope.click = jasmine.createSpy('click').and.callFake(function() {
|
||||
$rootScope.value = 'newValue';
|
||||
});
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
element.triggerHandler('click');
|
||||
});
|
||||
|
||||
expect($rootScope.click).toHaveBeenCalledOnce();
|
||||
expect(watchedVal).toEqual('newValue');
|
||||
}));
|
||||
|
||||
|
||||
it('should call the listener synchronously if the event is triggered outside of a digest',
|
||||
inject(function($rootScope, $compile) {
|
||||
var watchedVal;
|
||||
|
||||
element = $compile('<button type="button" ng-click="click()">Button</button>')($rootScope);
|
||||
$rootScope.$watch('value', function(newValue) {
|
||||
watchedVal = newValue;
|
||||
});
|
||||
$rootScope.click = jasmine.createSpy('click').and.callFake(function() {
|
||||
$rootScope.value = 'newValue';
|
||||
});
|
||||
|
||||
element.triggerHandler('click');
|
||||
|
||||
expect($rootScope.click).toHaveBeenCalledOnce();
|
||||
expect(watchedVal).toEqual('newValue');
|
||||
}));
|
||||
|
||||
|
||||
describe('throwing errors in event handlers', function() {
|
||||
|
||||
it('should not stop execution if the event is triggered outside a digest', function() {
|
||||
|
||||
module(function($exceptionHandlerProvider) {
|
||||
$exceptionHandlerProvider.mode('log');
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $exceptionHandler, $log) {
|
||||
|
||||
element = $compile('<button ng-click="click()">Click</button>')($rootScope);
|
||||
expect($log.assertEmpty());
|
||||
$rootScope.click = function() {
|
||||
throw new Error('listener error');
|
||||
};
|
||||
|
||||
$rootScope.do = function() {
|
||||
element.triggerHandler('click');
|
||||
$log.log('done');
|
||||
};
|
||||
|
||||
$rootScope.do();
|
||||
|
||||
expect($exceptionHandler.errors).toEqual([Error('listener error')]);
|
||||
expect($log.log.logs).toEqual([['done']]);
|
||||
$log.reset();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not stop execution if the event is triggered inside a digest', function() {
|
||||
|
||||
module(function($exceptionHandlerProvider) {
|
||||
$exceptionHandlerProvider.mode('log');
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $exceptionHandler, $log) {
|
||||
|
||||
element = $compile('<button ng-click="click()">Click</button>')($rootScope);
|
||||
expect($log.assertEmpty());
|
||||
$rootScope.click = function() {
|
||||
throw new Error('listener error');
|
||||
};
|
||||
|
||||
$rootScope.do = function() {
|
||||
element.triggerHandler('click');
|
||||
$log.log('done');
|
||||
};
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.do();
|
||||
});
|
||||
|
||||
expect($exceptionHandler.errors).toEqual([Error('listener error')]);
|
||||
expect($log.log.logs).toEqual([['done']]);
|
||||
$log.reset();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not stop execution if the event is triggered in a watch expression function', function() {
|
||||
|
||||
module(function($exceptionHandlerProvider) {
|
||||
$exceptionHandlerProvider.mode('log');
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $exceptionHandler, $log) {
|
||||
|
||||
element = $compile('<button ng-click="click()">Click</button>')($rootScope);
|
||||
$rootScope.click = function() {
|
||||
throw new Error('listener error');
|
||||
};
|
||||
|
||||
$rootScope.$watch(function() {
|
||||
element.triggerHandler('click');
|
||||
$log.log('done');
|
||||
});
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect($exceptionHandler.errors).toEqual([Error('listener error'), Error('listener error')]);
|
||||
expect($log.log.logs).toEqual([['done'], ['done']]);
|
||||
$log.reset();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ngModel', function() {
|
||||
|
||||
describe('NgModelController', function() {
|
||||
/* global NgModelController: false */
|
||||
var ctrl, scope, ngModelAccessor, element, parentFormCtrl;
|
||||
var ctrl, scope, element, parentFormCtrl;
|
||||
|
||||
beforeEach(inject(function($rootScope, $controller) {
|
||||
var attrs = {name: 'testAlias', ngModel: 'value'};
|
||||
@@ -21,7 +21,6 @@ describe('ngModel', function() {
|
||||
element = jqLite('<form><input></form>');
|
||||
|
||||
scope = $rootScope;
|
||||
ngModelAccessor = jasmine.createSpy('ngModel accessor');
|
||||
ctrl = $controller(NgModelController, {
|
||||
$scope: scope,
|
||||
$element: element.find('input'),
|
||||
@@ -438,6 +437,13 @@ describe('ngModel', function() {
|
||||
expect(ctrl.$modelValue).toBe('c');
|
||||
expect(scope.value).toBe('c');
|
||||
}));
|
||||
|
||||
|
||||
it('should not throw an error if the scope has been destroyed', function() {
|
||||
scope.$destroy();
|
||||
ctrl.$setViewValue('some-val');
|
||||
expect(ctrl.$viewValue).toBe('some-val');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
'use strict';
|
||||
|
||||
describe('ngRef', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
jasmine.addMatchers({
|
||||
toEqualJq: function(util) {
|
||||
return {
|
||||
compare: function(actual, expected) {
|
||||
// Jquery <= 2.2 objects add a context property that is irrelevant for equality
|
||||
if (actual && actual.hasOwnProperty('context')) {
|
||||
delete actual.context;
|
||||
}
|
||||
|
||||
if (expected && expected.hasOwnProperty('context')) {
|
||||
delete expected.context;
|
||||
}
|
||||
|
||||
return {
|
||||
pass: util.equals(actual, expected)
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('on a component', function() {
|
||||
|
||||
var myComponentController, attributeDirectiveController, $rootScope, $compile;
|
||||
|
||||
beforeEach(module(function($compileProvider) {
|
||||
$compileProvider.component('myComponent', {
|
||||
template: 'foo',
|
||||
controller: function() {
|
||||
myComponentController = this;
|
||||
}
|
||||
});
|
||||
|
||||
$compileProvider.directive('attributeDirective', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
controller: function() {
|
||||
attributeDirectiveController = this;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject(function(_$compile_, _$rootScope_) {
|
||||
$rootScope = _$rootScope_;
|
||||
$compile = _$compile_;
|
||||
}));
|
||||
|
||||
it('should bind in the current scope the controller of a component', function() {
|
||||
$rootScope.$ctrl = 'undamaged';
|
||||
|
||||
$compile('<my-component ng-ref="myComponentRef"></my-component>')($rootScope);
|
||||
expect($rootScope.$ctrl).toBe('undamaged');
|
||||
expect($rootScope.myComponentRef).toBe(myComponentController);
|
||||
});
|
||||
|
||||
it('should throw if the expression is not assignable', function() {
|
||||
expect(function() {
|
||||
$compile('<my-component ng-ref="\'hello\'"></my-component>')($rootScope);
|
||||
}).toThrowMinErr('ngRef', 'nonassign', 'Expression in ngRef="\'hello\'" is non-assignable!');
|
||||
});
|
||||
|
||||
it('should work with non:normalized entity name', function() {
|
||||
$compile('<my:component ng-ref="myComponent1"></my:component>')($rootScope);
|
||||
expect($rootScope.myComponent1).toBe(myComponentController);
|
||||
});
|
||||
|
||||
it('should work with data-non-normalized entity name', function() {
|
||||
$compile('<data-my-component ng-ref="myComponent2"></data-my-component>')($rootScope);
|
||||
expect($rootScope.myComponent2).toBe(myComponentController);
|
||||
});
|
||||
|
||||
it('should work with x-non-normalized entity name', function() {
|
||||
$compile('<x-my-component ng-ref="myComponent3"></x-my-component>')($rootScope);
|
||||
expect($rootScope.myComponent3).toBe(myComponentController);
|
||||
});
|
||||
|
||||
it('should work with data-non-normalized attribute name', function() {
|
||||
$compile('<my-component data-ng-ref="myComponent1"></my-component>')($rootScope);
|
||||
expect($rootScope.myComponent1).toBe(myComponentController);
|
||||
});
|
||||
|
||||
it('should work with x-non-normalized attribute name', function() {
|
||||
$compile('<my-component x-ng-ref="myComponent2"></my-component>')($rootScope);
|
||||
expect($rootScope.myComponent2).toBe(myComponentController);
|
||||
});
|
||||
|
||||
it('should not bind the controller of an attribute directive', function() {
|
||||
$compile('<my-component attribute-directive-1 ng-ref="myComponentRef"></my-component>')($rootScope);
|
||||
expect($rootScope.myComponentRef).toBe(myComponentController);
|
||||
});
|
||||
|
||||
it('should not leak to parent scopes', function() {
|
||||
var template =
|
||||
'<div ng-if="true">' +
|
||||
'<my-component ng-ref="myComponent"></my-component>' +
|
||||
'</div>';
|
||||
$compile(template)($rootScope);
|
||||
expect($rootScope.myComponent).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should nullify the variable once the component is destroyed', function() {
|
||||
var template = '<div><my-component ng-ref="myComponent"></my-component></div>';
|
||||
|
||||
var element = $compile(template)($rootScope);
|
||||
expect($rootScope.myComponent).toBe(myComponentController);
|
||||
|
||||
var componentElement = element.children();
|
||||
var isolateScope = componentElement.isolateScope();
|
||||
componentElement.remove();
|
||||
isolateScope.$destroy();
|
||||
expect($rootScope.myComponent).toBe(null);
|
||||
});
|
||||
|
||||
it('should be compatible with entering/leaving components', inject(function($animate) {
|
||||
var template = '<my-component ng-ref="myComponent"></my-component>';
|
||||
$rootScope.$ctrl = {};
|
||||
var parent = $compile('<div></div>')($rootScope);
|
||||
|
||||
var leaving = $compile(template)($rootScope);
|
||||
var leavingController = myComponentController;
|
||||
|
||||
$animate.enter(leaving, parent);
|
||||
expect($rootScope.myComponent).toBe(leavingController);
|
||||
|
||||
var entering = $compile(template)($rootScope);
|
||||
var enteringController = myComponentController;
|
||||
|
||||
$animate.enter(entering, parent);
|
||||
$animate.leave(leaving, parent);
|
||||
expect($rootScope.myComponent).toBe(enteringController);
|
||||
}));
|
||||
|
||||
it('should allow binding to a nested property', function() {
|
||||
$rootScope.obj = {};
|
||||
|
||||
$compile('<my-component ng-ref="obj.myComponent"></my-component>')($rootScope);
|
||||
expect($rootScope.obj.myComponent).toBe(myComponentController);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should bind the jqlite wrapped DOM element if there is no component', inject(function($compile, $rootScope) {
|
||||
|
||||
var el = $compile('<span ng-ref="mySpan">my text</span>')($rootScope);
|
||||
|
||||
expect($rootScope.mySpan).toEqualJq(el);
|
||||
expect($rootScope.mySpan[0].textContent).toBe('my text');
|
||||
}));
|
||||
|
||||
it('should nullify the expression value if the DOM element is destroyed', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<div><span ng-ref="mySpan">my text</span></div>')($rootScope);
|
||||
element.children().remove();
|
||||
expect($rootScope.mySpan).toBe(null);
|
||||
}));
|
||||
|
||||
it('should bind the controller of an element directive', function() {
|
||||
var myDirectiveController;
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('myDirective', function() {
|
||||
return {
|
||||
controller: function() {
|
||||
myDirectiveController = this;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<my-directive ng-ref="myDirective"></my-directive>')($rootScope);
|
||||
|
||||
expect($rootScope.myDirective).toBe(myDirectiveController);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngRefRead', function() {
|
||||
|
||||
it('should bind the element instead of the controller of a component if ngRefRead="$element" is set', function() {
|
||||
|
||||
module(function($compileProvider) {
|
||||
|
||||
$compileProvider.component('myComponent', {
|
||||
template: 'my text',
|
||||
controller: function() {}
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
|
||||
var el = $compile('<my-component ng-ref="myEl" ng-ref-read="$element"></my-component>')($rootScope);
|
||||
expect($rootScope.myEl).toEqualJq(el);
|
||||
expect($rootScope.myEl[0].textContent).toBe('my text');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should bind the element instead an element-directive controller if ngRefRead="$element" is set', function() {
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('myDirective', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: 'my text',
|
||||
controller: function() {}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var el = $compile('<my-directive ng-ref="myEl" ng-ref-read="$element"></my-directive>')($rootScope);
|
||||
|
||||
expect($rootScope.myEl).toEqualJq(el);
|
||||
expect($rootScope.myEl[0].textContent).toBe('my text');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should bind an attribute-directive controller if ngRefRead="controllerName" is set', function() {
|
||||
var attrDirective1Controller;
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('elementDirective', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: 'my text',
|
||||
controller: function() {}
|
||||
};
|
||||
});
|
||||
|
||||
$compileProvider.directive('attributeDirective1', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
controller: function() {
|
||||
attrDirective1Controller = this;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$compileProvider.directive('attributeDirective2', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
controller: function() {}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var el = $compile('<element-directive' +
|
||||
'attribute-directive-1' +
|
||||
'attribute-directive-2' +
|
||||
'ng-ref="myController"' +
|
||||
'ng-ref-read="$element"></element-directive>')($rootScope);
|
||||
|
||||
expect($rootScope.myController).toBe(attrDirective1Controller);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if no controller is found for the ngRefRead value', function() {
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('elementDirective', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: 'my text',
|
||||
controller: function() {}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
|
||||
expect(function() {
|
||||
$compile('<element-directive ' +
|
||||
'ng-ref="myController"' +
|
||||
'ng-ref-read="attribute"></element-directive>')($rootScope);
|
||||
}).toThrowMinErr('ngRef', 'noctrl', 'The controller for ngRefRead="attribute" could not be found on ngRef="myController"');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should bind the jqlite element if the controller is on an attribute-directive', function() {
|
||||
var myDirectiveController;
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('myDirective', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: 'my text',
|
||||
controller: function() {
|
||||
myDirectiveController = this;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var el = $compile('<div my-directive ng-ref="myEl"></div>')($rootScope);
|
||||
|
||||
expect(myDirectiveController).toBeDefined();
|
||||
expect($rootScope.myEl).toEqualJq(el);
|
||||
expect($rootScope.myEl[0].textContent).toBe('my text');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should bind the jqlite element if the controller is on an class-directive', function() {
|
||||
var myDirectiveController;
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('myDirective', function() {
|
||||
return {
|
||||
restrict: 'C',
|
||||
template: 'my text',
|
||||
controller: function() {
|
||||
myDirectiveController = this;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var el = $compile('<div class="my-directive" ng-ref="myEl"></div>')($rootScope);
|
||||
|
||||
expect(myDirectiveController).toBeDefined();
|
||||
expect($rootScope.myEl).toEqualJq(el);
|
||||
expect($rootScope.myEl[0].textContent).toBe('my text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transclusion', function() {
|
||||
|
||||
it('should work with simple transclusion', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider
|
||||
.component('myComponent', {
|
||||
transclude: true,
|
||||
template: '<ng-transclude></ng-transclude>',
|
||||
controller: function() {
|
||||
this.text = 'SUCCESS';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var template = '<my-component ng-ref="myComponent">{{myComponent.text}}</my-component>';
|
||||
var element = $compile(template)($rootScope);
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toBe('SUCCESS');
|
||||
dealoc(element);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be compatible with element transclude components', function() {
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider
|
||||
.component('myComponent', {
|
||||
transclude: 'element',
|
||||
controller: function($animate, $element, $transclude) {
|
||||
this.text = 'SUCCESS';
|
||||
this.$postLink = function() {
|
||||
$transclude(function(clone, newScope) {
|
||||
$animate.enter(clone, $element.parent(), $element);
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var template =
|
||||
'<div>' +
|
||||
'<my-component ng-ref="myComponent">' +
|
||||
'{{myComponent.text}}' +
|
||||
'</my-component>' +
|
||||
'</div>';
|
||||
var element = $compile(template)($rootScope);
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toBe('SUCCESS');
|
||||
dealoc(element);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be compatible with ngIf and transclusion on same element', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.component('myComponent', {
|
||||
template: '<ng-transclude></ng-transclude>',
|
||||
transclude: true,
|
||||
controller: function($scope) {
|
||||
this.text = 'SUCCESS';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var template =
|
||||
'<div>' +
|
||||
'<my-component ng-if="present" ng-ref="myComponent" >' +
|
||||
'{{myComponent.text}}' +
|
||||
'</my-component>' +
|
||||
'</div>';
|
||||
var element = $compile(template)($rootScope);
|
||||
|
||||
$rootScope.$apply('present = false');
|
||||
expect(element.text()).toBe('');
|
||||
$rootScope.$apply('present = true');
|
||||
expect(element.text()).toBe('SUCCESS');
|
||||
$rootScope.$apply('present = false');
|
||||
expect(element.text()).toBe('');
|
||||
$rootScope.$apply('present = true');
|
||||
expect(element.text()).toBe('SUCCESS');
|
||||
dealoc(element);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be compatible with element transclude & destroy components', function() {
|
||||
var myComponentController;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider
|
||||
.component('myTranscludingComponent', {
|
||||
transclude: 'element',
|
||||
controller: function($animate, $element, $transclude) {
|
||||
myComponentController = this;
|
||||
|
||||
var currentClone, currentScope;
|
||||
this.transclude = function(text) {
|
||||
this.text = text;
|
||||
$transclude(function(clone, newScope) {
|
||||
currentClone = clone;
|
||||
currentScope = newScope;
|
||||
$animate.enter(clone, $element.parent(), $element);
|
||||
});
|
||||
};
|
||||
this.destroy = function() {
|
||||
currentClone.remove();
|
||||
currentScope.$destroy();
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var template =
|
||||
'<div>' +
|
||||
'<my-transcluding-component ng-ref="myComponent">' +
|
||||
'{{myComponent.text}}' +
|
||||
'</my-transcluding-component>' +
|
||||
'</div>';
|
||||
var element = $compile(template)($rootScope);
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toBe('');
|
||||
|
||||
myComponentController.transclude('transcludedOk');
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toBe('transcludedOk');
|
||||
|
||||
myComponentController.destroy();
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be compatible with element transclude directives', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider
|
||||
.directive('myDirective', function($animate) {
|
||||
return {
|
||||
transclude: 'element',
|
||||
controller: function() {
|
||||
this.text = 'SUCCESS';
|
||||
},
|
||||
link: function(scope, element, attrs, ctrl, $transclude) {
|
||||
$transclude(function(clone, newScope) {
|
||||
$animate.enter(clone, element.parent(), element);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
var template =
|
||||
'<div>' +
|
||||
'<my-directive ng-ref="myDirective">' +
|
||||
'{{myDirective.text}}' +
|
||||
'</my-directive>' +
|
||||
'</div>';
|
||||
var element = $compile(template)($rootScope);
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toBe('SUCCESS');
|
||||
dealoc(element);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should work with components with templates via $http', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.component('httpComponent', {
|
||||
templateUrl: 'template.html',
|
||||
controller: function() {
|
||||
this.me = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($compile, $httpBackend, $rootScope) {
|
||||
var template = '<div><http-component ng-ref="controller"></http-component></div>';
|
||||
var element = $compile(template)($rootScope);
|
||||
$httpBackend.expect('GET', 'template.html').respond('ok');
|
||||
$rootScope.$apply();
|
||||
expect($rootScope.controller).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
expect($rootScope.controller.me).toBe(true);
|
||||
dealoc(element);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should work with ngRepeat-ed components', function() {
|
||||
var controllers = [];
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.component('myComponent', {
|
||||
template: 'foo',
|
||||
controller: function() {
|
||||
controllers.push(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
$rootScope.elements = [0,1,2,3,4];
|
||||
$rootScope.controllers = []; // Initialize the array because ngRepeat creates a child scope
|
||||
|
||||
var template = '<div><my-component ng-repeat="(key, el) in elements" ng-ref="controllers[key]"></my-component></div>';
|
||||
var element = $compile(template)($rootScope);
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($rootScope.controllers).toEqual(controllers);
|
||||
|
||||
$rootScope.$apply('elements = []');
|
||||
|
||||
expect($rootScope.controllers).toEqual([null, null, null, null, null]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+20
-20
@@ -2002,7 +2002,7 @@ describe('$http', function() {
|
||||
it('should immediately call `$browser.$$incOutstandingRequestCount()`', function() {
|
||||
expect(incOutstandingRequestCountSpy).not.toHaveBeenCalled();
|
||||
$http.get('');
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
});
|
||||
|
||||
|
||||
@@ -2012,7 +2012,7 @@ describe('$http', function() {
|
||||
$http.get('');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
$httpBackend.flush();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
|
||||
});
|
||||
|
||||
|
||||
@@ -2022,7 +2022,7 @@ describe('$http', function() {
|
||||
$http.get('').catch(noop);
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
$httpBackend.flush();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
|
||||
});
|
||||
|
||||
|
||||
@@ -2033,13 +2033,13 @@ describe('$http', function() {
|
||||
|
||||
$http.get('', {transformRequest: function() { throw new Error(); }}).catch(noop);
|
||||
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2052,13 +2052,13 @@ describe('$http', function() {
|
||||
$httpBackend.when('GET').respond(200);
|
||||
$http.get('', {transformResponse: function() { throw new Error(); }}).catch(noop);
|
||||
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -2112,7 +2112,7 @@ describe('$http', function() {
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(false);
|
||||
expect(resInterceptorFulfilled).toBe(false);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
|
||||
reqInterceptorDeferred.resolve();
|
||||
@@ -2120,7 +2120,7 @@ describe('$http', function() {
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(true);
|
||||
expect(resInterceptorFulfilled).toBe(false);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
|
||||
resInterceptorDeferred.resolve();
|
||||
@@ -2128,8 +2128,8 @@ describe('$http', function() {
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(true);
|
||||
expect(resInterceptorFulfilled).toBe(true);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2144,15 +2144,15 @@ describe('$http', function() {
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(false);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
|
||||
reqInterceptorDeferred.reject();
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(true);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2169,7 +2169,7 @@ describe('$http', function() {
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(false);
|
||||
expect(resInterceptorFulfilled).toBe(false);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
|
||||
reqInterceptorDeferred.resolve();
|
||||
@@ -2177,7 +2177,7 @@ describe('$http', function() {
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(true);
|
||||
expect(resInterceptorFulfilled).toBe(false);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
|
||||
|
||||
resInterceptorDeferred.reject();
|
||||
@@ -2185,8 +2185,8 @@ describe('$http', function() {
|
||||
|
||||
expect(reqInterceptorFulfilled).toBe(true);
|
||||
expect(resInterceptorFulfilled).toBe(true);
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
|
||||
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
|
||||
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
+117
-9
@@ -693,10 +693,10 @@ describe('$location', function() {
|
||||
|
||||
describe('location watch', function() {
|
||||
|
||||
it('should not update browser if only the empty hash fragment is cleared by updating the search', function() {
|
||||
it('should not update browser if only the empty hash fragment is cleared', function() {
|
||||
initService({supportHistory: true});
|
||||
mockUpBrowser({initialUrl:'http://new.com/a/b#', baseHref:'/base/'});
|
||||
inject(function($rootScope, $browser, $location) {
|
||||
mockUpBrowser({initialUrl: 'http://new.com/a/b#', baseHref: '/base/'});
|
||||
inject(function($browser, $rootScope) {
|
||||
$browser.url('http://new.com/a/b');
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough();
|
||||
$rootScope.$digest();
|
||||
@@ -707,10 +707,11 @@ describe('$location', function() {
|
||||
|
||||
it('should not replace browser url if only the empty hash fragment is cleared', function() {
|
||||
initService({html5Mode: true, supportHistory: true});
|
||||
mockUpBrowser({initialUrl:'http://new.com/#', baseHref: '/'});
|
||||
inject(function($browser, $location) {
|
||||
expect($browser.url()).toBe('http://new.com/#');
|
||||
mockUpBrowser({initialUrl: 'http://new.com/#', baseHref: '/'});
|
||||
inject(function($browser, $location, $window) {
|
||||
expect($browser.url()).toBe('http://new.com/');
|
||||
expect($location.absUrl()).toBe('http://new.com/');
|
||||
expect($window.location.href).toBe('http://new.com/#');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -747,6 +748,58 @@ describe('$location', function() {
|
||||
});
|
||||
|
||||
|
||||
//https://github.com/angular/angular.js/issues/16592
|
||||
it('should not infinitely digest when initial params contain a quote', function() {
|
||||
initService({html5Mode:true,supportHistory:true});
|
||||
mockUpBrowser({initialUrl:'http://localhost:9876/?q=\'', baseHref:'/'});
|
||||
inject(function($location, $browser, $rootScope) {
|
||||
expect(function() {
|
||||
$rootScope.$digest();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//https://github.com/angular/angular.js/issues/16592
|
||||
it('should not infinitely digest when initial params contain an escaped quote', function() {
|
||||
initService({html5Mode:true,supportHistory:true});
|
||||
mockUpBrowser({initialUrl:'http://localhost:9876/?q=%27', baseHref:'/'});
|
||||
inject(function($location, $browser, $rootScope) {
|
||||
expect(function() {
|
||||
$rootScope.$digest();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//https://github.com/angular/angular.js/issues/16592
|
||||
it('should not infinitely digest when updating params containing a quote (via $browser.url)', function() {
|
||||
initService({html5Mode:true,supportHistory:true});
|
||||
mockUpBrowser({initialUrl:'http://localhost:9876/', baseHref:'/'});
|
||||
inject(function($location, $browser, $rootScope) {
|
||||
$rootScope.$digest();
|
||||
$browser.url('http://localhost:9876/?q=\'');
|
||||
expect(function() {
|
||||
$rootScope.$digest();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//https://github.com/angular/angular.js/issues/16592
|
||||
it('should not infinitely digest when updating params containing a quote (via window.location + popstate)', function() {
|
||||
initService({html5Mode:true,supportHistory:true});
|
||||
mockUpBrowser({initialUrl:'http://localhost:9876/', baseHref:'/'});
|
||||
inject(function($window, $location, $browser, $rootScope) {
|
||||
$rootScope.$digest();
|
||||
$window.location.href = 'http://localhost:9876/?q=\'';
|
||||
expect(function() {
|
||||
jqLite($window).triggerHandler('popstate');
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('when changing the browser URL/history directly during a `$digest`', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -804,10 +857,13 @@ describe('$location', function() {
|
||||
});
|
||||
|
||||
|
||||
function updatePathOnLocationChangeSuccessTo(newPath) {
|
||||
function updatePathOnLocationChangeSuccessTo(newPath, newParams) {
|
||||
inject(function($rootScope, $location) {
|
||||
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
|
||||
$location.path(newPath);
|
||||
if (newParams) {
|
||||
$location.search(newParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -950,6 +1006,24 @@ describe('$location', function() {
|
||||
expect($browserUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
//https://github.com/angular/angular.js/issues/16592
|
||||
it('should not infinite $digest when going to base URL with trailing slash when $locationChangeSuccess watcher changes query params to contain quote', function() {
|
||||
initService({html5Mode: true, supportHistory: true});
|
||||
mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
|
||||
inject(function($rootScope, $injector, $browser) {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough();
|
||||
|
||||
var $location = $injector.get('$location');
|
||||
updatePathOnLocationChangeSuccessTo('/', {q: '\''});
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect($location.path()).toEqual('/');
|
||||
expect($location.search()).toEqual({q: '\''});
|
||||
expect($browserUrl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1140,6 +1214,40 @@ describe('$location', function() {
|
||||
});
|
||||
});
|
||||
|
||||
//https://github.com/angular/angular.js/issues/16592
|
||||
it('should not infinite $digest on pushState() with quote in param', function() {
|
||||
initService({html5Mode: true, supportHistory: true});
|
||||
mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
|
||||
inject(function($rootScope, $injector, $window) {
|
||||
var $location = $injector.get('$location');
|
||||
$rootScope.$digest(); //allow $location initialization to finish
|
||||
|
||||
$window.history.pushState({}, null, 'http://server/app/Home?q=\'');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect($location.absUrl()).toEqual('http://server/app/Home?q=\'');
|
||||
expect($location.path()).toEqual('/Home');
|
||||
expect($location.search()).toEqual({q: '\''});
|
||||
});
|
||||
});
|
||||
|
||||
//https://github.com/angular/angular.js/issues/16592
|
||||
it('should not infinite $digest on popstate event with quote in param', function() {
|
||||
initService({html5Mode: true, supportHistory: true});
|
||||
mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
|
||||
inject(function($rootScope, $injector, $window) {
|
||||
var $location = $injector.get('$location');
|
||||
$rootScope.$digest(); //allow $location initialization to finish
|
||||
|
||||
$window.location.href = 'http://server/app/Home?q=\'';
|
||||
jqLite($window).triggerHandler('popstate');
|
||||
|
||||
expect($location.absUrl()).toEqual('http://server/app/Home?q=\'');
|
||||
expect($location.path()).toEqual('/Home');
|
||||
expect($location.search()).toEqual({q: '\''});
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace browser url & state when replace() was called at least once', function() {
|
||||
initService({html5Mode:true, supportHistory: true});
|
||||
mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
|
||||
@@ -2766,9 +2874,9 @@ describe('$location', function() {
|
||||
};
|
||||
return win;
|
||||
};
|
||||
$browserProvider.$get = function($document, $window, $log, $sniffer) {
|
||||
$browserProvider.$get = function($document, $window, $log, $sniffer, $$taskTrackerFactory) {
|
||||
/* global Browser: false */
|
||||
browser = new Browser($window, $document, $log, $sniffer);
|
||||
browser = new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory);
|
||||
browser.baseHref = function() {
|
||||
return options.baseHref;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
'use strict';
|
||||
|
||||
describe('ngOn* event binding', function() {
|
||||
it('should add event listener of specified name', inject(function($compile, $rootScope) {
|
||||
$rootScope.name = 'Misko';
|
||||
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
|
||||
element.triggerHandler('foo');
|
||||
expect($rootScope.name).toBe('Misko3');
|
||||
}));
|
||||
|
||||
it('should use angular.element(x).on() API to add listener', inject(function($compile, $rootScope) {
|
||||
spyOn(angular.element.prototype, 'on');
|
||||
|
||||
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
|
||||
|
||||
expect(angular.element.prototype.on).toHaveBeenCalledWith('foo', jasmine.any(Function));
|
||||
}));
|
||||
|
||||
it('should allow access to the $event object', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-foo="e = $event"></span>')($rootScope);
|
||||
element.triggerHandler('foo');
|
||||
expect($rootScope.e.target).toBe(element[0]);
|
||||
}));
|
||||
|
||||
it('should call the listener synchronously', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<span ng-on-foo="fooEvent()"></span>')($rootScope);
|
||||
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
|
||||
|
||||
element.triggerHandler('foo');
|
||||
|
||||
expect($rootScope.fooEvent).toHaveBeenCalledOnce();
|
||||
}));
|
||||
|
||||
it('should support multiple events on a single element', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<span ng-on-foo="fooEvent()" ng-on-bar="barEvent()"></span>')($rootScope);
|
||||
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
|
||||
$rootScope.barEvent = jasmine.createSpy('barEvent');
|
||||
|
||||
element.triggerHandler('foo');
|
||||
expect($rootScope.fooEvent).toHaveBeenCalled();
|
||||
expect($rootScope.barEvent).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.fooEvent.calls.reset();
|
||||
$rootScope.barEvent.calls.reset();
|
||||
|
||||
element.triggerHandler('bar');
|
||||
expect($rootScope.fooEvent).not.toHaveBeenCalled();
|
||||
expect($rootScope.barEvent).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should work with different prefixes', inject(function($rootScope, $compile) {
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
var element = $compile('<span ng:on:test="cb(1)" ng-On-test2="cb(2)" ng_On_test3="cb(3)"></span>')($rootScope);
|
||||
|
||||
element.triggerHandler('test');
|
||||
expect(cb).toHaveBeenCalledWith(1);
|
||||
|
||||
element.triggerHandler('test2');
|
||||
expect(cb).toHaveBeenCalledWith(2);
|
||||
|
||||
element.triggerHandler('test3');
|
||||
expect(cb).toHaveBeenCalledWith(3);
|
||||
}));
|
||||
|
||||
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
var element = $compile('<span data-ng-on-test2="cb(2)" x-ng-on-test3="cb(3)" data-ng:on-test4="cb(4)" ' +
|
||||
'x_ng-on-test5="cb(5)" data:ng-on-test6="cb(6)"></span>')($rootScope);
|
||||
|
||||
element.triggerHandler('test2');
|
||||
expect(cb).toHaveBeenCalledWith(2);
|
||||
|
||||
element.triggerHandler('test3');
|
||||
expect(cb).toHaveBeenCalledWith(3);
|
||||
|
||||
element.triggerHandler('test4');
|
||||
expect(cb).toHaveBeenCalledWith(4);
|
||||
|
||||
element.triggerHandler('test5');
|
||||
expect(cb).toHaveBeenCalledWith(5);
|
||||
|
||||
element.triggerHandler('test6');
|
||||
expect(cb).toHaveBeenCalledWith(6);
|
||||
}));
|
||||
|
||||
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-asdf="cb()" asdf="foo" />')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
element.triggerHandler('asdf');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-asdf="cb()" ng-attr-asdf="foo" />')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
element.triggerHandler('asdf');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should work independently of properties with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-asdf="cb()" ng-prop-asdf="123" />')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
element.triggerHandler('asdf');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
}));
|
||||
|
||||
it('should use the full ng-on-* attribute name in $attr mappings', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-on-title="cb(1)" ng-on-super-title="cb(2)" ng-on-my-camel_title="cb(3)">')($rootScope);
|
||||
|
||||
expect(attrs.title).toBeUndefined();
|
||||
expect(attrs.$attr.title).toBeUndefined();
|
||||
expect(attrs.ngOnTitle).toBe('cb(1)');
|
||||
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
|
||||
|
||||
expect(attrs.superTitle).toBeUndefined();
|
||||
expect(attrs.$attr.superTitle).toBeUndefined();
|
||||
expect(attrs.ngOnSuperTitle).toBe('cb(2)');
|
||||
expect(attrs.$attr.ngOnSuperTitle).toBe('ng-on-super-title');
|
||||
|
||||
expect(attrs.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.$attr.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.ngOnMyCamelTitle).toBe('cb(3)');
|
||||
expect(attrs.$attr.ngOnMyCamelTitle).toBe('ng-on-my-camel_title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-on-title="42" ng-attr-title="foo" title="bar">')($rootScope);
|
||||
expect(attrs.title).toBe('foo');
|
||||
expect(attrs.$attr.title).toBe('title');
|
||||
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,836 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-script-url */
|
||||
|
||||
describe('ngProp*', function() {
|
||||
it('should bind boolean properties (input disabled)', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<button ng-prop-disabled="isDisabled">Button</button>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('disabled')).toBe(false);
|
||||
$rootScope.isDisabled = true;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('disabled')).toBe(true);
|
||||
$rootScope.isDisabled = false;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('disabled')).toBe(false);
|
||||
}));
|
||||
|
||||
it('should bind boolean properties (input checked)', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<input type="checkbox" ng-prop-checked="isChecked" />')($rootScope);
|
||||
expect(element.prop('checked')).toBe(false);
|
||||
$rootScope.isChecked = true;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('checked')).toBe(true);
|
||||
$rootScope.isChecked = false;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('checked')).toBe(false);
|
||||
}));
|
||||
|
||||
it('should bind string properties (title)', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-title="title" />')($rootScope);
|
||||
$rootScope.title = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('title')).toBe('123');
|
||||
$rootScope.title = 'foobar';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('title')).toBe('foobar');
|
||||
}));
|
||||
|
||||
it('should bind variable type properties', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-asdf="asdf" />')($rootScope);
|
||||
$rootScope.asdf = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
$rootScope.asdf = 'foobar';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe('foobar');
|
||||
$rootScope.asdf = true;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(true);
|
||||
}));
|
||||
|
||||
it('should support mixed case using underscore-separated names', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-a_bcd_e="value" />')($rootScope);
|
||||
$rootScope.value = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('aBcdE')).toBe(123);
|
||||
}));
|
||||
|
||||
it('should work with different prefixes', inject(function($rootScope, $compile) {
|
||||
$rootScope.name = 'Misko';
|
||||
var element = $compile('<span ng:prop:test="name" ng-Prop-test2="name" ng_Prop_test3="name"></span>')($rootScope);
|
||||
expect(element.prop('test')).toBe('Misko');
|
||||
expect(element.prop('test2')).toBe('Misko');
|
||||
expect(element.prop('test3')).toBe('Misko');
|
||||
}));
|
||||
|
||||
it('should work with the "href" property', inject(function($rootScope, $compile) {
|
||||
$rootScope.value = 'test';
|
||||
var element = $compile('<a ng-prop-href="\'test/\' + value"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toMatch(/\/test\/test$/);
|
||||
}));
|
||||
|
||||
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
|
||||
$rootScope.name = 'Misko';
|
||||
var element = $compile('<span data-ng-prop-test2="name" x-ng-prop-test3="name" data-ng:prop-test4="name" ' +
|
||||
'x_ng-prop-test5="name" data:ng-prop-test6="name"></span>')($rootScope);
|
||||
expect(element.prop('test2')).toBe('Misko');
|
||||
expect(element.prop('test3')).toBe('Misko');
|
||||
expect(element.prop('test4')).toBe('Misko');
|
||||
expect(element.prop('test5')).toBe('Misko');
|
||||
expect(element.prop('test6')).toBe('Misko');
|
||||
}));
|
||||
|
||||
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-asdf="asdf" asdf="foo" />')($rootScope);
|
||||
$rootScope.asdf = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-asdf="asdf" ng-attr-asdf="foo" />')($rootScope);
|
||||
$rootScope.asdf = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should use the full ng-prop-* attribute name in $attr mappings', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-prop-title="12" ng-prop-super-title="34" ng-prop-my-camel_title="56">')($rootScope);
|
||||
|
||||
expect(attrs.title).toBeUndefined();
|
||||
expect(attrs.$attr.title).toBeUndefined();
|
||||
expect(attrs.ngPropTitle).toBe('12');
|
||||
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
|
||||
|
||||
expect(attrs.superTitle).toBeUndefined();
|
||||
expect(attrs.$attr.superTitle).toBeUndefined();
|
||||
expect(attrs.ngPropSuperTitle).toBe('34');
|
||||
expect(attrs.$attr.ngPropSuperTitle).toBe('ng-prop-super-title');
|
||||
|
||||
expect(attrs.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.$attr.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.ngPropMyCamelTitle).toBe('56');
|
||||
expect(attrs.$attr.ngPropMyCamelTitle).toBe('ng-prop-my-camel_title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-prop-title="42" ng-attr-title="foo" title="bar">')($rootScope);
|
||||
expect(attrs.title).toBe('foo');
|
||||
expect(attrs.$attr.title).toBe('title');
|
||||
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should disallow property binding to onclick', inject(function($compile, $rootScope) {
|
||||
// All event prop bindings are disallowed.
|
||||
expect(function() {
|
||||
$compile('<button ng-prop-onclick="onClickJs"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-prop-ONCLICK="onClickJs"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
}));
|
||||
|
||||
it('should process property bindings in pre-linking phase at priority 100', function() {
|
||||
module(provideLog);
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('propLog', function(log, $rootScope) {
|
||||
return {
|
||||
compile: function($element, $attrs) {
|
||||
log('compile=' + $element.prop('myName'));
|
||||
|
||||
return {
|
||||
pre: function($scope, $element, $attrs) {
|
||||
log('preLinkP0=' + $element.prop('myName'));
|
||||
$rootScope.name = 'pre0';
|
||||
},
|
||||
post: function($scope, $element, $attrs) {
|
||||
log('postLink=' + $element.prop('myName'));
|
||||
$rootScope.name = 'post0';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('propLogHighPriority', function(log, $rootScope) {
|
||||
return {
|
||||
priority: 101,
|
||||
compile: function() {
|
||||
return {
|
||||
pre: function($scope, $element, $attrs) {
|
||||
log('preLinkP101=' + $element.prop('myName'));
|
||||
$rootScope.name = 'pre101';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
inject(function($rootScope, $compile, log) {
|
||||
var element = $compile('<div prop-log-high-priority prop-log ng-prop-my_name="name"></div>')($rootScope);
|
||||
$rootScope.name = 'angular';
|
||||
$rootScope.$apply();
|
||||
log('digest=' + element.prop('myName'));
|
||||
expect(log).toEqual('compile=undefined; preLinkP101=undefined; preLinkP0=pre101; postLink=pre101; digest=angular');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
['img', 'audio', 'video'].forEach(function(tag) {
|
||||
// Support: IE 9 only
|
||||
// IE9 rejects the `video` / `audio` tags with "Error: Not implemented"
|
||||
if (msie !== 9 || tag === 'img') {
|
||||
describe(tag + '[src] context requirement', function() {
|
||||
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('http://example.com/image.mp4');
|
||||
}));
|
||||
|
||||
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('untrusted:foo()');
|
||||
|
||||
// As a URL
|
||||
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('untrusted:foo()');
|
||||
|
||||
// As a RESOURCE URL
|
||||
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('untrusted:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = 'untrusted:foo()';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Support: IE 9 only
|
||||
// IE 9 rejects the `source` / `track` tags with
|
||||
// "Unable to get value of the property 'childNodes': object is null or undefined"
|
||||
if (msie !== 9) {
|
||||
['source', 'track'].forEach(function(tag) {
|
||||
describe(tag + '[src]', function() {
|
||||
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('http://example.com/image.mp4');
|
||||
}));
|
||||
|
||||
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('javascript:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
|
||||
|
||||
// As a URL
|
||||
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
|
||||
|
||||
// As a RESOURCE URL
|
||||
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = 'untrusted:foo()';
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('img[src] sanitization', function() {
|
||||
|
||||
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('someuntrustedthing:foo();');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('someuntrustedthing:foo();');
|
||||
}));
|
||||
|
||||
it('should use $$sanitizeUri', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
|
||||
$rootScope.testUrl = 'someUrl';
|
||||
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('src')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use $$sanitizeUri with trusted values', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.throwError('Should not have been called');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
|
||||
// Assigning javascript:foo to src makes at least IE9-11 complain, so use another
|
||||
// protocol name.
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo();');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('src')).toBe('untrusted:foo();');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
['img', 'source'].forEach(function(srcsetElement) {
|
||||
// Support: IE 9 only
|
||||
// IE9 ignores source[srcset] property assignments
|
||||
if (msie !== 9 || srcsetElement === 'img') {
|
||||
describe(srcsetElement + '[srcset] sanitization', function() {
|
||||
it('should not error if srcset is blank', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
// Set srcset to a value
|
||||
$rootScope.testUrl = 'http://example.com/';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toBe('http://example.com/');
|
||||
|
||||
// Now set it to blank
|
||||
$rootScope.testUrl = '';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toBe('');
|
||||
}));
|
||||
|
||||
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual('http://example.com/image.png');
|
||||
}));
|
||||
|
||||
it('should accept trusted values, if they are also whitelisted', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('http://example.com');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual('http://example.com');
|
||||
}));
|
||||
|
||||
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
|
||||
// Use trustAsHtml and ng-bind-html to work around this.
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual('unsafe:javascript:something');
|
||||
|
||||
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual(
|
||||
'unsafe:javascript:something ,unsafe:javascript:something');
|
||||
}));
|
||||
|
||||
it('should use $$sanitizeUri', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'someUrl';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('srcset')).toBe('someSanitizedUrl');
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
|
||||
|
||||
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:yay';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
|
||||
|
||||
element = $compile('<' + srcsetElement + ' ng-prop-srcset="\'java\' + testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'script:yay, javascript:nay';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize all uris in srcset', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
var testSet = {
|
||||
'http://example.com/image.png':'http://example.com/image.png',
|
||||
' http://example.com/image.png':'http://example.com/image.png',
|
||||
'http://example.com/image.png ':'http://example.com/image.png',
|
||||
'http://example.com/image.png 128w':'http://example.com/image.png 128w',
|
||||
'http://example.com/image.png 2x':'http://example.com/image.png 2x',
|
||||
'http://example.com/image.png 1.5x':'http://example.com/image.png 1.5x',
|
||||
'http://example.com/image1.png 1x,http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 1x ,http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 1x, http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 1x , http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 48w,http://example.com/image2.png 64w':'http://example.com/image1.png 48w,http://example.com/image2.png 64w',
|
||||
//Test regex to make sure doesn't mistake parts of url for width descriptors
|
||||
'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w':'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w',
|
||||
'http://example.com/image1.png 1x,http://example.com/image2.png 64w':'http://example.com/image1.png 1x,http://example.com/image2.png 64w',
|
||||
'http://example.com/image1.png,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png ,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png, http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png , http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png 1x, http://example.com/image2.png 2x, http://example.com/image3.png 3x':
|
||||
'http://example.com/image1.png 1x,http://example.com/image2.png 2x,http://example.com/image3.png 3x',
|
||||
'javascript:doEvilStuff() 2x': 'unsafe:javascript:doEvilStuff() 2x',
|
||||
'http://example.com/image1.png 1x,javascript:doEvilStuff() 2x':'http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x',
|
||||
'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x',
|
||||
//Test regex to make sure doesn't mistake parts of url for pixel density descriptors
|
||||
'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x'
|
||||
};
|
||||
|
||||
forEach(testSet, function(ref, url) {
|
||||
$rootScope.testUrl = url;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual(ref);
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('a[href] sanitization', function() {
|
||||
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile) {
|
||||
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('http://example.com/image.png');
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('http://example.com/image.png');
|
||||
}));
|
||||
|
||||
it('should accept trusted values for non-whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); // `javascript` is not whitelisted
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('javascript:foo()');
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('javascript:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile) {
|
||||
$rootScope.testUrl = 'javascript:foo()'; // `javascript` is not whitelisted
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
|
||||
}));
|
||||
|
||||
it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<div ng-prop-href="testUrl"></div>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:doEvilStuff()';
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('href')).toBe('javascript:doEvilStuff()');
|
||||
}));
|
||||
|
||||
it('should not sanitize properties other then those configured', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<a ng-prop-title="testUrl"></a>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:doEvilStuff()';
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('title')).toBe('javascript:doEvilStuff()');
|
||||
}));
|
||||
|
||||
it('should use $$sanitizeUri', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.testUrl = 'someUrl';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
|
||||
|
||||
$$sanitizeUri.calls.reset();
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not have endless digests when given arrays in concatenable context', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<foo ng-prop-href="testUrl"></foo><foo ng-prop-href="::testUrl"></foo>' +
|
||||
'<foo ng-prop-href="\'http://example.com/\' + testUrl"></foo><foo ng-prop-href="::\'http://example.com/\' + testUrl"></foo>')($rootScope);
|
||||
$rootScope.testUrl = [1];
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.testUrl = [];
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.testUrl = {a:'b'};
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.testUrl = {};
|
||||
$rootScope.$digest();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('iframe[src]', function() {
|
||||
it('should pass through src properties for the same domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = 'different_page';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('src')).toMatch(/\/different_page$/);
|
||||
}));
|
||||
|
||||
it('should clear out src properties for a different domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = 'http://a.different.domain.example.com';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: http://a.different.domain.example.com');
|
||||
}));
|
||||
|
||||
it('should clear out JS src properties', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:alert(1);';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:alert(1);');
|
||||
}));
|
||||
|
||||
it('should clear out non-resource_url src properties', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:doTrustedStuff()');
|
||||
}));
|
||||
|
||||
it('should pass through $sce.trustAs() values in src properties', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('src')).toEqual('javascript:doTrustedStuff()');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('base[href]', function() {
|
||||
it('should be a RESOURCE_URL context', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<base ng-prop-href="testUrl"/>')($rootScope);
|
||||
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('https://example.com/');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toContain('https://example.com/');
|
||||
|
||||
$rootScope.testUrl = 'https://not.example.com/';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: https://not.example.com/');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('form[action]', function() {
|
||||
it('should pass through action property for the same domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = 'different_page';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('action')).toMatch(/\/different_page$/);
|
||||
}));
|
||||
|
||||
it('should clear out action property for a different domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = 'http://a.different.domain.example.com';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: http://a.different.domain.example.com');
|
||||
}));
|
||||
|
||||
it('should clear out JS action property', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:alert(1);';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:alert(1);');
|
||||
}));
|
||||
|
||||
it('should clear out non-resource_url action property', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:doTrustedStuff()');
|
||||
}));
|
||||
|
||||
|
||||
it('should pass through $sce.trustAsResourceUrl() values in action property', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('action')).toEqual('javascript:doTrustedStuff()');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('link[href]', function() {
|
||||
it('should reject invalid RESOURCE_URLs', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
|
||||
$rootScope.testUrl = 'https://evil.example.org/css.css';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: https://evil.example.org/css.css');
|
||||
}));
|
||||
|
||||
it('should accept valid RESOURCE_URLs', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
|
||||
|
||||
$rootScope.testUrl = './css1.css';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toContain('css1.css');
|
||||
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('https://elsewhere.example.org/css2.css');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toContain('https://elsewhere.example.org/css2.css');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('*[innerHTML]', function() {
|
||||
describe('SCE disabled', function() {
|
||||
beforeEach(function() {
|
||||
module(function($sceProvider) { $sceProvider.enabled(false); });
|
||||
});
|
||||
|
||||
it('should set html', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should update html', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = 'hello';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('hello');
|
||||
$rootScope.html = 'goodbye';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('goodbye');
|
||||
}));
|
||||
|
||||
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="::html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('hello');
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('hello');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe('SCE enabled', function() {
|
||||
it('should NOT set html for untrusted values', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
it('should NOT set html for wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = $sce.trustAsCss('<div onclick="">hello</div>');
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
it('should set html for trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = $sce.trustAsHtml('<div onclick="">hello</div>');
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should update html', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = $sce.trustAsHtml('hello');
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('hello');
|
||||
$rootScope.html = $sce.trustAsHtml('goodbye');
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('goodbye');
|
||||
}));
|
||||
|
||||
it('should not cause infinite recursion for trustAsHtml object watches',
|
||||
inject(function($rootScope, $compile, $sce) {
|
||||
// Ref: https://github.com/angular/angular.js/issues/3932
|
||||
// If the binding is a function that creates a new value on every call via trustAs, we'll
|
||||
// trigger an infinite digest if we don't take care of it.
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
|
||||
$rootScope.getHtml = function() {
|
||||
return $sce.trustAsHtml('<div onclick="">hello</div>');
|
||||
};
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should handle custom $sce objects', function() {
|
||||
function MySafeHtml(val) { this.val = val; }
|
||||
|
||||
module(function($provide) {
|
||||
$provide.decorator('$sce', function($delegate) {
|
||||
$delegate.trustAsHtml = function(html) { return new MySafeHtml(html); };
|
||||
$delegate.getTrusted = function(type, mySafeHtml) { return mySafeHtml && mySafeHtml.val; };
|
||||
$delegate.valueOf = function(v) { return v instanceof MySafeHtml ? v.val : v; };
|
||||
return $delegate;
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $sce) {
|
||||
// Ref: https://github.com/angular/angular.js/issues/14526
|
||||
// Previous code used toString for change detection, which fails for custom objects
|
||||
// that don't override toString.
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
|
||||
var html = 'hello';
|
||||
$rootScope.getHtml = function() { return $sce.trustAsHtml(html); };
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('hello');
|
||||
html = 'goodbye';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('goodbye');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $sanitize is available', function() {
|
||||
beforeEach(function() { module('ngSanitize'); });
|
||||
|
||||
it('should sanitize untrusted html', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div>hello</div>');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('*[style]', function() {
|
||||
// Support: IE9
|
||||
// Some browsers throw when assignging to HTMLElement.style
|
||||
function canAssignStyleProp() {
|
||||
try {
|
||||
window.document.createElement('div').style = 'margin-left: 10px';
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
it('should NOT set style for untrusted values', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
|
||||
$rootScope.style = 'margin-left: 10px';
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
it('should NOT set style for wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
|
||||
$rootScope.style = $sce.trustAsHtml('margin-left: 10px');
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
if (canAssignStyleProp()) {
|
||||
it('should set style for trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
|
||||
$rootScope.style = $sce.trustAsCss('margin-left: 10px');
|
||||
$rootScope.$digest();
|
||||
|
||||
// Support: IE
|
||||
// IE allows assignments but does not register the styles
|
||||
// Sometimes the value is '0px', sometimes ''
|
||||
if (msie) {
|
||||
expect(parseInt(element.css('margin-left'), 10) || 0).toBe(0);
|
||||
} else {
|
||||
expect(element.css('margin-left')).toEqual('10px');
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2387,7 +2387,6 @@ describe('Scope', function() {
|
||||
|
||||
|
||||
it('should be cancelled if a $rootScope digest occurs before the next tick', inject(function($rootScope, $browser) {
|
||||
var apply = spyOn($rootScope, '$apply').and.callThrough();
|
||||
var cancel = spyOn($browser.defer, 'cancel').and.callThrough();
|
||||
var expression = jasmine.createSpy('expr');
|
||||
|
||||
|
||||
@@ -194,5 +194,14 @@ describe('$$testability', function() {
|
||||
$$testability.whenStable(callback);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should delegate to `$browser.notifyWhenNoOutstandingRequests()`',
|
||||
inject(function($$testability, $browser) {
|
||||
var spy = spyOn($browser, 'notifyWhenNoOutstandingRequests');
|
||||
var callback = noop;
|
||||
|
||||
$$testability.whenStable(callback);
|
||||
expect(spy).toHaveBeenCalledWith(callback);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"new-cap": "off"
|
||||
},
|
||||
"globals": {
|
||||
"getDomNode": false,
|
||||
"mergeAnimationDetails": false,
|
||||
"prepareAnimationOptions": false,
|
||||
"applyAnimationStyles": false,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
'use strict';
|
||||
|
||||
describe('ngAnimate $$animateCache', function() {
|
||||
beforeEach(module('ngAnimate'));
|
||||
|
||||
it('should store the details in a lookup', inject(function($$animateCache) {
|
||||
var data = { 'hello': 'there' };
|
||||
$$animateCache.put('key', data, true);
|
||||
expect($$animateCache.get('key')).toBe(data);
|
||||
}));
|
||||
|
||||
it('should update existing stored details in a lookup', inject(function($$animateCache) {
|
||||
var data = { 'hello': 'there' };
|
||||
$$animateCache.put('key', data, true);
|
||||
|
||||
var otherData = { 'hi': 'you' };
|
||||
$$animateCache.put('key', otherData, true);
|
||||
expect($$animateCache.get('key')).toBe(otherData);
|
||||
}));
|
||||
|
||||
it('should create a special cacheKey based on the element/parent and className relationship', inject(function($$animateCache) {
|
||||
var cacheKey, elm = jqLite('<div></div>');
|
||||
elm.addClass('one two');
|
||||
|
||||
var parent1 = jqLite('<div></div>');
|
||||
parent1.append(elm);
|
||||
|
||||
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event');
|
||||
expect(cacheKey).toBe('1 event one two');
|
||||
|
||||
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event', 'add');
|
||||
expect(cacheKey).toBe('1 event one two add');
|
||||
|
||||
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event', 'add', 'remove');
|
||||
expect(cacheKey).toBe('1 event one two add remove');
|
||||
|
||||
var parent2 = jqLite('<div></div>');
|
||||
parent2.append(elm);
|
||||
|
||||
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event');
|
||||
expect(cacheKey).toBe('2 event one two');
|
||||
|
||||
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event', 'three', 'four');
|
||||
expect(cacheKey).toBe('2 event one two three four');
|
||||
}));
|
||||
|
||||
it('should keep a count of how many times a cache key has been updated', inject(function($$animateCache) {
|
||||
var data = { 'hello': 'there' };
|
||||
var key = 'key';
|
||||
expect($$animateCache.count(key)).toBe(0);
|
||||
|
||||
$$animateCache.put(key, data, true);
|
||||
expect($$animateCache.count(key)).toBe(1);
|
||||
|
||||
var otherData = { 'other': 'data' };
|
||||
$$animateCache.put(key, otherData, true);
|
||||
expect($$animateCache.count(key)).toBe(2);
|
||||
}));
|
||||
|
||||
it('should flush the cache and the counters', inject(function($$animateCache) {
|
||||
$$animateCache.put('key1', { data: 'value' }, true);
|
||||
$$animateCache.put('key2', { data: 'value' }, true);
|
||||
|
||||
expect($$animateCache.count('key1')).toBe(1);
|
||||
expect($$animateCache.count('key2')).toBe(1);
|
||||
|
||||
$$animateCache.flush();
|
||||
|
||||
expect($$animateCache.get('key1')).toBeFalsy();
|
||||
expect($$animateCache.get('key2')).toBeFalsy();
|
||||
|
||||
expect($$animateCache.count('key1')).toBe(0);
|
||||
expect($$animateCache.count('key2')).toBe(0);
|
||||
}));
|
||||
|
||||
describe('containsCachedAnimationWithoutDuration', function() {
|
||||
it('should return false if the validity of a key is false', inject(function($$animateCache) {
|
||||
var validEntry = { someEssentialProperty: true };
|
||||
var invalidEntry = { someEssentialProperty: false };
|
||||
|
||||
$$animateCache.put('key1', validEntry, true);
|
||||
$$animateCache.put('key2', invalidEntry, false);
|
||||
|
||||
expect($$animateCache.containsCachedAnimationWithoutDuration('key1')).toBe(false);
|
||||
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(true);
|
||||
}));
|
||||
|
||||
it('should return false if the key does not exist in the cache', inject(function($$animateCache) {
|
||||
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(false);
|
||||
|
||||
$$animateCache.put('key2', {}, false);
|
||||
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(true);
|
||||
|
||||
$$animateCache.flush();
|
||||
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
@@ -577,6 +577,41 @@ describe('animations', function() {
|
||||
$rootScope.$digest();
|
||||
expect(capturedAnimation).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should remove the element from the `disabledElementsLookup` map on `$destroy`',
|
||||
inject(function($$Map, $animate, $rootScope) {
|
||||
|
||||
var setSpy = spyOn($$Map.prototype, 'set').and.callThrough();
|
||||
var deleteSpy = spyOn($$Map.prototype, 'delete').and.callThrough();
|
||||
|
||||
parent.append(element);
|
||||
|
||||
$animate.enabled(element, false);
|
||||
$animate.enabled(element, true);
|
||||
$animate.enabled(element, false);
|
||||
expect(setSpy).toHaveBeenCalledWith(element[0], jasmine.any(Boolean));
|
||||
expect(deleteSpy).not.toHaveBeenCalledWith(element[0]);
|
||||
expect($animate.enabled(element)).toBe(false);
|
||||
|
||||
// No clean-up on `detach` (no `$destroy` event).
|
||||
element.detach();
|
||||
expect(deleteSpy).not.toHaveBeenCalledWith(element[0]);
|
||||
expect($animate.enabled(element)).toBe(false);
|
||||
|
||||
// Clean-up on `remove` (causes `$destroy` event).
|
||||
element.remove();
|
||||
expect(deleteSpy).toHaveBeenCalledOnceWith(element[0]);
|
||||
expect($animate.enabled(element)).toBe(true);
|
||||
|
||||
deleteSpy.calls.reset();
|
||||
|
||||
element.triggerHandler('$destroy');
|
||||
expect(deleteSpy).not.toHaveBeenCalledWith(element[0]);
|
||||
|
||||
$animate.enabled(element, true);
|
||||
element.triggerHandler('$destroy');
|
||||
expect(deleteSpy).toHaveBeenCalledOnceWith(element[0]);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should strip all comment nodes from the animation and not issue an animation if not real elements are found',
|
||||
@@ -2784,6 +2819,244 @@ describe('animations', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('event data', function() {
|
||||
|
||||
it('should be included for enter',
|
||||
inject(function($animate, $rootScope, $rootElement, $document) {
|
||||
var eventData;
|
||||
|
||||
$animate.on('enter', jqLite($document[0].body), function(element, phase, data) {
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
$animate.enter(element, $rootElement, null, {
|
||||
addClass: 'red blue',
|
||||
removeClass: 'yellow green',
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
$rootScope.$digest();
|
||||
|
||||
$animate.flush();
|
||||
|
||||
expect(eventData).toEqual({
|
||||
addClass: 'red blue',
|
||||
removeClass: null,
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should be included for leave',
|
||||
inject(function($animate, $rootScope, $rootElement, $document) {
|
||||
var eventData;
|
||||
|
||||
$animate.on('leave', jqLite($document[0].body), function(element, phase, data) {
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
var outerContainer = jqLite('<div></div>');
|
||||
element = jqLite('<div></div>');
|
||||
outerContainer.append(element);
|
||||
$rootElement.append(outerContainer);
|
||||
|
||||
$animate.leave(element, {
|
||||
addClass: 'red blue',
|
||||
removeClass: 'yellow green',
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
|
||||
$animate.flush();
|
||||
|
||||
expect(eventData).toEqual({
|
||||
addClass: 'red blue',
|
||||
removeClass: null,
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
it('should be included for move',
|
||||
inject(function($animate, $rootScope, $rootElement, $document) {
|
||||
var eventData;
|
||||
|
||||
$animate.on('move', jqLite($document[0].body), function(element, phase, data) {
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
var parent = jqLite('<div></div>');
|
||||
var parent2 = jqLite('<div></div>');
|
||||
element = jqLite('<div></div>');
|
||||
parent.append(element);
|
||||
$rootElement.append(parent);
|
||||
$rootElement.append(parent2);
|
||||
|
||||
$animate.move(element, parent2, null, {
|
||||
addClass: 'red blue',
|
||||
removeClass: 'yellow green',
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
|
||||
$animate.flush();
|
||||
|
||||
expect(eventData).toEqual({
|
||||
addClass: 'red blue',
|
||||
removeClass: null,
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
it('should be included for addClass', inject(function($animate, $rootElement) {
|
||||
var eventData;
|
||||
|
||||
element = jqLite('<div class="purple"></div>');
|
||||
$animate.on('addClass', element, function(element, phase, data) {
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
$rootElement.append(element);
|
||||
$animate.addClass(element, 'red blue', {
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
$animate.flush();
|
||||
|
||||
expect(eventData).toEqual({
|
||||
addClass: 'red blue',
|
||||
removeClass: null,
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should be included for removeClass', inject(function($animate, $rootElement) {
|
||||
var eventData;
|
||||
|
||||
element = jqLite('<div class="red blue purple"></div>');
|
||||
$animate.on('removeClass', element, function(element, phase, data) {
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
$rootElement.append(element);
|
||||
$animate.removeClass(element, 'red blue', {
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
$animate.flush();
|
||||
|
||||
expect(eventData).toEqual({
|
||||
removeClass: 'red blue',
|
||||
addClass: null,
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should be included for setClass', inject(function($animate, $rootElement) {
|
||||
var eventData;
|
||||
|
||||
element = jqLite('<div class="yellow green purple"></div>');
|
||||
|
||||
$animate.on('setClass', element, function(element, phase, data) {
|
||||
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
$rootElement.append(element);
|
||||
$animate.setClass(element, 'red blue', 'yellow green', {
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
$animate.flush();
|
||||
|
||||
expect(eventData).toEqual({
|
||||
addClass: 'red blue',
|
||||
removeClass: 'yellow green',
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
}));
|
||||
|
||||
it('should be included for animate', inject(function($animate, $rootElement) {
|
||||
// The event for animate changes to 'setClass' if both addClass and removeClass
|
||||
// are definded, because the operations are merged. However, it is still 'animate'
|
||||
// and not 'addClass' if only 'addClass' is defined.
|
||||
// Ideally, we would make this consistent, but it's a BC
|
||||
var eventData, eventName;
|
||||
|
||||
element = jqLite('<div class="yellow green purple"></div>');
|
||||
|
||||
$animate.on('setClass', element, function(element, phase, data) {
|
||||
eventData = data;
|
||||
eventName = 'setClass';
|
||||
});
|
||||
|
||||
$animate.on('animate', element, function(element, phase, data) {
|
||||
eventData = data;
|
||||
eventName = 'animate';
|
||||
});
|
||||
|
||||
$rootElement.append(element);
|
||||
var runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
|
||||
addClass: 'red blue',
|
||||
removeClass: 'yellow green'
|
||||
});
|
||||
$animate.flush();
|
||||
runner.end();
|
||||
|
||||
expect(eventName).toBe('setClass');
|
||||
expect(eventData).toEqual({
|
||||
addClass: 'red blue',
|
||||
removeClass: 'yellow green',
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
|
||||
eventData = eventName = null;
|
||||
runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
|
||||
addClass: 'yellow green'
|
||||
});
|
||||
|
||||
$animate.flush();
|
||||
runner.end();
|
||||
|
||||
expect(eventName).toBe('animate');
|
||||
expect(eventData).toEqual({
|
||||
addClass: 'yellow green',
|
||||
removeClass: null,
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
|
||||
eventData = eventName = null;
|
||||
runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
|
||||
removeClass: 'yellow green'
|
||||
});
|
||||
|
||||
$animate.flush();
|
||||
runner.end();
|
||||
|
||||
expect(eventName).toBe('animate');
|
||||
expect(eventData).toEqual({
|
||||
addClass: null,
|
||||
removeClass: 'yellow green',
|
||||
from: {opacity: 0},
|
||||
to: {opacity: 1}
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
they('should trigger a callback for a $prop animation if the listener is on the document',
|
||||
['enter', 'leave'], function($event) {
|
||||
module(function($provide) {
|
||||
|
||||
@@ -36,6 +36,9 @@ describe('$$animation', function() {
|
||||
});
|
||||
inject(function($$animation, $animate, $rootScope) {
|
||||
element = jqLite('<div></div>');
|
||||
var parent = jqLite('<div></div>');
|
||||
parent.append(element);
|
||||
|
||||
var done = false;
|
||||
$$animation(element, 'someEvent').then(function() {
|
||||
done = true;
|
||||
@@ -197,7 +200,11 @@ describe('$$animation', function() {
|
||||
});
|
||||
|
||||
inject(function($$animation, $rootScope, $animate) {
|
||||
var status, element = jqLite('<div></div>');
|
||||
var status;
|
||||
var element = jqLite('<div></div>');
|
||||
var parent = jqLite('<div></div>');
|
||||
parent.append(element);
|
||||
|
||||
var runner = $$animation(element, 'enter');
|
||||
runner.then(function() {
|
||||
status = 'resolve';
|
||||
@@ -519,11 +526,24 @@ describe('$$animation', function() {
|
||||
}));
|
||||
|
||||
|
||||
they('should add the preparation class before the $prop-animation is pushed to the queue',
|
||||
they('should only apply the ng-$prop-prepare class if there are a child animations',
|
||||
['enter', 'leave', 'move'], function(animationType) {
|
||||
inject(function($$animation, $rootScope, $animate) {
|
||||
var runner = $$animation(element, animationType);
|
||||
expect(element).toHaveClass('ng-' + animationType + '-prepare');
|
||||
var expectedClassName = 'ng-' + animationType + '-prepare';
|
||||
|
||||
$$animation(element, animationType);
|
||||
$rootScope.$digest();
|
||||
expect(element).not.toHaveClass(expectedClassName);
|
||||
|
||||
var child = jqLite('<div></div>');
|
||||
element.append(child);
|
||||
|
||||
$$animation(element, animationType);
|
||||
$$animation(child, animationType);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(element).not.toHaveClass(expectedClassName);
|
||||
expect(child).toHaveClass(expectedClassName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -531,9 +551,22 @@ describe('$$animation', function() {
|
||||
they('should remove the preparation class before the $prop-animation starts',
|
||||
['enter', 'leave', 'move'], function(animationType) {
|
||||
inject(function($$animation, $rootScope, $$rAF) {
|
||||
var runner = $$animation(element, animationType);
|
||||
var expectedClassName = 'ng-' + animationType + '-prepare';
|
||||
|
||||
var child = jqLite('<div></div>');
|
||||
element.append(child);
|
||||
|
||||
$$animation(element, animationType);
|
||||
$$animation(child, animationType);
|
||||
$rootScope.$digest();
|
||||
expect(element).not.toHaveClass('ng-' + animationType + '-prepare');
|
||||
|
||||
expect(element).not.toHaveClass(expectedClassName);
|
||||
expect(child).toHaveClass(expectedClassName);
|
||||
|
||||
$$rAF.flush();
|
||||
|
||||
expect(element).not.toHaveClass(expectedClassName);
|
||||
expect(child).not.toHaveClass(expectedClassName);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -982,11 +1015,12 @@ describe('$$animation', function() {
|
||||
});
|
||||
inject(function($$animation, $rootScope, $animate) {
|
||||
element.addClass('four');
|
||||
parent.append(element);
|
||||
|
||||
var completed = false;
|
||||
$$animation(element, 'event', {
|
||||
from: { background: 'red' },
|
||||
to: { background: 'blue', 'font-size': '50px' }
|
||||
from: { height: '100px' },
|
||||
to: { height: '200px', 'font-size': '50px' }
|
||||
}).then(function() {
|
||||
completed = true;
|
||||
});
|
||||
@@ -997,7 +1031,7 @@ describe('$$animation', function() {
|
||||
$rootScope.$digest(); //the runner promise
|
||||
|
||||
expect(completed).toBe(true);
|
||||
expect(element.css('background')).toContain('blue');
|
||||
expect(element.css('height')).toContain('200px');
|
||||
expect(element.css('font-size')).toBe('50px');
|
||||
});
|
||||
});
|
||||
@@ -1033,6 +1067,7 @@ describe('$$animation', function() {
|
||||
});
|
||||
});
|
||||
inject(function($$animation, $rootScope, $animate) {
|
||||
parent.append(element);
|
||||
element.addClass('four');
|
||||
|
||||
var completed = false;
|
||||
|
||||
@@ -316,6 +316,8 @@ describe('ngAnimate integration tests', function() {
|
||||
it('should issue a RAF for each element animation on all DOM levels', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
|
||||
ss.addRule('.ng-enter', 'transition:2s linear all;');
|
||||
|
||||
element = jqLite(
|
||||
'<div ng-class="{parent:exp}">' +
|
||||
'<div ng-class="{parent2:exp}">' +
|
||||
@@ -395,6 +397,76 @@ describe('ngAnimate integration tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should avoid adding the ng-enter-prepare method to a parent structural animation that contains child animations', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
|
||||
element = jqLite(
|
||||
'<div ng-animate-children="true">' +
|
||||
'<div ng-if="parent" class="parent">' +
|
||||
'<div ng-if="child" class="child">' +
|
||||
'<div ng-class="{something:true}"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
ss.addRule('.ng-enter', 'transition:2s linear all;');
|
||||
|
||||
$rootElement.append(element);
|
||||
jqLite($document[0].body).append($rootElement);
|
||||
|
||||
$compile(element)($rootScope);
|
||||
$rootScope.parent = true;
|
||||
$rootScope.child = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var parent = jqLite(element[0].querySelector('.parent'));
|
||||
var child = jqLite(element[0].querySelector('.child'));
|
||||
|
||||
expect(parent).not.toHaveClass('ng-enter-prepare');
|
||||
expect(child).toHaveClass('ng-enter-prepare');
|
||||
|
||||
$$rAF.flush();
|
||||
|
||||
expect(parent).not.toHaveClass('ng-enter-prepare');
|
||||
expect(child).not.toHaveClass('ng-enter-prepare');
|
||||
});
|
||||
});
|
||||
|
||||
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
|
||||
element = jqLite(
|
||||
'<div ng-class="{parent:exp}">' +
|
||||
'<div ng-if="exp">' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
ss.addRule('.ng-enter', 'transition:2s linear all;');
|
||||
ss.addRule('.parent-add', 'transition:5s linear all;');
|
||||
|
||||
$rootElement.append(element);
|
||||
jqLite($document[0].body).append($rootElement);
|
||||
|
||||
$compile(element)($rootScope);
|
||||
$rootScope.exp = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var parent = element;
|
||||
var child = element.find('div');
|
||||
|
||||
expect(parent).not.toHaveClass('parent');
|
||||
expect(parent).toHaveClass('parent-add');
|
||||
expect(child).not.toHaveClass('ng-enter');
|
||||
expect(child).toHaveClass('ng-enter-prepare');
|
||||
|
||||
$animate.flush();
|
||||
expect(parent).toHaveClass('parent parent-add parent-add-active');
|
||||
expect(child).toHaveClass('ng-enter ng-enter-active');
|
||||
expect(child).not.toHaveClass('ng-enter-prepare');
|
||||
});
|
||||
});
|
||||
|
||||
it('should pack level elements into their own RAF flush', function() {
|
||||
module('ngAnimateMock');
|
||||
@@ -544,6 +616,45 @@ describe('ngAnimate integration tests', function() {
|
||||
expect(child).not.toHaveClass('blue');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply ngAnimate CSS preparation classes when a css animation definition has duration = 0', function() {
|
||||
function fill(max) {
|
||||
var arr = [];
|
||||
for (var i = 0; i < max; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
inject(function($animate, $rootScope, $compile, $timeout, $$rAF, $$jqLite) {
|
||||
ss.addRule('.animate-me', 'transition: all 0.5s;');
|
||||
|
||||
var classAddSpy = spyOn($$jqLite, 'addClass').and.callThrough();
|
||||
var classRemoveSpy = spyOn($$jqLite, 'removeClass').and.callThrough();
|
||||
|
||||
element = jqLite(
|
||||
'<div>' +
|
||||
'<div ng-repeat="item in items"></div>' +
|
||||
'</div> '
|
||||
);
|
||||
|
||||
html(element);
|
||||
$compile(element)($rootScope);
|
||||
|
||||
$rootScope.items = fill(100);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(classAddSpy.calls.count()).toBe(2);
|
||||
expect(classRemoveSpy.calls.count()).toBe(2);
|
||||
|
||||
expect(classAddSpy.calls.argsFor(0)[1]).toBe('ng-animate');
|
||||
expect(classAddSpy.calls.argsFor(1)[1]).toBe('ng-enter');
|
||||
expect(classRemoveSpy.calls.argsFor(0)[1]).toBe('ng-enter');
|
||||
expect(classRemoveSpy.calls.argsFor(1)[1]).toBe('ng-animate');
|
||||
|
||||
expect(element.children().length).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JS animations', function() {
|
||||
|
||||
+324
-108
@@ -9,17 +9,236 @@ describe('$aria', function() {
|
||||
dealoc(element);
|
||||
});
|
||||
|
||||
function injectScopeAndCompiler() {
|
||||
return inject(function(_$compile_, _$rootScope_) {
|
||||
$compile = _$compile_;
|
||||
scope = _$rootScope_;
|
||||
});
|
||||
}
|
||||
describe('with `ngAriaDisable`', function() {
|
||||
beforeEach(injectScopeAndCompiler);
|
||||
beforeEach(function() {
|
||||
jasmine.addMatchers({
|
||||
toHaveAttribute: function toHaveAttributeMatcher() {
|
||||
return {
|
||||
compare: function toHaveAttributeCompare(element, attr) {
|
||||
var node = element[0];
|
||||
var pass = node.hasAttribute(attr);
|
||||
var message = 'Expected `' + node.outerHTML + '` ' + (pass ? 'not ' : '') +
|
||||
'to have attribute `' + attr + '`.';
|
||||
|
||||
function compileElement(inputHtml) {
|
||||
element = $compile(inputHtml)(scope);
|
||||
scope.$digest();
|
||||
}
|
||||
return {
|
||||
pass: pass,
|
||||
message: message
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ariaChecked
|
||||
it('should not attach aria-checked to custom checkbox', function() {
|
||||
compileElement('<div role="checkbox" ng-model="val" ng-aria-disable></div>');
|
||||
|
||||
scope.$apply('val = false');
|
||||
expect(element).not.toHaveAttribute('aria-checked');
|
||||
|
||||
scope.$apply('val = true');
|
||||
expect(element).not.toHaveAttribute('aria-checked');
|
||||
});
|
||||
|
||||
it('should not attach aria-checked to custom radio controls', function() {
|
||||
compileElement(
|
||||
'<div role="radio" ng-model="val" value="one" ng-aria-disable></div>' +
|
||||
'<div role="radio" ng-model="val" value="two" ng-aria-disable></div>');
|
||||
|
||||
var radio1 = element.eq(0);
|
||||
var radio2 = element.eq(1);
|
||||
|
||||
scope.$apply('val = "one"');
|
||||
expect(radio1).not.toHaveAttribute('aria-checked');
|
||||
expect(radio2).not.toHaveAttribute('aria-checked');
|
||||
|
||||
scope.$apply('val = "two"');
|
||||
expect(radio1).not.toHaveAttribute('aria-checked');
|
||||
expect(radio2).not.toHaveAttribute('aria-checked');
|
||||
});
|
||||
|
||||
// ariaDisabled
|
||||
it('should not attach aria-disabled to custom controls', function() {
|
||||
compileElement('<div ng-disabled="val" ng-aria-disable></div>');
|
||||
|
||||
scope.$apply('val = false');
|
||||
expect(element).not.toHaveAttribute('aria-disabled');
|
||||
|
||||
scope.$apply('val = true');
|
||||
expect(element).not.toHaveAttribute('aria-disabled');
|
||||
});
|
||||
|
||||
// ariaHidden
|
||||
it('should not attach aria-hidden to `ngShow`', function() {
|
||||
compileElement('<div ng-show="val" ng-aria-disable></div>');
|
||||
|
||||
scope.$apply('val = false');
|
||||
expect(element).not.toHaveAttribute('aria-hidden');
|
||||
|
||||
scope.$apply('val = true');
|
||||
expect(element).not.toHaveAttribute('aria-hidden');
|
||||
});
|
||||
|
||||
it('should not attach aria-hidden to `ngHide`', function() {
|
||||
compileElement('<div ng-hide="val" ng-aria-disable></div>');
|
||||
|
||||
scope.$apply('val = false');
|
||||
expect(element).not.toHaveAttribute('aria-hidden');
|
||||
|
||||
scope.$apply('val = true');
|
||||
expect(element).not.toHaveAttribute('aria-hidden');
|
||||
});
|
||||
|
||||
// ariaInvalid
|
||||
it('should not attach aria-invalid to input', function() {
|
||||
compileElement('<input ng-model="val" ng-minlength="10" ng-aria-disable />');
|
||||
|
||||
scope.$apply('val = "lt 10"');
|
||||
expect(element).not.toHaveAttribute('aria-invalid');
|
||||
|
||||
scope.$apply('val = "gt 10 characters"');
|
||||
expect(element).not.toHaveAttribute('aria-invalid');
|
||||
});
|
||||
|
||||
it('should not attach aria-invalid to custom controls', function() {
|
||||
compileElement('<div role="textbox" ng-model="val" ng-minlength="10" ng-aria-disable></div>');
|
||||
|
||||
scope.$apply('val = "lt 10"');
|
||||
expect(element).not.toHaveAttribute('aria-invalid');
|
||||
|
||||
scope.$apply('val = "gt 10 characters"');
|
||||
expect(element).not.toHaveAttribute('aria-invalid');
|
||||
});
|
||||
|
||||
// ariaLive
|
||||
it('should not attach aria-live to `ngMessages`', function() {
|
||||
compileElement('<div ng-messages="val" ng-aria-disable>');
|
||||
expect(element).not.toHaveAttribute('aria-live');
|
||||
});
|
||||
|
||||
// ariaReadonly
|
||||
it('should not attach aria-readonly to custom controls', function() {
|
||||
compileElement('<div ng-readonly="val" ng-aria-disable></div>');
|
||||
|
||||
scope.$apply('val = false');
|
||||
expect(element).not.toHaveAttribute('aria-readonly');
|
||||
|
||||
scope.$apply('val = true');
|
||||
expect(element).not.toHaveAttribute('aria-readonly');
|
||||
});
|
||||
|
||||
// ariaRequired
|
||||
it('should not attach aria-required to custom controls with `required`', function() {
|
||||
compileElement('<div ng-model="val" required ng-aria-disable></div>');
|
||||
expect(element).not.toHaveAttribute('aria-required');
|
||||
});
|
||||
|
||||
it('should not attach aria-required to custom controls with `ngRequired`', function() {
|
||||
compileElement('<div ng-model="val" ng-required="val" ng-aria-disable></div>');
|
||||
|
||||
scope.$apply('val = false');
|
||||
expect(element).not.toHaveAttribute('aria-required');
|
||||
|
||||
scope.$apply('val = true');
|
||||
expect(element).not.toHaveAttribute('aria-required');
|
||||
});
|
||||
|
||||
// ariaValue
|
||||
it('should not attach aria-value* to input[range]', function() {
|
||||
compileElement('<input type="range" ng-model="val" min="0" max="100" ng-aria-disable />');
|
||||
|
||||
expect(element).not.toHaveAttribute('aria-valuemax');
|
||||
expect(element).not.toHaveAttribute('aria-valuemin');
|
||||
expect(element).not.toHaveAttribute('aria-valuenow');
|
||||
|
||||
scope.$apply('val = 50');
|
||||
expect(element).not.toHaveAttribute('aria-valuemax');
|
||||
expect(element).not.toHaveAttribute('aria-valuemin');
|
||||
expect(element).not.toHaveAttribute('aria-valuenow');
|
||||
|
||||
scope.$apply('val = 150');
|
||||
expect(element).not.toHaveAttribute('aria-valuemax');
|
||||
expect(element).not.toHaveAttribute('aria-valuemin');
|
||||
expect(element).not.toHaveAttribute('aria-valuenow');
|
||||
});
|
||||
|
||||
it('should not attach aria-value* to custom controls', function() {
|
||||
compileElement(
|
||||
'<div role="progressbar" ng-model="val" min="0" max="100" ng-aria-disable></div>' +
|
||||
'<div role="slider" ng-model="val" min="0" max="100" ng-aria-disable></div>');
|
||||
|
||||
var progressbar = element.eq(0);
|
||||
var slider = element.eq(1);
|
||||
|
||||
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
|
||||
expect(progressbar).not.toHaveAttribute(attr);
|
||||
expect(slider).not.toHaveAttribute(attr);
|
||||
});
|
||||
|
||||
scope.$apply('val = 50');
|
||||
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
|
||||
expect(progressbar).not.toHaveAttribute(attr);
|
||||
expect(slider).not.toHaveAttribute(attr);
|
||||
});
|
||||
|
||||
scope.$apply('val = 150');
|
||||
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
|
||||
expect(progressbar).not.toHaveAttribute(attr);
|
||||
expect(slider).not.toHaveAttribute(attr);
|
||||
});
|
||||
});
|
||||
|
||||
// bindKeypress
|
||||
it('should not bind keypress to `ngClick`', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
compileElement(
|
||||
'<div ng-click="onClick()" tabindex="0" ng-aria-disable></div>' +
|
||||
'<ul><li ng-click="onClick()" tabindex="0" ng-aria-disable></li></ul>');
|
||||
|
||||
var div = element.find('div');
|
||||
var li = element.find('li');
|
||||
|
||||
div.triggerHandler({type: 'keypress', keyCode: 32});
|
||||
li.triggerHandler({type: 'keypress', keyCode: 32});
|
||||
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// bindRoleForClick
|
||||
it('should not attach role to custom controls', function() {
|
||||
compileElement(
|
||||
'<div ng-click="onClick()" ng-aria-disable></div>' +
|
||||
'<div type="checkbox" ng-model="val" ng-aria-disable></div>' +
|
||||
'<div type="radio" ng-model="val" ng-aria-disable></div>' +
|
||||
'<div type="range" ng-model="val" ng-aria-disable></div>');
|
||||
|
||||
expect(element.eq(0)).not.toHaveAttribute('role');
|
||||
expect(element.eq(1)).not.toHaveAttribute('role');
|
||||
expect(element.eq(2)).not.toHaveAttribute('role');
|
||||
expect(element.eq(3)).not.toHaveAttribute('role');
|
||||
});
|
||||
|
||||
// tabindex
|
||||
it('should not attach tabindex to custom controls', function() {
|
||||
compileElement(
|
||||
'<div role="checkbox" ng-model="val" ng-aria-disable></div>' +
|
||||
'<div role="slider" ng-model="val" ng-aria-disable></div>');
|
||||
|
||||
expect(element.eq(0)).not.toHaveAttribute('tabindex');
|
||||
expect(element.eq(1)).not.toHaveAttribute('tabindex');
|
||||
});
|
||||
|
||||
it('should not attach tabindex to `ngClick` or `ngDblclick`', function() {
|
||||
compileElement(
|
||||
'<div ng-click="onClick()" ng-aria-disable></div>' +
|
||||
'<div ng-dblclick="onDblclick()" ng-aria-disable></div>');
|
||||
|
||||
expect(element.eq(0)).not.toHaveAttribute('tabindex');
|
||||
expect(element.eq(1)).not.toHaveAttribute('tabindex');
|
||||
});
|
||||
});
|
||||
|
||||
describe('aria-hidden', function() {
|
||||
beforeEach(injectScopeAndCompiler);
|
||||
@@ -703,115 +922,100 @@ describe('$aria', function() {
|
||||
});
|
||||
|
||||
describe('accessible actions', function() {
|
||||
var clickEvents;
|
||||
|
||||
beforeEach(injectScopeAndCompiler);
|
||||
beforeEach(function() {
|
||||
clickEvents = [];
|
||||
scope.onClick = jasmine.createSpy('onClick').and.callFake(function(evt) {
|
||||
var nodeName = evt ? evt.target.nodeName.toLowerCase() : '';
|
||||
var prevented = !!(evt && evt.isDefaultPrevented());
|
||||
clickEvents.push(nodeName + '(' + prevented + ')');
|
||||
});
|
||||
});
|
||||
|
||||
var clickFn;
|
||||
it('should trigger a click from the keyboard (and prevent default action)', function() {
|
||||
compileElement(
|
||||
'<section>' +
|
||||
'<div ng-click="onClick($event)"></div>' +
|
||||
'<ul><li ng-click="onClick($event)"></li></ul>' +
|
||||
'</section>');
|
||||
|
||||
it('should trigger a click from the keyboard', function() {
|
||||
scope.someAction = function() {};
|
||||
|
||||
var elements = $compile('<section>' +
|
||||
'<div class="div-click" ng-click="someAction(\'div\')" tabindex="0"></div>' +
|
||||
'<ul><li ng-click="someAction( \'li\')" tabindex="0"></li></ul>' +
|
||||
'</section>')(scope);
|
||||
|
||||
scope.$digest();
|
||||
|
||||
clickFn = spyOn(scope, 'someAction');
|
||||
|
||||
var divElement = elements.find('div');
|
||||
var liElement = elements.find('li');
|
||||
var divElement = element.find('div');
|
||||
var liElement = element.find('li');
|
||||
|
||||
divElement.triggerHandler({type: 'keydown', keyCode: 13});
|
||||
liElement.triggerHandler({type: 'keydown', keyCode: 13});
|
||||
divElement.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
liElement.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
|
||||
expect(clickFn).toHaveBeenCalledWith('div');
|
||||
expect(clickFn).toHaveBeenCalledWith('li');
|
||||
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
|
||||
});
|
||||
|
||||
it('should trigger a click in browsers that provide event.which instead of event.keyCode', function() {
|
||||
scope.someAction = function() {};
|
||||
it('should trigger a click in browsers that provide `event.which` instead of `event.keyCode`',
|
||||
function() {
|
||||
compileElement(
|
||||
'<section>' +
|
||||
'<div ng-click="onClick($event)"></div>' +
|
||||
'<ul><li ng-click="onClick($event)"></li></ul>' +
|
||||
'</section>');
|
||||
|
||||
var elements = $compile('<section>' +
|
||||
'<div class="div-click" ng-click="someAction(\'div\')" tabindex="0"></div>' +
|
||||
'<ul><li ng-click="someAction( \'li\')" tabindex="0"></li></ul>' +
|
||||
'</section>')(scope);
|
||||
var divElement = element.find('div');
|
||||
var liElement = element.find('li');
|
||||
|
||||
scope.$digest();
|
||||
divElement.triggerHandler({type: 'keydown', which: 13});
|
||||
liElement.triggerHandler({type: 'keydown', which: 13});
|
||||
divElement.triggerHandler({type: 'keydown', which: 32});
|
||||
liElement.triggerHandler({type: 'keydown', which: 32});
|
||||
|
||||
clickFn = spyOn(scope, 'someAction');
|
||||
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
|
||||
}
|
||||
);
|
||||
|
||||
var divElement = elements.find('div');
|
||||
var liElement = elements.find('li');
|
||||
they('should not bind to key events if there is existing `ng-$prop`',
|
||||
['keydown', 'keypress', 'keyup'], function(eventName) {
|
||||
scope.onKeyEvent = jasmine.createSpy('onKeyEvent');
|
||||
compileElement('<div ng-click="onClick()" ng-' + eventName + '="onKeyEvent()"></div>');
|
||||
|
||||
divElement.triggerHandler({type: 'keydown', which: 32});
|
||||
liElement.triggerHandler({type: 'keydown', which: 32});
|
||||
element.triggerHandler({type: eventName, keyCode: 13});
|
||||
element.triggerHandler({type: eventName, keyCode: 32});
|
||||
|
||||
expect(clickFn).toHaveBeenCalledWith('div');
|
||||
expect(clickFn).toHaveBeenCalledWith('li');
|
||||
});
|
||||
|
||||
it('should not bind to key events if there is existing ng-keydown', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
scope.onKeydown = jasmine.createSpy('onKeydown');
|
||||
|
||||
var tmpl = '<div ng-click="onClick()" ng-keydown="onKeydown()" tabindex="0"></div>';
|
||||
compileElement(tmpl);
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
|
||||
expect(scope.onKeydown).toHaveBeenCalled();
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not bind to key events if there is existing ng-keypress', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
scope.onKeypress = jasmine.createSpy('onKeypress');
|
||||
|
||||
var tmpl = '<div ng-click="onClick()" ng-keypress="onKeypress()" tabindex="0"></div>';
|
||||
compileElement(tmpl);
|
||||
|
||||
element.triggerHandler({type: 'keypress', keyCode: 32});
|
||||
|
||||
expect(scope.onKeypress).toHaveBeenCalled();
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not bind to key events if there is existing ng-keyup', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
scope.onKeyup = jasmine.createSpy('onKeyup');
|
||||
|
||||
var tmpl = '<div ng-click="onClick()" ng-keyup="onKeyup()" tabindex="0"></div>';
|
||||
compileElement(tmpl);
|
||||
|
||||
element.triggerHandler({type: 'keyup', keyCode: 32});
|
||||
|
||||
expect(scope.onKeyup).toHaveBeenCalled();
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
expect(scope.onKeyEvent).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
);
|
||||
|
||||
it('should update bindings when keydown is handled', function() {
|
||||
compileElement('<div ng-click="text = \'clicked!\'">{{text}}</div>');
|
||||
expect(element.text()).toBe('');
|
||||
spyOn(scope.$root, '$digest').and.callThrough();
|
||||
element.triggerHandler({ type: 'keydown', keyCode: 13 });
|
||||
expect(element.text()).toBe('clicked!');
|
||||
expect(scope.$root.$digest).toHaveBeenCalledOnce();
|
||||
scope.count = 0;
|
||||
compileElement('<div ng-click="count = count + 1">Count: {{ count }}</div>');
|
||||
|
||||
expect(element.text()).toBe('Count: 0');
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 13});
|
||||
expect(element.text()).toBe('Count: 1');
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
expect(element.text()).toBe('Count: 2');
|
||||
});
|
||||
|
||||
it('should pass $event to ng-click handler as local', function() {
|
||||
compileElement('<div ng-click="event = $event">{{event.type}}' +
|
||||
'{{event.keyCode}}</div>');
|
||||
it('should pass `$event` to `ng-click` handler as local', function() {
|
||||
compileElement('<div ng-click="event = $event">{{ event.type }}{{ event.keyCode }}</div>');
|
||||
expect(element.text()).toBe('');
|
||||
element.triggerHandler({ type: 'keydown', keyCode: 13 });
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 13});
|
||||
expect(element.text()).toBe('keydown13');
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
expect(element.text()).toBe('keydown32');
|
||||
});
|
||||
|
||||
it('should not bind keydown to natively interactive elements', function() {
|
||||
compileElement('<button ng-click="event = $event">{{event.type}}{{event.keyCode}}</button>');
|
||||
expect(element.text()).toBe('');
|
||||
element.triggerHandler({ type: 'keydown', keyCode: 13 });
|
||||
expect(element.text()).toBe('');
|
||||
compileElement('<button ng-click="onClick()">Click me</button>');
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 13});
|
||||
element.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -895,19 +1099,31 @@ describe('$aria', function() {
|
||||
expect(element.attr('tabindex')).toBe('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectAriaAttrOnEachElement(elem, ariaAttr, expected) {
|
||||
angular.forEach(elem, function(val) {
|
||||
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
|
||||
});
|
||||
}
|
||||
// Helpers
|
||||
function compileElement(inputHtml) {
|
||||
element = $compile(inputHtml)(scope);
|
||||
scope.$digest();
|
||||
}
|
||||
|
||||
function configAriaProvider(config) {
|
||||
return function() {
|
||||
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider) {
|
||||
$ariaProvider.config(config);
|
||||
function configAriaProvider(config) {
|
||||
return function() {
|
||||
module(function($ariaProvider) {
|
||||
$ariaProvider.config(config);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function expectAriaAttrOnEachElement(elem, ariaAttr, expected) {
|
||||
angular.forEach(elem, function(val) {
|
||||
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
|
||||
});
|
||||
module('ariaTest');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function injectScopeAndCompiler() {
|
||||
return inject(function(_$compile_, _$rootScope_) {
|
||||
$compile = _$compile_;
|
||||
scope = _$rootScope_;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -661,6 +661,100 @@ describe('ngMessages', function() {
|
||||
);
|
||||
|
||||
|
||||
describe('default message', function() {
|
||||
it('should render a default message when no message matches', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-messages="col">' +
|
||||
' <div ng-message="val">Message is set</div>' +
|
||||
' <div ng-message-default>Default message is set</div>' +
|
||||
'</div>')($rootScope);
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { unexpected: false };
|
||||
});
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(element.text().trim()).toBe('');
|
||||
expect(element).not.toHaveClass('ng-active');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { unexpected: true };
|
||||
});
|
||||
|
||||
expect(element.text().trim()).toBe('Default message is set');
|
||||
expect(element).toHaveClass('ng-active');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { unexpected: false };
|
||||
});
|
||||
|
||||
expect(element.text().trim()).toBe('');
|
||||
expect(element).not.toHaveClass('ng-active');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { val: true, unexpected: true };
|
||||
});
|
||||
|
||||
expect(element.text().trim()).toBe('Message is set');
|
||||
expect(element).toHaveClass('ng-active');
|
||||
}));
|
||||
|
||||
it('should not render a default message with ng-messages-multiple if another error matches',
|
||||
inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-messages="col" ng-messages-multiple>' +
|
||||
' <div ng-message="val">Message is set</div>' +
|
||||
' <div ng-message="other">Other message is set</div>' +
|
||||
' <div ng-message-default>Default message is set</div>' +
|
||||
'</div>')($rootScope);
|
||||
|
||||
expect(element.text().trim()).toBe('');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { val: true, other: false, unexpected: false };
|
||||
});
|
||||
|
||||
expect(element.text().trim()).toBe('Message is set');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { val: true, other: true, unexpected: true };
|
||||
});
|
||||
|
||||
expect(element.text().trim()).toBe('Message is set Other message is set');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { val: false, other: false, unexpected: true };
|
||||
});
|
||||
|
||||
expect(element.text().trim()).toBe('Default message is set');
|
||||
})
|
||||
);
|
||||
|
||||
it('should handle a default message with ngIf', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-messages="col">' +
|
||||
' <div ng-message="val">Message is set</div>' +
|
||||
' <div ng-if="default" ng-message-default>Default message is set</div>' +
|
||||
'</div>')($rootScope);
|
||||
$rootScope.default = true;
|
||||
$rootScope.col = {unexpected: true};
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(element.text().trim()).toBe('Default message is set');
|
||||
|
||||
$rootScope.$apply('default = false');
|
||||
|
||||
expect(element.text().trim()).toBe('');
|
||||
|
||||
$rootScope.$apply('default = true');
|
||||
|
||||
expect(element.text().trim()).toBe('Default message is set');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = { val: true };
|
||||
});
|
||||
|
||||
expect(element.text().trim()).toBe('Message is set');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when including templates', function() {
|
||||
they('should work with a dynamic collection model which is managed by ngRepeat',
|
||||
{'<div ng-messages-include="...">': '<div ng-messages="item">' +
|
||||
|
||||
Vendored
+403
-52
@@ -323,16 +323,16 @@ describe('ngMock', function() {
|
||||
|
||||
it('should NOT call $apply if invokeApply is set to false',
|
||||
inject(function($interval, $rootScope) {
|
||||
var applySpy = spyOn($rootScope, '$apply').and.callThrough();
|
||||
var digestSpy = spyOn($rootScope, '$digest').and.callThrough();
|
||||
|
||||
var counter = 0;
|
||||
$interval(function increment() { counter++; }, 1000, 0, false);
|
||||
|
||||
expect(applySpy).not.toHaveBeenCalled();
|
||||
expect(digestSpy).not.toHaveBeenCalled();
|
||||
expect(counter).toBe(0);
|
||||
|
||||
$interval.flush(2000);
|
||||
expect(applySpy).not.toHaveBeenCalled();
|
||||
expect(digestSpy).not.toHaveBeenCalled();
|
||||
expect(counter).toBe(2);
|
||||
}));
|
||||
|
||||
@@ -601,7 +601,7 @@ describe('ngMock', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('defer', function() {
|
||||
describe('$browser', function() {
|
||||
var browser, log;
|
||||
beforeEach(inject(function($browser) {
|
||||
browser = $browser;
|
||||
@@ -614,47 +614,292 @@ describe('ngMock', function() {
|
||||
};
|
||||
}
|
||||
|
||||
it('should flush', function() {
|
||||
browser.defer(logFn('A'));
|
||||
expect(log).toEqual('');
|
||||
browser.defer.flush();
|
||||
expect(log).toEqual('A;');
|
||||
describe('defer.flush', function() {
|
||||
it('should flush', function() {
|
||||
browser.defer(logFn('A'));
|
||||
browser.defer(logFn('B'), null, 'taskType');
|
||||
expect(log).toEqual('');
|
||||
|
||||
browser.defer.flush();
|
||||
expect(log).toEqual('A;B;');
|
||||
});
|
||||
|
||||
it('should flush delayed', function() {
|
||||
browser.defer(logFn('A'));
|
||||
browser.defer(logFn('B'), 0, 'taskTypeB');
|
||||
browser.defer(logFn('C'), 10, 'taskTypeC');
|
||||
browser.defer(logFn('D'), 20);
|
||||
expect(log).toEqual('');
|
||||
expect(browser.defer.now).toEqual(0);
|
||||
|
||||
browser.defer.flush(0);
|
||||
expect(log).toEqual('A;B;');
|
||||
|
||||
browser.defer.flush();
|
||||
expect(log).toEqual('A;B;C;D;');
|
||||
});
|
||||
|
||||
it('should defer and flush over time', function() {
|
||||
browser.defer(logFn('A'), 1);
|
||||
browser.defer(logFn('B'), 2, 'taskType');
|
||||
browser.defer(logFn('C'), 3);
|
||||
|
||||
browser.defer.flush(0);
|
||||
expect(browser.defer.now).toEqual(0);
|
||||
expect(log).toEqual('');
|
||||
|
||||
browser.defer.flush(1);
|
||||
expect(browser.defer.now).toEqual(1);
|
||||
expect(log).toEqual('A;');
|
||||
|
||||
browser.defer.flush(2);
|
||||
expect(browser.defer.now).toEqual(3);
|
||||
expect(log).toEqual('A;B;C;');
|
||||
});
|
||||
|
||||
it('should throw an exception if there is nothing to be flushed', function() {
|
||||
expect(function() {browser.defer.flush();}).toThrowError('No deferred tasks to be flushed');
|
||||
});
|
||||
|
||||
it('should not throw an exception when passing a specific delay', function() {
|
||||
expect(function() {browser.defer.flush(100);}).not.toThrow();
|
||||
});
|
||||
|
||||
describe('tasks scheduled during flushing', function() {
|
||||
it('should be flushed if they do not exceed the target delay (when no delay specified)',
|
||||
function() {
|
||||
browser.defer(function() {
|
||||
logFn('1')();
|
||||
browser.defer(function() {
|
||||
logFn('3')();
|
||||
browser.defer(logFn('4'), 1);
|
||||
}, 2);
|
||||
}, 1);
|
||||
browser.defer(function() {
|
||||
logFn('2')();
|
||||
browser.defer(logFn('6'), 4);
|
||||
}, 2);
|
||||
browser.defer(logFn('5'), 5);
|
||||
|
||||
browser.defer.flush(0);
|
||||
expect(browser.defer.now).toEqual(0);
|
||||
expect(log).toEqual('');
|
||||
|
||||
browser.defer.flush();
|
||||
expect(browser.defer.now).toEqual(5);
|
||||
expect(log).toEqual('1;2;3;4;5;');
|
||||
}
|
||||
);
|
||||
|
||||
it('should be flushed if they do not exceed the specified delay',
|
||||
function() {
|
||||
browser.defer(function() {
|
||||
logFn('1')();
|
||||
browser.defer(function() {
|
||||
logFn('3')();
|
||||
browser.defer(logFn('4'), 1);
|
||||
}, 2);
|
||||
}, 1);
|
||||
browser.defer(function() {
|
||||
logFn('2')();
|
||||
browser.defer(logFn('6'), 4);
|
||||
}, 2);
|
||||
browser.defer(logFn('5'), 5);
|
||||
|
||||
browser.defer.flush(0);
|
||||
expect(browser.defer.now).toEqual(0);
|
||||
expect(log).toEqual('');
|
||||
|
||||
browser.defer.flush(4);
|
||||
expect(browser.defer.now).toEqual(4);
|
||||
expect(log).toEqual('1;2;3;4;');
|
||||
|
||||
browser.defer.flush(6);
|
||||
expect(browser.defer.now).toEqual(10);
|
||||
expect(log).toEqual('1;2;3;4;5;6;');
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should flush delayed', function() {
|
||||
browser.defer(logFn('A'));
|
||||
browser.defer(logFn('B'), 10);
|
||||
browser.defer(logFn('C'), 20);
|
||||
expect(log).toEqual('');
|
||||
describe('defer.cancel', function() {
|
||||
it('should cancel a pending task', function() {
|
||||
var taskId1 = browser.defer(logFn('A'), 100, 'fooType');
|
||||
var taskId2 = browser.defer(logFn('B'), 200);
|
||||
|
||||
expect(browser.defer.now).toEqual(0);
|
||||
browser.defer.flush(0);
|
||||
expect(log).toEqual('A;');
|
||||
expect(log).toBe('');
|
||||
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).toThrow();
|
||||
expect(function() {browser.defer.verifyNoPendingTasks();}).toThrow();
|
||||
|
||||
browser.defer.flush();
|
||||
expect(log).toEqual('A;B;C;');
|
||||
browser.defer.cancel(taskId1);
|
||||
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).not.toThrow();
|
||||
expect(function() {browser.defer.verifyNoPendingTasks();}).toThrow();
|
||||
|
||||
browser.defer.cancel(taskId2);
|
||||
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).not.toThrow();
|
||||
expect(function() {browser.defer.verifyNoPendingTasks();}).not.toThrow();
|
||||
|
||||
browser.defer.flush(1000);
|
||||
expect(log).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should defer and flush over time', function() {
|
||||
browser.defer(logFn('A'), 1);
|
||||
browser.defer(logFn('B'), 2);
|
||||
browser.defer(logFn('C'), 3);
|
||||
describe('defer.verifyNoPendingTasks', function() {
|
||||
it('should throw if there are pending tasks', function() {
|
||||
expect(browser.defer.verifyNoPendingTasks).not.toThrow();
|
||||
|
||||
browser.defer.flush(0);
|
||||
expect(browser.defer.now).toEqual(0);
|
||||
expect(log).toEqual('');
|
||||
browser.defer(noop);
|
||||
expect(browser.defer.verifyNoPendingTasks).toThrow();
|
||||
});
|
||||
|
||||
browser.defer.flush(1);
|
||||
expect(browser.defer.now).toEqual(1);
|
||||
expect(log).toEqual('A;');
|
||||
it('should list the pending tasks (in order) in the error message', function() {
|
||||
browser.defer(noop, 100);
|
||||
browser.defer(noop, 300, 'fooType');
|
||||
browser.defer(noop, 200, 'barType');
|
||||
|
||||
browser.defer.flush(2);
|
||||
expect(browser.defer.now).toEqual(3);
|
||||
expect(log).toEqual('A;B;C;');
|
||||
var expectedError =
|
||||
'Deferred tasks to flush (3):\n' +
|
||||
' {id: 0, type: $$default$$, time: 100}\n' +
|
||||
' {id: 2, type: barType, time: 200}\n' +
|
||||
' {id: 1, type: fooType, time: 300}';
|
||||
expect(browser.defer.verifyNoPendingTasks).toThrowError(expectedError);
|
||||
});
|
||||
|
||||
describe('with specific task type', function() {
|
||||
it('should throw if there are pending tasks', function() {
|
||||
browser.defer(noop, 0, 'fooType');
|
||||
|
||||
expect(function() {browser.defer.verifyNoPendingTasks('barType');}).not.toThrow();
|
||||
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).toThrow();
|
||||
expect(function() {browser.defer.verifyNoPendingTasks();}).toThrow();
|
||||
});
|
||||
|
||||
it('should list the pending tasks (in order) in the error message', function() {
|
||||
browser.defer(noop, 100);
|
||||
browser.defer(noop, 300, 'fooType');
|
||||
browser.defer(noop, 200, 'barType');
|
||||
browser.defer(noop, 400, 'fooType');
|
||||
|
||||
var expectedError =
|
||||
'Deferred tasks to flush (2):\n' +
|
||||
' {id: 1, type: fooType, time: 300}\n' +
|
||||
' {id: 3, type: fooType, time: 400}';
|
||||
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).
|
||||
toThrowError(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if there is nothing to be flushed', function() {
|
||||
expect(function() {browser.defer.flush();}).toThrowError('No deferred tasks to be flushed');
|
||||
describe('notifyWhenNoOutstandingRequests', function() {
|
||||
var callback;
|
||||
beforeEach(function() {
|
||||
callback = jasmine.createSpy('callback');
|
||||
});
|
||||
|
||||
it('should immediately run the callback if no pending tasks', function() {
|
||||
browser.notifyWhenNoOutstandingRequests(callback);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should run the callback as soon as there are no pending tasks', function() {
|
||||
browser.defer(noop, 100);
|
||||
browser.defer(noop, 200);
|
||||
|
||||
browser.notifyWhenNoOutstandingRequests(callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.flush(100);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.flush(100);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not run the callback more than once', function() {
|
||||
browser.defer(noop, 100);
|
||||
browser.notifyWhenNoOutstandingRequests(callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.flush(100);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
browser.defer(noop, 200);
|
||||
browser.defer.flush(100);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
describe('with specific task type', function() {
|
||||
it('should immediately run the callback if no pending tasks', function() {
|
||||
browser.notifyWhenNoOutstandingRequests(callback, 'fooType');
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should run the callback as soon as there are no pending tasks', function() {
|
||||
browser.defer(noop, 100, 'fooType');
|
||||
browser.defer(noop, 200, 'barType');
|
||||
|
||||
browser.notifyWhenNoOutstandingRequests(callback, 'fooType');
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.flush(100);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not run the callback more than once', function() {
|
||||
browser.defer(noop, 100, 'fooType');
|
||||
browser.defer(noop, 200);
|
||||
|
||||
browser.notifyWhenNoOutstandingRequests(callback, 'fooType');
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
browser.defer.flush(100);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
browser.defer.flush(100);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
browser.defer(noop, 100, 'fooType');
|
||||
browser.defer(noop, 200);
|
||||
browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('$flushPendingTasks', function() {
|
||||
var $flushPendingTasks;
|
||||
var browserDeferFlushSpy;
|
||||
|
||||
beforeEach(inject(function($browser, _$flushPendingTasks_) {
|
||||
$flushPendingTasks = _$flushPendingTasks_;
|
||||
browserDeferFlushSpy = spyOn($browser.defer, 'flush').and.returnValue('flushed');
|
||||
}));
|
||||
|
||||
it('should delegate to `$browser.defer.flush()`', function() {
|
||||
var result = $flushPendingTasks(42);
|
||||
|
||||
expect(browserDeferFlushSpy).toHaveBeenCalledOnceWith(42);
|
||||
expect(result).toBe('flushed');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('$verifyNoPendingTasks', function() {
|
||||
var $verifyNoPendingTasks;
|
||||
var browserDeferVerifySpy;
|
||||
|
||||
beforeEach(inject(function($browser, _$verifyNoPendingTasks_) {
|
||||
$verifyNoPendingTasks = _$verifyNoPendingTasks_;
|
||||
browserDeferVerifySpy = spyOn($browser.defer, 'verifyNoPendingTasks').and.returnValue('verified');
|
||||
}));
|
||||
|
||||
it('should delegate to `$browser.defer.verifyNoPendingTasks()`', function() {
|
||||
var result = $verifyNoPendingTasks('fortyTwo');
|
||||
|
||||
expect(browserDeferVerifySpy).toHaveBeenCalledOnceWith('fortyTwo');
|
||||
expect(result).toBe('verified');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -705,47 +950,74 @@ describe('ngMock', function() {
|
||||
|
||||
describe('$timeout', function() {
|
||||
it('should expose flush method that will flush the pending queue of tasks', inject(
|
||||
function($timeout) {
|
||||
function($rootScope, $timeout) {
|
||||
var logger = [],
|
||||
logFn = function(msg) { return function() { logger.push(msg); }; };
|
||||
|
||||
$timeout(logFn('t1'));
|
||||
$timeout(logFn('t2'), 200);
|
||||
$rootScope.$evalAsync(logFn('rs')); // Non-timeout tasks are flushed as well.
|
||||
$timeout(logFn('t3'));
|
||||
expect(logger).toEqual([]);
|
||||
|
||||
$timeout.flush();
|
||||
expect(logger).toEqual(['t1', 't3', 't2']);
|
||||
expect(logger).toEqual(['t1', 'rs', 't3', 't2']);
|
||||
}));
|
||||
|
||||
|
||||
it('should throw an exception when not flushed', inject(function($timeout) {
|
||||
$timeout(noop);
|
||||
it('should throw an exception when not flushed', inject(function($rootScope, $timeout) {
|
||||
$timeout(noop, 100);
|
||||
$rootScope.$evalAsync(noop);
|
||||
|
||||
var expectedError = 'Deferred tasks to flush (1): {id: 0, time: 0}';
|
||||
expect(function() {$timeout.verifyNoPendingTasks();}).toThrowError(expectedError);
|
||||
var expectedError =
|
||||
'Deferred tasks to flush (2):\n' +
|
||||
' {id: 1, type: $evalAsync, time: 0}\n' +
|
||||
' {id: 0, type: $timeout, time: 100}';
|
||||
expect($timeout.verifyNoPendingTasks).toThrowError(expectedError);
|
||||
}));
|
||||
|
||||
|
||||
it('should do nothing when all tasks have been flushed', inject(function($timeout) {
|
||||
$timeout(noop);
|
||||
it('should recommend `$verifyNoPendingTasks()` when all pending tasks are not timeouts',
|
||||
inject(function($rootScope, $timeout) {
|
||||
var extraMessage = 'None of the pending tasks are timeouts. If you only want to verify ' +
|
||||
'pending timeouts, use `$verifyNoPendingTasks(\'$timeout\')` instead.';
|
||||
var errorMessage;
|
||||
|
||||
$timeout(noop, 100);
|
||||
$rootScope.$evalAsync(noop);
|
||||
try { $timeout.verifyNoPendingTasks(); } catch (err) { errorMessage = err.message; }
|
||||
|
||||
expect(errorMessage).not.toContain(extraMessage);
|
||||
|
||||
$timeout.flush(100);
|
||||
$rootScope.$evalAsync(noop);
|
||||
try { $timeout.verifyNoPendingTasks(); } catch (err) { errorMessage = err.message; }
|
||||
|
||||
expect(errorMessage).toContain(extraMessage);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
it('should do nothing when all tasks have been flushed', inject(function($rootScope, $timeout) {
|
||||
$timeout(noop, 100);
|
||||
$rootScope.$evalAsync(noop);
|
||||
|
||||
$timeout.flush();
|
||||
expect(function() {$timeout.verifyNoPendingTasks();}).not.toThrow();
|
||||
expect($timeout.verifyNoPendingTasks).not.toThrow();
|
||||
}));
|
||||
|
||||
|
||||
it('should check against the delay if provided within timeout', inject(function($timeout) {
|
||||
$timeout(noop, 100);
|
||||
$timeout.flush(100);
|
||||
expect(function() {$timeout.verifyNoPendingTasks();}).not.toThrow();
|
||||
expect($timeout.verifyNoPendingTasks).not.toThrow();
|
||||
|
||||
$timeout(noop, 1000);
|
||||
$timeout.flush(100);
|
||||
expect(function() {$timeout.verifyNoPendingTasks();}).toThrow();
|
||||
expect($timeout.verifyNoPendingTasks).toThrow();
|
||||
|
||||
$timeout.flush(900);
|
||||
expect(function() {$timeout.verifyNoPendingTasks();}).not.toThrow();
|
||||
expect($timeout.verifyNoPendingTasks).not.toThrow();
|
||||
}));
|
||||
|
||||
|
||||
@@ -763,6 +1035,7 @@ describe('ngMock', function() {
|
||||
expect(count).toBe(2);
|
||||
}));
|
||||
|
||||
|
||||
it('should resolve timeout functions following the timeline', inject(function($timeout) {
|
||||
var count1 = 0, count2 = 0;
|
||||
var iterate1 = function() {
|
||||
@@ -1056,7 +1329,7 @@ describe('ngMock', function() {
|
||||
|
||||
|
||||
describe('$httpBackend', function() {
|
||||
var hb, callback, realBackendSpy;
|
||||
var hb, callback;
|
||||
|
||||
beforeEach(inject(function($httpBackend) {
|
||||
callback = jasmine.createSpy('callback');
|
||||
@@ -1233,6 +1506,42 @@ describe('ngMock', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should throw error when expectation fails', function() {
|
||||
expect(function() {
|
||||
hb.expectPOST('/some', {foo: 1}).respond({});
|
||||
hb('POST', '/some', {foo: 2}, callback);
|
||||
hb.flush();
|
||||
}).toThrowError(/^Expected POST \/some with different data/);
|
||||
});
|
||||
|
||||
|
||||
it('should throw error when expectation about headers fails', function() {
|
||||
expect(function() {
|
||||
hb.expectPOST('/some', {foo: 1}, {X: 'val1'}).respond({});
|
||||
hb('POST', '/some', {foo: 1}, callback, {X: 'val2'});
|
||||
hb.flush();
|
||||
}).toThrowError(/^Expected POST \/some with different headers/);
|
||||
});
|
||||
|
||||
|
||||
it('should throw error about data when expectations about both data and headers fail', function() {
|
||||
expect(function() {
|
||||
hb.expectPOST('/some', {foo: 1}, {X: 'val1'}).respond({});
|
||||
hb('POST', '/some', {foo: 2}, callback, {X: 'val2'});
|
||||
hb.flush();
|
||||
}).toThrowError(/^Expected POST \/some with different data/);
|
||||
});
|
||||
|
||||
|
||||
it('should throw error when response is not defined for a backend definition', function() {
|
||||
expect(function() {
|
||||
hb.whenGET('/some'); // no .respond(...) !
|
||||
hb('GET', '/some', null, callback);
|
||||
hb.flush();
|
||||
}).toThrowError('No response defined !');
|
||||
});
|
||||
|
||||
|
||||
it('should match headers if specified', function() {
|
||||
hb.when('GET', '/url', null, {'X': 'val1'}).respond(201, 'content1');
|
||||
hb.when('GET', '/url', null, {'X': 'val2'}).respond(202, 'content2');
|
||||
@@ -1941,12 +2250,36 @@ describe('ngMock', function() {
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
|
||||
}
|
||||
);
|
||||
they('should ignore query param when matching in ' + routeShortcut + ' $prop method', methods,
|
||||
they('should ignore query params when matching in ' + routeShortcut + ' $prop method', methods,
|
||||
function() {
|
||||
hb[routeShortcut](this, '/route/:id').respond('path');
|
||||
hb(this, '/route/123?q=str&foo=bar', undefined, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
|
||||
angular.forEach([
|
||||
{route: '/route1/:id', url: '/route1/Alpha', expectedParams: {id: 'Alpha'}},
|
||||
{route: '/route2/:id', url: '/route2/Bravo/?', expectedParams: {id: 'Bravo'}},
|
||||
{route: '/route3/:id', url: '/route3/Charlie?q=str&foo=bar', expectedParams: {id: 'Charlie', q: 'str', foo: 'bar'}},
|
||||
{route: '/:x/route4', url: '/Delta/route4?q=str&foo=bar', expectedParams: {x: 'Delta', q: 'str', foo: 'bar'}},
|
||||
{route: '/route5/:id*', url: '/route5/Echo/456?q=str&foo=bar', expectedParams: {id: 'Echo/456', q: 'str', foo: 'bar'}},
|
||||
{route: '/route6/:id*', url: '/route6/Foxtrot/456/?q=str&foo=bar', expectedParams: {id: 'Foxtrot/456', q: 'str', foo: 'bar'}},
|
||||
{route: '/route7/:id*', url: '/route7/Golf/456//?q=str&foo=bar', expectedParams: {id: 'Golf/456', q: 'str', foo: 'bar'}},
|
||||
{route: '/:x*/route8', url: '/Hotel/123/456/route8/?q=str&foo=bar', expectedParams: {x: 'Hotel/123/456', q: 'str', foo: 'bar'}},
|
||||
{route: '/:x*/route9/:id', url: '/India/456/route9/0?q=str&foo=bar', expectedParams: {x: 'India/456', id: '0', q: 'str', foo: 'bar'}},
|
||||
{route: '/route10', url: '/route10?q=Juliet&foo=bar', expectedParams: {q: 'Juliet', foo: 'bar'}},
|
||||
{route: '/route11', url: '/route11///?q=Kilo', expectedParams: {q: 'Kilo'}},
|
||||
{route: '/route12', url: '/route12///', expectedParams: {}}
|
||||
], function(testDataEntry) {
|
||||
callback.calls.reset();
|
||||
var paramsSpy = jasmine.createSpy('params');
|
||||
hb[routeShortcut](this, testDataEntry.route).respond(
|
||||
function(method, url, data, headers, params) {
|
||||
paramsSpy(params);
|
||||
// status, response, headers, statusText, xhrStatus
|
||||
return [200, 'path', { 'x-header': 'foo' }, 'OK', 'complete'];
|
||||
}
|
||||
);
|
||||
hb(this, testDataEntry.url, undefined, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', 'x-header: foo', 'OK', 'complete');
|
||||
expect(paramsSpy).toHaveBeenCalledOnceWith(testDataEntry.expectedParams);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -2536,6 +2869,24 @@ describe('ngMockE2E', function() {
|
||||
}).toThrowError('Unexpected request: GET /some\nNo more request expected');
|
||||
});
|
||||
|
||||
it('should throw error when expectation fails - without error callback', function() {
|
||||
expect(function() {
|
||||
hb.expectPOST('/some', { foo: 1 }).respond({});
|
||||
$http.post('/some', { foo: 2 }).then(noop);
|
||||
|
||||
hb.flush();
|
||||
}).toThrowError(/^Expected POST \/some with different data/);
|
||||
});
|
||||
|
||||
it('should throw error when unexpected request - with error callback', function() {
|
||||
expect(function() {
|
||||
hb.expectPOST('/some', { foo: 1 }).respond({});
|
||||
$http.post('/some', { foo: 2 }).then(noop, noop);
|
||||
|
||||
hb.flush();
|
||||
}).toThrowError(/^Expected POST \/some with different data/);
|
||||
});
|
||||
|
||||
|
||||
describe('passThrough()', function() {
|
||||
it('should delegate requests to the real backend when passThrough is invoked', function() {
|
||||
|
||||
+403
-92
@@ -65,8 +65,8 @@ describe('$route', function() {
|
||||
$httpBackend.when('GET', 'Chapter.html').respond('chapter');
|
||||
$httpBackend.when('GET', 'test.html').respond('test');
|
||||
$httpBackend.when('GET', 'foo.html').respond('foo');
|
||||
$httpBackend.when('GET', 'baz.html').respond('baz');
|
||||
$httpBackend.when('GET', 'bar.html').respond('bar');
|
||||
$httpBackend.when('GET', 'baz.html').respond('baz');
|
||||
$httpBackend.when('GET', 'http://example.com/trusted-template.html').respond('cross domain trusted template');
|
||||
$httpBackend.when('GET', '404.html').respond('not found');
|
||||
};
|
||||
@@ -76,6 +76,7 @@ describe('$route', function() {
|
||||
dealoc(element);
|
||||
});
|
||||
|
||||
|
||||
it('should allow cancellation via $locationChangeStart via $routeChangeStart', function() {
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/Edit', {
|
||||
@@ -1677,95 +1678,413 @@ describe('$route', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('reloadOnUrl', function() {
|
||||
it('should reload when `reloadOnUrl` is true and `.url()` changes', function() {
|
||||
var routeChange = jasmine.createSpy('routeChange');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/path/:param', {});
|
||||
});
|
||||
|
||||
inject(function($location, $rootScope, $routeParams) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
|
||||
// Initial load
|
||||
$location.path('/path/foo');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({param: 'foo'});
|
||||
|
||||
routeChange.calls.reset();
|
||||
|
||||
// Reload on `path` change
|
||||
$location.path('/path/bar');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({param: 'bar'});
|
||||
|
||||
routeChange.calls.reset();
|
||||
|
||||
// Reload on `search` change
|
||||
$location.search('foo', 'bar');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
|
||||
|
||||
routeChange.calls.reset();
|
||||
|
||||
// Reload on `hash` change
|
||||
$location.hash('baz');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reload when `reloadOnUrl` is false and URL maps to different route',
|
||||
function() {
|
||||
var routeChange = jasmine.createSpy('routeChange');
|
||||
var routeUpdate = jasmine.createSpy('routeUpdate');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.
|
||||
when('/path/:param', {reloadOnUrl: false}).
|
||||
otherwise({});
|
||||
});
|
||||
|
||||
inject(function($location, $rootScope, $routeParams) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChange);
|
||||
$rootScope.$on('$routeUpdate', routeUpdate);
|
||||
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
|
||||
// Initial load
|
||||
$location.path('/path/foo');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
expect(routeUpdate).not.toHaveBeenCalled();
|
||||
expect($routeParams).toEqual({param: 'foo'});
|
||||
|
||||
routeChange.calls.reset();
|
||||
|
||||
// Route change
|
||||
$location.path('/other/path/bar');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
expect(routeUpdate).not.toHaveBeenCalled();
|
||||
expect($routeParams).toEqual({});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should not reload when `reloadOnUrl` is false and URL maps to the same route',
|
||||
function() {
|
||||
var routeChange = jasmine.createSpy('routeChange');
|
||||
var routeUpdate = jasmine.createSpy('routeUpdate');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/path/:param', {reloadOnUrl: false});
|
||||
});
|
||||
|
||||
inject(function($location, $rootScope, $routeParams) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChange);
|
||||
$rootScope.$on('$routeUpdate', routeUpdate);
|
||||
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
|
||||
// Initial load
|
||||
$location.path('/path/foo');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
expect(routeUpdate).not.toHaveBeenCalled();
|
||||
expect($routeParams).toEqual({param: 'foo'});
|
||||
|
||||
routeChange.calls.reset();
|
||||
|
||||
// Route update (no reload)
|
||||
$location.path('/path/bar').search('foo', 'bar').hash('baz');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
expect(routeUpdate).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should update `$routeParams` even when not reloading a route', function() {
|
||||
var routeChange = jasmine.createSpy('routeChange');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/path/:param', {reloadOnUrl: false});
|
||||
});
|
||||
|
||||
inject(function($location, $rootScope, $routeParams) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChange);
|
||||
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
|
||||
// Initial load
|
||||
$location.path('/path/foo');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
expect($routeParams).toEqual({param: 'foo'});
|
||||
|
||||
routeChange.calls.reset();
|
||||
|
||||
// Route update (no reload)
|
||||
$location.path('/path/bar');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
expect($routeParams).toEqual({param: 'bar'});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('with `$route.reload()`', function() {
|
||||
var $location;
|
||||
var $log;
|
||||
var $rootScope;
|
||||
var $route;
|
||||
var routeChangeStart;
|
||||
var routeChangeSuccess;
|
||||
|
||||
beforeEach(module(function($routeProvider) {
|
||||
$routeProvider.when('/path/:param', {
|
||||
template: '',
|
||||
reloadOnUrl: false,
|
||||
controller: function Controller($log) {
|
||||
$log.debug('initialized');
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($compile, _$location_, _$log_, _$rootScope_, _$route_) {
|
||||
$location = _$location_;
|
||||
$log = _$log_;
|
||||
$rootScope = _$rootScope_;
|
||||
$route = _$route_;
|
||||
|
||||
routeChangeStart = jasmine.createSpy('routeChangeStart');
|
||||
routeChangeSuccess = jasmine.createSpy('routeChangeSuccess');
|
||||
|
||||
$rootScope.$on('$routeChangeStart', routeChangeStart);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChangeSuccess);
|
||||
|
||||
element = $compile('<div><ng-view></ng-view></div>')($rootScope);
|
||||
}));
|
||||
|
||||
|
||||
it('should reload the current route', function() {
|
||||
$location.path('/path/foo');
|
||||
$rootScope.$digest();
|
||||
expect($location.path()).toBe('/path/foo');
|
||||
expect(routeChangeStart).toHaveBeenCalledOnce();
|
||||
expect(routeChangeSuccess).toHaveBeenCalledOnce();
|
||||
expect($log.debug.logs).toEqual([['initialized']]);
|
||||
|
||||
routeChangeStart.calls.reset();
|
||||
routeChangeSuccess.calls.reset();
|
||||
$log.reset();
|
||||
|
||||
$route.reload();
|
||||
$rootScope.$digest();
|
||||
expect($location.path()).toBe('/path/foo');
|
||||
expect(routeChangeStart).toHaveBeenCalledOnce();
|
||||
expect(routeChangeSuccess).toHaveBeenCalledOnce();
|
||||
expect($log.debug.logs).toEqual([['initialized']]);
|
||||
|
||||
$log.reset();
|
||||
});
|
||||
|
||||
|
||||
it('should support preventing a route reload', function() {
|
||||
$location.path('/path/foo');
|
||||
$rootScope.$digest();
|
||||
expect($location.path()).toBe('/path/foo');
|
||||
expect(routeChangeStart).toHaveBeenCalledOnce();
|
||||
expect(routeChangeSuccess).toHaveBeenCalledOnce();
|
||||
expect($log.debug.logs).toEqual([['initialized']]);
|
||||
|
||||
routeChangeStart.calls.reset();
|
||||
routeChangeSuccess.calls.reset();
|
||||
$log.reset();
|
||||
|
||||
routeChangeStart.and.callFake(function(evt) { evt.preventDefault(); });
|
||||
|
||||
$route.reload();
|
||||
$rootScope.$digest();
|
||||
expect($location.path()).toBe('/path/foo');
|
||||
expect(routeChangeStart).toHaveBeenCalledOnce();
|
||||
expect(routeChangeSuccess).not.toHaveBeenCalled();
|
||||
expect($log.debug.logs).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
it('should reload the current route even if `reloadOnUrl` is disabled',
|
||||
inject(function($routeParams) {
|
||||
$location.path('/path/foo');
|
||||
$rootScope.$digest();
|
||||
expect(routeChangeStart).toHaveBeenCalledOnce();
|
||||
expect(routeChangeSuccess).toHaveBeenCalledOnce();
|
||||
expect($log.debug.logs).toEqual([['initialized']]);
|
||||
expect($routeParams).toEqual({param: 'foo'});
|
||||
|
||||
routeChangeStart.calls.reset();
|
||||
routeChangeSuccess.calls.reset();
|
||||
$log.reset();
|
||||
|
||||
$location.path('/path/bar');
|
||||
$rootScope.$digest();
|
||||
expect(routeChangeStart).not.toHaveBeenCalled();
|
||||
expect(routeChangeSuccess).not.toHaveBeenCalled();
|
||||
expect($log.debug.logs).toEqual([]);
|
||||
expect($routeParams).toEqual({param: 'bar'});
|
||||
|
||||
$route.reload();
|
||||
$rootScope.$digest();
|
||||
expect(routeChangeStart).toHaveBeenCalledOnce();
|
||||
expect(routeChangeSuccess).toHaveBeenCalledOnce();
|
||||
expect($log.debug.logs).toEqual([['initialized']]);
|
||||
expect($routeParams).toEqual({param: 'bar'});
|
||||
|
||||
$log.reset();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloadOnSearch', function() {
|
||||
it('should reload a route when reloadOnSearch is enabled and .search() changes', function() {
|
||||
it('should not have any effect if `reloadOnUrl` is false', function() {
|
||||
var reloaded = jasmine.createSpy('route reload');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/foo', {controller: angular.noop});
|
||||
$routeProvider.when('/foo', {
|
||||
reloadOnUrl: false,
|
||||
reloadOnSearch: true
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($route, $location, $rootScope, $routeParams) {
|
||||
$rootScope.$on('$routeChangeStart', reloaded);
|
||||
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
expect(reloaded).toHaveBeenCalled();
|
||||
expect(reloaded).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({});
|
||||
|
||||
reloaded.calls.reset();
|
||||
|
||||
// trigger reload
|
||||
// trigger reload (via .search())
|
||||
$location.search({foo: 'bar'});
|
||||
$rootScope.$digest();
|
||||
expect(reloaded).toHaveBeenCalled();
|
||||
expect($routeParams).toEqual({foo:'bar'});
|
||||
expect(reloaded).not.toHaveBeenCalled();
|
||||
expect($routeParams).toEqual({foo: 'bar'});
|
||||
|
||||
// trigger reload (via .hash())
|
||||
$location.hash('baz');
|
||||
$rootScope.$digest();
|
||||
expect(reloaded).not.toHaveBeenCalled();
|
||||
expect($routeParams).toEqual({foo: 'bar'});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not reload a route when reloadOnSearch is disabled and only .search() changes', function() {
|
||||
var routeChange = jasmine.createSpy('route change'),
|
||||
routeUpdate = jasmine.createSpy('route update');
|
||||
it('should reload when `reloadOnSearch` is true and `.search()`/`.hash()` changes',
|
||||
function() {
|
||||
var reloaded = jasmine.createSpy('route reload');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
|
||||
});
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/foo', {controller: angular.noop});
|
||||
});
|
||||
|
||||
inject(function($route, $location, $rootScope) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChange);
|
||||
$rootScope.$on('$routeUpdate', routeUpdate);
|
||||
inject(function($route, $location, $rootScope, $routeParams) {
|
||||
$rootScope.$on('$routeChangeStart', reloaded);
|
||||
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
expect(reloaded).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({});
|
||||
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalled();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
expect(routeUpdate).not.toHaveBeenCalled();
|
||||
routeChange.calls.reset();
|
||||
reloaded.calls.reset();
|
||||
|
||||
// don't trigger reload
|
||||
$location.search({foo: 'bar'});
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
expect(routeUpdate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
// trigger reload (via .search())
|
||||
$location.search({foo: 'bar'});
|
||||
$rootScope.$digest();
|
||||
expect(reloaded).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({foo: 'bar'});
|
||||
|
||||
reloaded.calls.reset();
|
||||
|
||||
// trigger reload (via .hash())
|
||||
$location.hash('baz');
|
||||
$rootScope.$digest();
|
||||
expect(reloaded).toHaveBeenCalledOnce();
|
||||
expect($routeParams).toEqual({foo: 'bar'});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should reload reloadOnSearch route when url differs only in route path param', function() {
|
||||
var routeChange = jasmine.createSpy('route change');
|
||||
it('should not reload when `reloadOnSearch` is false and `.search()`/`.hash()` changes',
|
||||
function() {
|
||||
var routeChange = jasmine.createSpy('route change'),
|
||||
routeUpdate = jasmine.createSpy('route update');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
|
||||
});
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
|
||||
});
|
||||
|
||||
inject(function($route, $location, $rootScope) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChange);
|
||||
inject(function($route, $location, $rootScope) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChange);
|
||||
$rootScope.$on('$routeUpdate', routeUpdate);
|
||||
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
|
||||
$location.path('/foo/aaa');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalled();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
routeChange.calls.reset();
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
expect(routeUpdate).not.toHaveBeenCalled();
|
||||
|
||||
$location.path('/foo/bbb');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalled();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
routeChange.calls.reset();
|
||||
routeChange.calls.reset();
|
||||
|
||||
$location.search({foo: 'bar'});
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
// don't trigger reload (via .search())
|
||||
$location.search({foo: 'bar'});
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
expect(routeUpdate).toHaveBeenCalledOnce();
|
||||
|
||||
routeUpdate.calls.reset();
|
||||
|
||||
// don't trigger reload (via .hash())
|
||||
$location.hash('baz');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
expect(routeUpdate).toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should update params when reloadOnSearch is disabled and .search() changes', function() {
|
||||
it('should reload when `reloadOnSearch` is false and url differs only in route path param',
|
||||
function() {
|
||||
var routeChange = jasmine.createSpy('route change');
|
||||
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
|
||||
});
|
||||
|
||||
inject(function($route, $location, $rootScope) {
|
||||
$rootScope.$on('$routeChangeStart', routeChange);
|
||||
$rootScope.$on('$routeChangeSuccess', routeChange);
|
||||
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
|
||||
$location.path('/foo/aaa');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
routeChange.calls.reset();
|
||||
|
||||
$location.path('/foo/bbb');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).toHaveBeenCalledTimes(2);
|
||||
routeChange.calls.reset();
|
||||
|
||||
$location.search({foo: 'bar'}).hash('baz');
|
||||
$rootScope.$digest();
|
||||
expect(routeChange).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should update params when `reloadOnSearch` is false and `.search()` changes', function() {
|
||||
var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher');
|
||||
|
||||
module(function($routeProvider) {
|
||||
@@ -1852,7 +2171,8 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('reload', function() {
|
||||
|
||||
describe('with `$route.reload()`', function() {
|
||||
var $location;
|
||||
var $log;
|
||||
var $rootScope;
|
||||
@@ -1886,6 +2206,7 @@ describe('$route', function() {
|
||||
element = $compile('<div><div ng-view></div></div>')($rootScope);
|
||||
}));
|
||||
|
||||
|
||||
it('should reload the current route', function() {
|
||||
$location.path('/bar/123');
|
||||
$rootScope.$digest();
|
||||
@@ -1908,6 +2229,7 @@ describe('$route', function() {
|
||||
$log.reset();
|
||||
});
|
||||
|
||||
|
||||
it('should support preventing a route reload', function() {
|
||||
$location.path('/bar/123');
|
||||
$rootScope.$digest();
|
||||
@@ -1930,6 +2252,7 @@ describe('$route', function() {
|
||||
expect($log.debug.logs).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
it('should reload even if reloadOnSearch is false', inject(function($routeParams) {
|
||||
$location.path('/bar/123');
|
||||
$rootScope.$digest();
|
||||
@@ -1946,6 +2269,15 @@ describe('$route', function() {
|
||||
expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
|
||||
expect($log.debug.logs).toEqual([]);
|
||||
|
||||
routeChangeSuccessSpy.calls.reset();
|
||||
$log.reset();
|
||||
|
||||
$location.hash('c');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({barId: '123', a: 'b'});
|
||||
expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
|
||||
expect($log.debug.logs).toEqual([]);
|
||||
|
||||
$route.reload();
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({barId: '123', a: 'b'});
|
||||
@@ -2087,9 +2419,8 @@ describe('$route', function() {
|
||||
it('should wait for $resolve promises before calling callbacks', function() {
|
||||
var deferred;
|
||||
|
||||
module(function($provide, $routeProvider) {
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/path', {
|
||||
template: '',
|
||||
resolve: {
|
||||
a: function($q) {
|
||||
deferred = $q.defer();
|
||||
@@ -2099,7 +2430,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2108,7 +2439,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2116,9 +2447,8 @@ describe('$route', function() {
|
||||
it('should call callback after $resolve promises are rejected', function() {
|
||||
var deferred;
|
||||
|
||||
module(function($provide, $routeProvider) {
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/path', {
|
||||
template: '',
|
||||
resolve: {
|
||||
a: function($q) {
|
||||
deferred = $q.defer();
|
||||
@@ -2128,7 +2458,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2137,7 +2467,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.reject();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2145,7 +2475,7 @@ describe('$route', function() {
|
||||
it('should wait for resolveRedirectTo promises before calling callbacks', function() {
|
||||
var deferred;
|
||||
|
||||
module(function($provide, $routeProvider) {
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/path', {
|
||||
resolveRedirectTo: function($q) {
|
||||
deferred = $q.defer();
|
||||
@@ -2154,7 +2484,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2163,7 +2493,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2171,7 +2501,7 @@ describe('$route', function() {
|
||||
it('should call callback after resolveRedirectTo promises are rejected', function() {
|
||||
var deferred;
|
||||
|
||||
module(function($provide, $routeProvider) {
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/path', {
|
||||
resolveRedirectTo: function($q) {
|
||||
deferred = $q.defer();
|
||||
@@ -2180,7 +2510,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2189,7 +2519,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.reject();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2197,30 +2527,11 @@ describe('$route', function() {
|
||||
it('should wait for all route promises before calling callbacks', function() {
|
||||
var deferreds = {};
|
||||
|
||||
module(function($provide, $routeProvider) {
|
||||
// While normally `$browser.defer()` modifies the `outstandingRequestCount`, the mocked
|
||||
// version (provided by `ngMock`) does not. This doesn't matter in most tests, but in this
|
||||
// case we need the `outstandingRequestCount` logic to ensure that we don't call the
|
||||
// `$$testability.whenStable()` callbacks part way through a `$rootScope.$evalAsync` block.
|
||||
// See ngRoute's commitRoute()'s finally() block for details.
|
||||
$provide.decorator('$browser', function($delegate) {
|
||||
var oldDefer = $delegate.defer;
|
||||
var newDefer = function(fn, delay) {
|
||||
var requestCountAwareFn = function() { $delegate.$$completeOutstandingRequest(fn); };
|
||||
$delegate.$$incOutstandingRequestCount();
|
||||
return oldDefer.call($delegate, requestCountAwareFn, delay);
|
||||
};
|
||||
|
||||
$delegate.defer = angular.extend(newDefer, oldDefer);
|
||||
|
||||
return $delegate;
|
||||
});
|
||||
|
||||
module(function($routeProvider) {
|
||||
addRouteWithAsyncRedirect('/foo', '/bar');
|
||||
addRouteWithAsyncRedirect('/bar', '/baz');
|
||||
addRouteWithAsyncRedirect('/baz', '/qux');
|
||||
$routeProvider.when('/qux', {
|
||||
template: '',
|
||||
resolve: {
|
||||
a: function($q) {
|
||||
var deferred = deferreds['/qux'] = $q.defer();
|
||||
@@ -2240,7 +2551,7 @@ describe('$route', function() {
|
||||
}
|
||||
});
|
||||
|
||||
inject(function($browser, $location, $rootScope, $route, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user