Compare commits
48 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 |
+120
-2
@@ -1,3 +1,92 @@
|
||||
<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)
|
||||
|
||||
@@ -448,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
|
||||
@@ -463,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
+5
@@ -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',
|
||||
@@ -104,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'
|
||||
@@ -131,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'
|
||||
@@ -140,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 {
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
+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"
|
||||
|
||||
+6
-6
@@ -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');
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
$FilterProvider,
|
||||
$$ForceReflowProvider,
|
||||
$InterpolateProvider,
|
||||
$$IntervalFactoryProvider,
|
||||
$IntervalProvider,
|
||||
$HttpProvider,
|
||||
$HttpParamSerializerProvider,
|
||||
@@ -88,6 +89,7 @@
|
||||
$SceProvider,
|
||||
$SceDelegateProvider,
|
||||
$SnifferProvider,
|
||||
$$TaskTrackerFactoryProvider,
|
||||
$TemplateCacheProvider,
|
||||
$TemplateRequestProvider,
|
||||
$$TestabilityProvider,
|
||||
@@ -241,6 +243,7 @@ function publishExternalAPI(angular) {
|
||||
$$forceReflow: $$ForceReflowProvider,
|
||||
$interpolate: $InterpolateProvider,
|
||||
$interval: $IntervalProvider,
|
||||
$$intervalFactory: $$IntervalFactoryProvider,
|
||||
$http: $HttpProvider,
|
||||
$httpParamSerializer: $HttpParamSerializerProvider,
|
||||
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
|
||||
@@ -256,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);
|
||||
|
||||
+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);
|
||||
}];
|
||||
}
|
||||
|
||||
+575
-82
@@ -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') {
|
||||
@@ -2620,11 +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].childNodes, transcludeFn);
|
||||
var slotCompileNodes = jqLite(slots[slotName].childNodes);
|
||||
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slotCompileNodes, transcludeFn);
|
||||
}
|
||||
}
|
||||
|
||||
$template = $template.childNodes;
|
||||
$template = jqLite($template.childNodes);
|
||||
}
|
||||
|
||||
$compileNode.empty(); // clear contents
|
||||
@@ -2960,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) {
|
||||
@@ -3317,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;
|
||||
|
||||
@@ -3361,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
|
||||
|
||||
@@ -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'];
|
||||
|
||||
+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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -786,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);
|
||||
|
||||
+4
-1
@@ -387,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);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+304
-176
@@ -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) {
|
||||
|
||||
@@ -1663,40 +1771,10 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* 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
|
||||
@@ -1717,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) {
|
||||
|
||||
@@ -1876,7 +1955,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* 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);
|
||||
};
|
||||
|
||||
@@ -2206,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;
|
||||
}];
|
||||
|
||||
@@ -2460,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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
+6
-46
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
/* global routeToRegExp: false */
|
||||
/* global shallowCopy: false */
|
||||
|
||||
// `isArray` and `isObject` are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
|
||||
@@ -184,7 +185,7 @@ function $RouteProvider() {
|
||||
* route definition, will cause the latter to be ignored.
|
||||
*
|
||||
* - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes
|
||||
* (inluding the path) even if the new URL maps to the same route.
|
||||
* (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
|
||||
@@ -224,7 +225,7 @@ function $RouteProvider() {
|
||||
}
|
||||
routes[path] = angular.extend(
|
||||
routeCopy,
|
||||
path && pathRegExp(path, routeCopy)
|
||||
path && routeToRegExp(path, routeCopy)
|
||||
);
|
||||
|
||||
// create redirection for trailing slashes
|
||||
@@ -235,7 +236,7 @@ function $RouteProvider() {
|
||||
|
||||
routes[redirectPath] = angular.extend(
|
||||
{redirectTo: path},
|
||||
pathRegExp(redirectPath, routeCopy)
|
||||
routeToRegExp(redirectPath, routeCopy)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,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
|
||||
@@ -693,7 +653,7 @@ function $RouteProvider() {
|
||||
|
||||
var nextRoutePromise = $q.resolve(nextRoute);
|
||||
|
||||
$browser.$$incOutstandingRequestCount();
|
||||
$browser.$$incOutstandingRequestCount('$route');
|
||||
|
||||
nextRoutePromise.
|
||||
then(getRedirectionData).
|
||||
@@ -721,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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
@@ -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>');
|
||||
|
||||
+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() {
|
||||
|
||||
+148
-8
@@ -11480,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);
|
||||
@@ -11705,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) {
|
||||
@@ -11833,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();
|
||||
@@ -12026,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);
|
||||
@@ -12112,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+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() {
|
||||
|
||||
+70
-85
@@ -922,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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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() {
|
||||
|
||||
+14
-35
@@ -2419,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();
|
||||
@@ -2431,7 +2430,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2440,7 +2439,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2448,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();
|
||||
@@ -2460,7 +2458,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2469,7 +2467,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.reject();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2477,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();
|
||||
@@ -2486,7 +2484,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2495,7 +2493,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2503,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();
|
||||
@@ -2512,7 +2510,7 @@ describe('$route', function() {
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
|
||||
inject(function($browser, $location, $rootScope, $$testability) {
|
||||
$location.path('/path');
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -2521,7 +2519,7 @@ describe('$route', function() {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
deferred.reject();
|
||||
$rootScope.$digest();
|
||||
$browser.defer.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2529,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();
|
||||
@@ -2572,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