Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be240b1176 | |||
| 61b33543ff | |||
| 1dcba9cd88 | |||
| 8cd54d7794 | |||
| 3105b2c26a | |||
| bc5a48d4a4 | |||
| 2bbc7c464f | |||
| e85f91d582 | |||
| 862a78dfd2 | |||
| bd772abf34 | |||
| b074d719ae | |||
| a43a40b778 | |||
| 2ceeb739f3 | |||
| fa715abf45 | |||
| f943e377e8 | |||
| 30084c1369 | |||
| 668a33da34 | |||
| 19e2347759 | |||
| f17292e1b5 | |||
| 77b4330011 | |||
| 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 |
+144
-2
@@ -1,3 +1,116 @@
|
||||
<a name="1.7.4"></a>
|
||||
# 1.7.4 interstellar-exploration (2018-09-07)
|
||||
|
||||
## Bug Fixes
|
||||
- **ngAria.ngClick:** prevent default event on space/enter only for non-interactive elements
|
||||
([61b335](https://github.com/angular/angular.js/commit/61b33543ff8e7f32464dec98a46bf0a35e9b03a4),
|
||||
[#16664](https://github.com/angular/angular.js/issues/16664),
|
||||
[#16680](https://github.com/angular/angular.js/issues/16680))
|
||||
- **ngAnimate:** remove the "prepare" classes with multiple structural animations
|
||||
([3105b2](https://github.com/angular/angular.js/commit/3105b2c26a71594c4e7904efc18f4b2e9da25b1b),
|
||||
[#16681](https://github.com/angular/angular.js/issues/16681),
|
||||
[#16677](https://github.com/angular/angular.js/issues/16677))
|
||||
- **$route:** correctly extract path params if the path contains a question mark or a hash
|
||||
([2ceeb7](https://github.com/angular/angular.js/commit/2ceeb739f35e01fcebcabac4beeeb7684ae9f86d))
|
||||
- **ngHref:** allow numbers and other objects in interpolation
|
||||
([30084c](https://github.com/angular/angular.js/commit/30084c13699c814ff6703d7aa2d3947a9b2f7067),
|
||||
[#16652](https://github.com/angular/angular.js/issues/16652),
|
||||
[#16626](https://github.com/angular/angular.js/issues/16626))
|
||||
- **select:** allow to select first option with value `undefined`
|
||||
([668a33](https://github.com/angular/angular.js/commit/668a33da3439f17e61dfa8f6d9b114ebde8c9d87),
|
||||
[#16653](https://github.com/angular/angular.js/issues/16653),
|
||||
[#16656](https://github.com/angular/angular.js/issues/16656))
|
||||
|
||||
|
||||
<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 +561,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 +576,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'
|
||||
],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
|
||||
.config(function($animateProvider) {
|
||||
$animateProvider.classNameFilter(/animate-/);
|
||||
})
|
||||
.run(function($rootScope) {
|
||||
$rootScope.fileType = 'classfilter';
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('repeatAnimateBenchmark', [])
|
||||
.run(function($rootScope) {
|
||||
$rootScope.fileType = 'noanimate';
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
|
||||
.run(function($rootScope) {
|
||||
$rootScope.fileType = 'default';
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* eslint-env node */
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
scripts: [
|
||||
{
|
||||
id: 'angular',
|
||||
src: '/build/angular.js'
|
||||
},
|
||||
{
|
||||
id: 'angular-animate',
|
||||
src: '/build/angular-animate.js'
|
||||
},
|
||||
{
|
||||
id: 'app',
|
||||
src: 'app.js'
|
||||
},
|
||||
{
|
||||
src: 'common.js'
|
||||
}]
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
(function() {
|
||||
var app = angular.module('repeatAnimateBenchmark');
|
||||
|
||||
app.config(function($compileProvider, $animateProvider) {
|
||||
if ($compileProvider.debugInfoEnabled) {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.run(function($animate) {
|
||||
if ($animate.enabled) {
|
||||
$animate.enabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
app.controller('DataController', function($scope, $rootScope, $animate) {
|
||||
var totalRows = 500;
|
||||
var totalColumns = 20;
|
||||
|
||||
var data = $scope.data = [];
|
||||
|
||||
function fillData() {
|
||||
if ($animate.enabled) {
|
||||
$animate.enabled($scope.benchmarkType !== 'globallyDisabled');
|
||||
}
|
||||
|
||||
for (var i = 0; i < totalRows; i++) {
|
||||
data[i] = [];
|
||||
for (var j = 0; j < totalColumns; j++) {
|
||||
data[i][j] = {
|
||||
i: i
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'enter',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
fillData();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'leave',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
data = $scope.data = [];
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.directive('disableAnimations', function($animate) {
|
||||
return {
|
||||
link: {
|
||||
pre: function(s, e) {
|
||||
$animate.enabled(e, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.directive('noop', function($animate) {
|
||||
return {
|
||||
link: {
|
||||
pre: angular.noop
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.directive('baseline', function($document) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, $element) {
|
||||
var document = $document[0];
|
||||
|
||||
var i, j, row, cell, comment;
|
||||
var template = document.createElement('span');
|
||||
template.setAttribute('ng-repeat', 'foo in foos');
|
||||
template.classList.add('ng-scope');
|
||||
template.appendChild(document.createElement('span'));
|
||||
template.appendChild(document.createTextNode(':'));
|
||||
|
||||
function createList() {
|
||||
for (i = 0; i < $scope.data.length; i++) {
|
||||
row = document.createElement('div');
|
||||
$element[0].appendChild(row);
|
||||
for (j = 0; j < $scope.data[i].length; j++) {
|
||||
cell = template.cloneNode(true);
|
||||
row.appendChild(cell);
|
||||
cell.childNodes[0].textContent = i;
|
||||
cell.ng339 = 'xxx';
|
||||
comment = document.createComment('ngRepeat end: bar in foo');
|
||||
row.appendChild(comment);
|
||||
}
|
||||
|
||||
comment = document.createComment('ngRepeat end: foo in foos');
|
||||
$element[0].appendChild(comment);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch('data.length', function(newVal) {
|
||||
if (newVal === 0) {
|
||||
while ($element[0].firstChild) {
|
||||
$element[0].removeChild($element[0].firstChild);
|
||||
}
|
||||
} else {
|
||||
createList();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
<div ng-app="repeatAnimateBenchmark" ng-cloak>
|
||||
<div ng-controller="DataController">
|
||||
<div class="container-fluid">
|
||||
<p>
|
||||
Tests rendering of an ngRepeat with 500 elements.<br>
|
||||
Animations can be enabled / disabled in different ways.<br>
|
||||
Two tests require reloading the app with different module / app configurations.
|
||||
</p>
|
||||
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="none">none: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="baseline">baseline (vanilla Javascript): </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="enabled">enabled : </label> (requires <a href="./">app.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default' && fileType !== 'classfilter'" value="globallyDisabled">globally disabled:</label> (requires <a href="./">app.js</a> or <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="disabledParentElement">disabled by $animate.enabled() on parent element: </label> (requires <a href="./">app.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'noanimate'" value="noanimate">Without ngAnimate:</label> (requires <a href="?app=app-noanimate.js">app-noanimate.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'classfilter'" value="disabledClassFilter">disabled by classNameFilter on element:</label> (requires <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
|
||||
|
||||
<ng-switch on="benchmarkType">
|
||||
<baseline ng-switch-when="baseline">
|
||||
</baseline>
|
||||
<div ng-switch-when="noanimate">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="enabled">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="globallyDisabled">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="disabledClassFilter">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span class="disable-animations" ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="disabledParentElement">
|
||||
<div disable-animations>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-switch>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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:
|
||||
|
||||
|
||||
+146
-100
@@ -25,7 +25,7 @@ Additionally, we have removed some long-deprecated modules and APIs.
|
||||
|
||||
The most notable changes are:
|
||||
|
||||
- $resource has now support for request and requestError interceptors
|
||||
- `$resource` has now support for request and requestError interceptors
|
||||
|
||||
- Several deprecated features have been removed:
|
||||
- the `$controllerProvider.allowGlobals()` flag
|
||||
@@ -36,8 +36,8 @@ The most notable changes are:
|
||||
- the complete `ngScenario` module
|
||||
|
||||
Please note that feature development (without breaking changes) has happened in parallel on the
|
||||
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those features
|
||||
that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
|
||||
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those
|
||||
features that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
|
||||
|
||||
|
||||
<br />
|
||||
@@ -48,11 +48,11 @@ Below is the full list of breaking changes:
|
||||
<a name="migrate1.6to1.7-ng-directives"></a>
|
||||
### Core: _Directives_
|
||||
|
||||
<a name="migrate1.6to1.7-ng-directives-form"></a>
|
||||
|
||||
#### **form**
|
||||
|
||||
**Due to [223de5](https://github.com/angular/angular.js/commit/223de59e988dc0cc8b4ec3a045b7c0735eba1c77)**,
|
||||
forms will now set $submitted on child forms when they are submitted.
|
||||
forms will now set `$submitted` on child forms when they are submitted.
|
||||
For example:
|
||||
```
|
||||
<form name="parentform" ng-submit="$ctrl.submit()">
|
||||
@@ -63,15 +63,16 @@ For example:
|
||||
</form>
|
||||
```
|
||||
|
||||
Submitting this form will set $submitted on "parentform" and "childform".
|
||||
Submitting this form will set `$submitted` on "parentform" and "childform".
|
||||
Previously, it was only set on "parentform".
|
||||
|
||||
This change was introduced because mixing form and ngForm does not create
|
||||
This change was introduced because mixing `form` and `ngForm` does not create
|
||||
logically separate forms, but rather something like input groups.
|
||||
Therefore, child forms should inherit the submission state from their parent form.
|
||||
|
||||
|
||||
#### **input[radio]** and **input[checkbox]**
|
||||
|
||||
**Due to [656c8f](https://github.com/angular/angular.js/commit/656c8fa8f23b1277cc5c214c4d0237f3393afa1e)**,
|
||||
`input[radio]` and `input[checkbox]` now listen to the "change" event instead of the "click" event.
|
||||
Most apps should not be affected, as "change" is automatically fired by browsers after "click"
|
||||
@@ -84,10 +85,10 @@ Two scenarios might need migration:
|
||||
Before this change, custom click event listeners on radio / checkbox would be called after the
|
||||
input element and `ngModel` had been updated, unless they were specifically registered before
|
||||
the built-in click handlers.
|
||||
After this change, they are called before the input is updated, and can call event.preventDefault()
|
||||
to prevent the input from updating.
|
||||
After this change, they are called before the input is updated, and can call
|
||||
`event.preventDefault()` to prevent the input from updating.
|
||||
|
||||
If an app uses a click event listener that expects ngModel to be updated when it is called, it now
|
||||
If an app uses a click event listener that expects `ngModel` to be updated when it is called, it now
|
||||
needs to register a change event listener instead.
|
||||
|
||||
- Triggering click events:
|
||||
@@ -95,50 +96,52 @@ needs to register a change event listener instead.
|
||||
Conventional trigger functions:
|
||||
|
||||
The change event might not be fired when the input element is not attached to the document. This
|
||||
can happen in **tests** that compile input elements and
|
||||
trigger click events on them. Depending on the browser (Chrome and Safari) and the trigger method,
|
||||
the change event will not be fired when the input isn't attached to the document.
|
||||
can happen in **tests** that compile input elements and trigger click events on them. Depending on
|
||||
the browser (Chrome and Safari) and the trigger method, the change event will not be fired when the
|
||||
input isn't attached to the document.
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
it('should update the model', inject(function($compile, $rootScope) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
it('should update the model', inject(function($compile, $rootScope) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
With this patch, `$rootScope.checkbox` might not be true, because the click event
|
||||
hasn't triggered the change event. To make the test, work append the inputElm to the app's
|
||||
`$rootElement`, and the `$rootElement` to the `$document`.
|
||||
With this patch, `$rootScope.checkbox` might not be true, because the click event hasn't triggered
|
||||
the change event. To make the test, work append `inputElm` to the app's `$rootElement`, and the
|
||||
`$rootElement` to the `$document`.
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
|
||||
$rootElement.append(inputElm);
|
||||
$document.append($rootElement);
|
||||
$rootElement.append(inputElm);
|
||||
$document.append($rootElement);
|
||||
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
#### **input\[number\]**
|
||||
|
||||
**Due to [aa3f95](https://github.com/angular/angular.js/commit/aa3f951330ec7b10b43ea884d9b5754e296770ec)**,
|
||||
`input[type=number]` with `ngModel` now validates the input for the `max`/`min` restriction against
|
||||
the `ngModelController.$viewValue` instead of against the `ngModelController.$modelValue`.
|
||||
|
||||
This affects apps that use `$parsers` or `$formatters` to transform the input / model value.
|
||||
|
||||
If you rely on the $modelValue validation, you can overwrite the `min`/`max` validator from a custom directive, as seen in the following example directive definition object:
|
||||
If you rely on the `$modelValue` validation, you can overwrite the `min`/`max` validator from a
|
||||
custom directive, as seen in the following example directive definition object:
|
||||
|
||||
```
|
||||
```js
|
||||
{
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
@@ -154,11 +157,11 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
|
||||
|
||||
|
||||
#### **ngModel, input**
|
||||
|
||||
**Due to [74b04c](https://github.com/angular/angular.js/commit/74b04c9403af4fc7df5b6420f22c9f45a3e84140)**,
|
||||
*Custom* parsers that fail to parse on input types "email", "url", "number", "date", "month",
|
||||
"time", "datetime-local", "week", no longer set `ngModelController.$error[inputType]`, and
|
||||
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" do no
|
||||
longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
|
||||
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" no longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
|
||||
|
||||
Instead, any custom parsers on these inputs set `ngModelController.$error.parse` and
|
||||
`ng-invalid-parse`. This change was made to make distinguishing errors from built-in parsers
|
||||
@@ -166,6 +169,7 @@ and custom parsers easier.
|
||||
|
||||
|
||||
#### **ngModelOptions**
|
||||
|
||||
**Due to [55ba44](https://github.com/angular/angular.js/commit/55ba44913e02650b56410aa9ab5eeea5d3492b68)**,
|
||||
the 'default' key in 'debounce' now only debounces the default event, i.e. the event that is added
|
||||
as an update trigger by the different input directives automatically.
|
||||
@@ -179,24 +183,24 @@ See the following example:
|
||||
|
||||
Pre-1.7:
|
||||
'mouseup' is also debounced by 500 milliseconds because 'default' is applied:
|
||||
```
|
||||
```html
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { 'default': 500, 'blur': 0 }
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
1.7:
|
||||
The pre-1.7 behavior can be re-created by setting '*' as a catch-all debounce value:
|
||||
```
|
||||
```html
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { '*': 500, 'blur': 0 }
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
In contrast, when only 'default' is used, 'blur' and 'mouseup' are not debounced:
|
||||
```
|
||||
```html
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { 'default': 500 }
|
||||
@@ -207,14 +211,15 @@ ng-model-options="{
|
||||
#### **ngStyle**
|
||||
|
||||
**Due to [15bbd3](https://github.com/angular/angular.js/commit/15bbd3e18cd89b91f7206a06c73d40e54a8a48a0)**,
|
||||
previously the use of deep watch by ng-style would trigger styles to be
|
||||
re-applied when nested state changed. Now only changes to direct
|
||||
properties of the watched object will trigger changes.
|
||||
the use of deep-watching in `ngStyle` has changed. Previously, `ngStyle` would trigger styles to be
|
||||
re-applied whenever nested state changed. Now, only changes to direct properties of the watched
|
||||
object will trigger changes.
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ng-services"></a>
|
||||
### Core: _Services_
|
||||
|
||||
|
||||
#### **$compile**
|
||||
|
||||
**Due to [38f8c9](https://github.com/angular/angular.js/commit/38f8c97af74649ce224b6dd45f433cc665acfbfb)**,
|
||||
@@ -238,16 +243,16 @@ migrating to AngularJS 1.7.0 shouldn't require any further action.
|
||||
3. If you specified `$compileProvider.preAssignBindingsEnabled(true)` you need
|
||||
to first migrate your code so that the flag can be flipped to `false`. The
|
||||
instructions on how to do that are available in the "Migrating from 1.5 to 1.6"
|
||||
guide:
|
||||
https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
|
||||
guide: https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
|
||||
Afterwards, remove the `$compileProvider.preAssignBindingsEnabled(true)`
|
||||
statement.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [6ccbfa](https://github.com/angular/angular.js/commit/6ccbfa65d60a3dc396d0cf6da21b993ad74653fd)**,
|
||||
the `xlink:href` security context for SVG's `a` and `image` elements has been lowered.
|
||||
|
||||
In the unlikely case that an app relied on RESOURCE_URL whitelisting for the
|
||||
In the unlikely case that an app relied on `RESOURCE_URL` whitelisting for the
|
||||
purpose of binding to the `xlink:href` property of SVG's `<a>` or `<image>`
|
||||
elements and if the values do not pass the regular URL sanitization, they will
|
||||
break.
|
||||
@@ -258,37 +263,39 @@ To fix this you need to ensure that the values used for binding to the affected
|
||||
`imgSrcSanitizationWhitelist` (for `<image>` elements).
|
||||
|
||||
<hr />
|
||||
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
|
||||
deepWatch is no longer used in in literal one-way bindings.
|
||||
|
||||
Previously when a literal value was passed into a directive/component via
|
||||
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
|
||||
deep-watching is no longer used in literal one-way bindings.
|
||||
|
||||
Previously, when a literal value was passed into a directive/component via
|
||||
one-way binding it would be watched with a deep watcher.
|
||||
|
||||
For example, for `<my-component input="[a]">`, a new instance of the array
|
||||
would be passed into the directive/component (and trigger $onChanges) not
|
||||
would be passed into the directive/component (and trigger `$onChanges`) not
|
||||
only if `a` changed but also if any sub property of `a` changed such as
|
||||
`a.b` or `a.b.c.d.e` etc.
|
||||
|
||||
This also means a new but equal value for `a` would NOT trigger such a
|
||||
change.
|
||||
|
||||
Now literal values use an input-based watch similar to other directive/component
|
||||
Now, literal values use an input-based watch similar to other directive/component
|
||||
one-way bindings. In this context inputs are the non-constant parts of the
|
||||
literal. In the example above the input would be `a`. Changes are only
|
||||
triggered when the inputs to the literal change.
|
||||
literal. In the example above, the input would be `a`. Changes are only
|
||||
triggered, when the inputs to the literal change.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [1cf728](https://github.com/angular/angular.js/commit/1cf728e209a9e0016068fac2769827e8f747760e)**,
|
||||
`base[href]` was added to the list of RESOURCE_URL context attributes.
|
||||
`base[href]` was added to the list of `RESOURCE_URL` context attributes.
|
||||
|
||||
Previously, `<base href="{{ $ctrl.baseUrl }}" />` would not require `baseUrl` to
|
||||
be trusted as a RESOURCE_URL. Now, `baseUrl` will be sent to `$sce`'s
|
||||
RESOURCE_URL checks. By default, it will break unless `baseUrl` is of the same
|
||||
be trusted as a `RESOURCE_URL`. Now, `baseUrl` will be sent to `$sce`'s
|
||||
`RESOURCE_URL` checks. By default, it will break unless `baseUrl` is of the same
|
||||
origin as the application document.
|
||||
|
||||
Refer to the
|
||||
[`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce)
|
||||
for more info on how to trust a value in a RESOURCE_URL context.
|
||||
for more info on how to trust a value in a `RESOURCE_URL` context.
|
||||
|
||||
Also, concatenation in trusted contexts is not allowed, which means that the
|
||||
following won't work: `<base href="/something/{{ $ctrl.partialPath }}" />`.
|
||||
@@ -315,10 +322,10 @@ except for the simplest of cases):
|
||||
**Due to ([c2b8fa](https://github.com/angular/angular.js/commit/c2b8fab0a480204374d561d6b9b3d47347ac5570))**,
|
||||
the arguments of `$watchGroup` callbacks have changed.
|
||||
|
||||
Previously when using `$watchGroup` the entries in `newValues` and
|
||||
Previously, when using `$watchGroup`, the entries in `newValues` and
|
||||
`oldValues` represented the *most recent change of each entry*.
|
||||
|
||||
Now the entries in `oldValues` will always equal the `newValues` of the previous
|
||||
Now, the entries in `oldValues` will always equal the `newValues` of the previous
|
||||
call of the listener. This means comparing the entries in `newValues` and
|
||||
`oldValues` can be used to determine which individual expressions changed.
|
||||
|
||||
@@ -343,7 +350,7 @@ Now the `oldValue` will always equal the previous `newValue`:
|
||||
|
||||
Note the last call now shows `a === 2` in the `oldValues` array.
|
||||
|
||||
This also makes the `oldValue` of one-time watchers more clear. Previously
|
||||
This also makes the `oldValue` of one-time watchers more clear. Previously,
|
||||
the `oldValue` of a one-time watcher would remain `undefined` forever. For
|
||||
example `$scope.$watchGroup(['a', '::b'], fn)` would previously:
|
||||
|
||||
@@ -367,7 +374,7 @@ Where now the `oldValue` will always equal the previous `newValue`:
|
||||
#### **$interval**
|
||||
|
||||
**Due to [a8bef9](https://github.com/angular/angular.js/commit/a8bef95127775d83d80daa4617c33227c4b443d4)**,
|
||||
`$interval.cancel() will throw an error if called with a promise that was not generated by
|
||||
`$interval.cancel()` will throw an error if called with a promise that was not generated by
|
||||
`$interval()`. Previously, it would silently do nothing.
|
||||
|
||||
Before:
|
||||
@@ -393,7 +400,7 @@ $interval.cancel(promise); // Interval canceled.
|
||||
#### **$timeout**
|
||||
|
||||
**Due to [336525](https://github.com/angular/angular.js/commit/3365256502344970f86355d3ace1cb4251ae9828)**,
|
||||
`$timeout.cancel() will throw an error if called with a promise that was not generated by
|
||||
`$timeout.cancel()` will throw an error if called with a promise that was not generated by
|
||||
`$timeout()`. Previously, it would silently do nothing.
|
||||
|
||||
Before:
|
||||
@@ -417,10 +424,11 @@ $timeout.cancel(promise); // Timeout canceled.
|
||||
|
||||
|
||||
#### **$cookies**
|
||||
|
||||
**Due to [73c646](https://github.com/angular/angular.js/commit/73c6467f1468353215dc689c019ed83aa4993c77)**,
|
||||
the `$cookieStore`service has been removed. Migrate to the $cookies service. Note that
|
||||
for object values you need to use the `putObject` & `getObject` methods as
|
||||
`get`/`put` will not correctly save/retrieve them.
|
||||
the `$cookieStore`service has been removed. Migrate to the `$cookies` service. Note that
|
||||
for object values you need to use the `putObject` & `getObject` methods, as
|
||||
`get`/`put` will not correctly save/retrieve the object values.
|
||||
|
||||
Before:
|
||||
```js
|
||||
@@ -433,29 +441,31 @@ $cookieStore.remove('name');
|
||||
#### **$templateRequest**
|
||||
|
||||
**Due to [c617d6](https://github.com/angular/angular.js/commit/c617d6dceee5b000bfceda44ced22fc16b48b18b)**,
|
||||
give tpload error namespace has changed. Previously the `tpload` error was namespaced to `$compile`.
|
||||
If you have code that matches errors of the form `[$compile:tpload]` it will no
|
||||
longer run. You should change the code to match
|
||||
`[$templateRequest:tpload]`.
|
||||
the `tpload` error namespace has changed. Previously, the `tpload` error was namespaced to
|
||||
`$compile`. If you have code that matches errors of the form `[$compile:tpload]` it will no longer
|
||||
run. You should change the code to match `[$templateRequest:tpload]`.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to ([fb0099](https://github.com/angular/angular.js/commit/fb00991460cf69ae8bc7f1f826363d09c73c0d5e)**,
|
||||
the service now returns the result of `$templateCache.put()` when making a server request to the
|
||||
template. Previously it would return the content of the response directly.
|
||||
This now means if you are decorating `$templateCache.put()` to manipulate the template, you will
|
||||
now get this manipulated result also on the first `$templateRequest` rather than only on subsequent
|
||||
calls (when the template is retrived from the cache).
|
||||
In practice this should not affect any apps, as it is unlikely that they rely on the template being
|
||||
`$templateRequest()` now returns the result of `$templateCache.put()` when making a server request
|
||||
for a template. Previously, it would return the content of the response directly.
|
||||
|
||||
This means that if you are decorating `$templateCache.put()` to manipulate the template, you will
|
||||
now get this manipulated result also on the first `$templateRequest()` call rather than only on
|
||||
subsequent calls (when the template is retrieved from the cache).
|
||||
|
||||
In practice, this should not affect any apps, as it is unlikely that they rely on the template being
|
||||
different in the first and subsequent calls.
|
||||
|
||||
|
||||
#### **$animate**
|
||||
|
||||
**Due to [16b82c](https://github.com/angular/angular.js/commit/16b82c6afe0ab916fef1d6ca78053b00bf5ada83)**,
|
||||
$animate.cancel(runner) now rejects the underlying
|
||||
promise and calls the catch() handler on the runner
|
||||
returned by $animate functions (enter, leave, move,
|
||||
addClass, removeClass, setClass, animate).
|
||||
Previously it would resolve the promise as if the animation
|
||||
had ended successfully.
|
||||
`$animate.cancel(runner)` now rejects the underlying promise and calls the `catch()` handler on the
|
||||
runner returned by `$animate` functions (`enter`, `leave`, `move`, `addClass`, `removeClass`,
|
||||
`setClass`, `animate`).
|
||||
Previously, it would resolve the promise as if the animation had ended successfully.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -468,7 +478,7 @@ runner.cancel();
|
||||
```
|
||||
|
||||
Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'.
|
||||
To migrate, add a catch() handler to your animation runners.
|
||||
To migrate, add a `catch()` handler to your animation runners.
|
||||
|
||||
|
||||
#### **$controller**
|
||||
@@ -479,38 +489,74 @@ has been removed. Likewise, the deprecated `$controllerProvider.allowGlobals()`
|
||||
method that could enable this behavior, has been removed.
|
||||
|
||||
This behavior had been deprecated since AngularJS v1.3.0, because polluting the global scope
|
||||
is bad. To migrate, remove the call to $controllerProvider.allowGlobals() in the config, and
|
||||
register your controller via the Module API or the $controllerProvider, e.g.
|
||||
is considered bad practice. To migrate, remove the call to `$controllerProvider.allowGlobals()` in
|
||||
the config, and register your controller via the Module API or the `$controllerProvider`, e.g.:
|
||||
|
||||
```
|
||||
```js
|
||||
angular.module('myModule', []).controller('myController', function() {...});
|
||||
|
||||
// or
|
||||
|
||||
angular.module('myModule', []).config(function($controllerProvider) {
|
||||
$controllerProvider.register('myController', function() {...});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### **$sce**
|
||||
|
||||
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
|
||||
if you use `attrs.$set` for URL attributes (a[href] and img[src]) there will no
|
||||
if you use `attrs.$set` for URL attributes (`a[href]` and `img[src]`) there will no
|
||||
longer be any automated sanitization of the value. This is in line with other
|
||||
programmatic operations, such as writing to the innerHTML of an element.
|
||||
programmatic operations, such as writing to the `innerHTML` of an element.
|
||||
|
||||
If you are programmatically writing URL values to attributes from untrusted
|
||||
input then you must sanitize it yourself. You could write your own sanitizer or copy
|
||||
input, then you must sanitize it yourself. You could write your own sanitizer or copy
|
||||
the private `$$sanitizeUri` service.
|
||||
|
||||
Note that values that have been passed through the `$interpolate` service within the
|
||||
`URL` or `MEDIA_URL` will have already been sanitized, so you would not need to sanitize
|
||||
these values again.
|
||||
|
||||
<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>
|
||||
### Core: _Filters_
|
||||
|
||||
|
||||
#### **orderBy**
|
||||
|
||||
**Due to [1d8046](https://github.com/angular/angular.js/commit/1d804645f7656d592c90216a0355b4948807f6b8)**,
|
||||
when using `orderBy` to sort arrays containing `null` values, the `null` values
|
||||
will be considered "greater than" all other values, except for `undefined`.
|
||||
@@ -535,8 +581,9 @@ orderByFilter(['a', undefined, 'o', null, 'z']);
|
||||
|
||||
|
||||
#### **jqLite**
|
||||
|
||||
**Due to [b7d396](https://github.com/angular/angular.js/commit/b7d396b8b6e8f27a1f4556d58fc903321e8d532a)**,
|
||||
removeData() no longer removes event handlers.
|
||||
`removeData()` no longer removes event handlers.
|
||||
|
||||
Before this commit `removeData()` invoked on an element removed its event
|
||||
handlers as well. If you want to trigger a full cleanup of an element, change:
|
||||
@@ -561,22 +608,20 @@ elem.remove();
|
||||
will remove event handlers as well.
|
||||
|
||||
|
||||
|
||||
#### **Angular**
|
||||
#### **Helpers**
|
||||
|
||||
**Due to [1daa4f](https://github.com/angular/angular.js/commit/1daa4f2231a89ee88345689f001805ffffa9e7de)**,
|
||||
the helper functions `angular.lowercase` `and angular.uppercase` have been removed.
|
||||
the helper functions `angular.lowercase` and `angular.uppercase` have been removed.
|
||||
|
||||
These functions have been deprecated since 1.5.0. They are internally
|
||||
used, but should not be exposed as they contain special locale handling
|
||||
(for Turkish) to maintain internal consistency regardless of user-set locale.
|
||||
|
||||
Developers should generally use the built-ins `toLowerCase` and `toUpperCase`
|
||||
Developers should generally use the built-in methods `toLowerCase` and `toUpperCase`
|
||||
or `toLocaleLowerCase` and `toLocaleUpperCase` for special cases.
|
||||
|
||||
Further, we generally discourage using the angular.x helpers in application code.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [e3ece2](https://github.com/angular/angular.js/commit/e3ece2fad9e1e6d47b5f06815ff186d7e6f44948)**,
|
||||
`angular.isArray()` now supports Array subclasses.
|
||||
|
||||
@@ -597,18 +642,19 @@ be able to handle these objects better when copying or watching.
|
||||
### ngAria
|
||||
|
||||
**Due to [6d5ef3](https://github.com/angular/angular.js/commit/6d5ef34fc6a974cde73157ba94f9706723dd8f5b)**,
|
||||
the ngAria directive no longer sets aria-* attributes on input[type="hidden"] with ngModel.
|
||||
This can affect apps that test for the presence of aria attributes on hidden inputs.
|
||||
`ngAria` no longer sets `aria-*` attributes on `input[type="hidden"]` with `ngModel`.
|
||||
This can affect apps that test for the presence of ARIA attributes on hidden inputs.
|
||||
To migrate, remove these assertions.
|
||||
In actual apps, this should not have a user-facing effect, as the previous behavior
|
||||
was incorrect, and the new behavior is correct for accessibility.
|
||||
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngResource"></a>
|
||||
### ngResource
|
||||
|
||||
|
||||
#### **$resource**
|
||||
|
||||
**Due to [ea0585](https://github.com/angular/angular.js/commit/ea0585773bb93fd891576e2271254a17e15f1ddd)**,
|
||||
the behavior of interceptors and success/error callbacks has changed.
|
||||
|
||||
@@ -690,6 +736,7 @@ User.get({id: 2}, onSuccess, onError);
|
||||
```
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [240a3d](https://github.com/angular/angular.js/commit/240a3ddbf12a9bb79754031be95dae4b6bd2dded)**,
|
||||
`$http` will be called asynchronously from `$resource` methods
|
||||
(regardless if a `request`/`requestError` interceptor has been defined).
|
||||
@@ -728,7 +775,6 @@ it('...', function() {
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngScenario"></a>
|
||||
### ngScenario
|
||||
|
||||
@@ -736,7 +782,7 @@ it('...', function() {
|
||||
the angular scenario runner end-to-end test framework has been
|
||||
removed from the project and will no longer be available on npm
|
||||
or bower starting with 1.7.0.
|
||||
It was deprecated and removed from the documentation in 2014.
|
||||
It has been deprecated and removed from the documentation since 2014.
|
||||
Applications that still use it should migrate to
|
||||
[Protractor](http://www.protractortest.org).
|
||||
Technically, it should also be possible to continue using an
|
||||
@@ -748,10 +794,10 @@ not changed. However, we do not guarantee future compatibility.
|
||||
### ngTouch
|
||||
|
||||
**Due to [11d9ad](https://github.com/angular/angular.js/commit/11d9ad1eb25eaf5967195e424108207427835d50)**,
|
||||
the `ngClick` directive from the ngTouch module has been removed, and with it the
|
||||
the `ngClick` directive of the `ngTouch` module has been removed, and with it the
|
||||
corresponding `$touchProvider` and `$touch` service.
|
||||
|
||||
If you have included ngTouch v1.5.0 or higher in your application, and have not
|
||||
If you have included `ngTouch` v1.5.0 or higher in your application, and have not
|
||||
changed the value of `$touchProvider.ngClickOverrideEnabled()`, or injected and used the `$touch`
|
||||
service, then there are no migration steps for your code. Otherwise you must remove references to
|
||||
the provider and service.
|
||||
|
||||
@@ -102,7 +102,7 @@ For more information please visit {@link $http#json-vulnerability-protection JSO
|
||||
|
||||
Bear in mind that calling `$http.jsonp` gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
|
||||
instant remote code execution in your application: the result of these requests is handed off
|
||||
to the browser as regular `<script>` tag.
|
||||
to the browser as a regular `<script>` tag.
|
||||
|
||||
## Strict Contextual Escaping
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,26 +7,9 @@
|
||||
This page describes the support status of the significant versions of AngularJS.
|
||||
|
||||
<div class="alert alert-info">
|
||||
AngularJS is planning one more significant release, version 1.7, and on July 1, 2018 it will enter a 3 year Long Term Support period.
|
||||
On July 1, 2018 AngularJS entered a 3 year Long Term Support period.
|
||||
</div>
|
||||
|
||||
### Until July 1st 2018
|
||||
|
||||
Any version branch not shown in the following table (e.g. 1.5.x) is no longer being developed.
|
||||
|
||||
<table class="dev-status table table-bordered">
|
||||
<thead>
|
||||
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="security"><td><span>1.2.x</span></td><td>Security patches only</td><td>Last version to provide IE 8 support</td></tr>
|
||||
<tr class="stable"><td><span>1.6.x</span></td><td>Patch Releases</td><td>Minor features, bug fixes, security patches - no breaking changes</td></tr>
|
||||
<tr class="current"><td><span>1.7.x</span></td><td>Active Development</td><td>1.7.0 (not yet released) will be the last release of AngularJS to contain breaking changes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### After July 1st 2018
|
||||
|
||||
Any version branch not shown in the following table (e.g. 1.6.x) is no longer being developed.
|
||||
|
||||
<table class="dev-status table table-bordered">
|
||||
@@ -36,7 +19,7 @@ Any version branch not shown in the following table (e.g. 1.6.x) is no longer be
|
||||
<tbody>
|
||||
<tr class="security">
|
||||
<td><span>1.2.x</span></td>
|
||||
<td>Long Term Support</td>
|
||||
<td>Security patches only</td>
|
||||
<td>Last version to provide IE 8 support</td>
|
||||
</tr>
|
||||
<tr class="stable">
|
||||
@@ -49,14 +32,16 @@ Any version branch not shown in the following table (e.g. 1.6.x) is no longer be
|
||||
|
||||
### Long Term Support
|
||||
|
||||
On July 1st 2018, we will enter a Long Term Support period for AngularJS.
|
||||
On July 1st 2018, AngularJS entered a Long Term Support period for AngularJS.
|
||||
|
||||
At this time we will focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
|
||||
We now focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
|
||||
|
||||
* A security flaw is detected in the 1.7.x branch of the framework
|
||||
* One of the major browsers releases a version that will cause current production applications using AngularJS 1.7.x to stop working
|
||||
* The jQuery library releases a version that will cause current production applications using AngularJS 1.7.x to stop working.
|
||||
|
||||
AngularJS 1.2.x will get a new version if and only if a new severe security issue is discovered.
|
||||
|
||||
### Blog Post
|
||||
|
||||
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
|
||||
|
||||
+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"
|
||||
|
||||
+7
-7
@@ -33,7 +33,7 @@
|
||||
"cross-spawn": "^4.0.0",
|
||||
"cz-conventional-changelog": "1.1.4",
|
||||
"dgeni": "^0.4.9",
|
||||
"dgeni-packages": "^0.26.2",
|
||||
"dgeni-packages": "^0.26.5",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"event-stream": "~3.1.0",
|
||||
"glob": "^6.0.1",
|
||||
@@ -63,18 +63,18 @@
|
||||
"jquery": "3.2.1",
|
||||
"jquery-2.1": "npm:jquery@2.1.4",
|
||||
"jquery-2.2": "npm:jquery@2.2.4",
|
||||
"karma": "^2.0.0",
|
||||
"karma-browserstack-launcher": "^1.2.0",
|
||||
"karma-chrome-launcher": "^2.1.1",
|
||||
"karma": "^2.0.4",
|
||||
"karma-browserstack-launcher": "^1.3.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-edge-launcher": "^0.4.2",
|
||||
"karma-firefox-launcher": "^1.0.1",
|
||||
"karma-firefox-launcher": "^1.1.0",
|
||||
"karma-ie-launcher": "^1.0.0",
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-junit-reporter": "^1.2.0",
|
||||
"karma-safari-launcher": "^1.0.0",
|
||||
"karma-sauce-launcher": "^1.2.0",
|
||||
"karma-script-launcher": "^1.0.0",
|
||||
"karma-spec-reporter": "^0.0.31",
|
||||
"karma-spec-reporter": "^0.0.32",
|
||||
"load-grunt-tasks": "^3.5.0",
|
||||
"lodash": "~2.4.1",
|
||||
"log4js": "^0.6.27",
|
||||
|
||||
@@ -171,9 +171,15 @@
|
||||
/* ng/q.js */
|
||||
"markQExceptionHandled": false,
|
||||
|
||||
/* sce.js */
|
||||
"SCE_CONTEXTS": false,
|
||||
|
||||
/* ng/directive/directives.js */
|
||||
"ngDirective": false,
|
||||
|
||||
/* ng/directive/ngEventDirs.js */
|
||||
"createEventDirective": false,
|
||||
|
||||
/* ng/directive/input.js */
|
||||
"VALID_CLASS": false,
|
||||
"INVALID_CLASS": false,
|
||||
|
||||
+12
-6
@@ -792,15 +792,16 @@ function arrayRemove(array, value) {
|
||||
* * If `source` is identical to `destination` an exception will be thrown.
|
||||
*
|
||||
* <br />
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* Only enumerable properties are taken into account. Non-enumerable properties (both on `source`
|
||||
* and on `destination`) will be ignored.
|
||||
* </div>
|
||||
*
|
||||
* @param {*} source The source that will be used to make a copy.
|
||||
* Can be any type, including primitives, `null`, and `undefined`.
|
||||
* @param {(Object|Array)=} destination Destination into which the source is copied. If
|
||||
* provided, must be of the same type as `source`.
|
||||
* @param {*} source The source that will be used to make a copy. Can be any type, including
|
||||
* primitives, `null`, and `undefined`.
|
||||
* @param {(Object|Array)=} destination Destination into which the source is copied. If provided,
|
||||
* must be of the same type as `source`.
|
||||
* @returns {*} The copy or updated `destination`, if `destination` was specified.
|
||||
*
|
||||
* @example
|
||||
@@ -1695,8 +1696,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 +1915,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'];
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
* For example, if an item is added to the collection, `ngRepeat` will know that all other items
|
||||
* already have DOM elements, and will not re-render them.
|
||||
*
|
||||
* All different types of tracking functions, their syntax, and and their support for duplicate
|
||||
* All different types of tracking functions, their syntax, and their support for duplicate
|
||||
* items in collections can be found in the
|
||||
* {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
|
||||
*
|
||||
|
||||
@@ -383,7 +383,7 @@ var SelectController =
|
||||
|
||||
if (optionAttrs.$attr.ngValue) {
|
||||
// The value attribute is set by ngValue
|
||||
var oldVal, hashedVal = NaN;
|
||||
var oldVal, hashedVal;
|
||||
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
|
||||
|
||||
var removal;
|
||||
@@ -556,18 +556,6 @@ var SelectController =
|
||||
* {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
|
||||
*
|
||||
*
|
||||
* @knownIssue
|
||||
*
|
||||
* In Firefox, the select model is only updated when the select element is blurred. For example,
|
||||
* when switching between options with the keyboard, the select model is only set to the
|
||||
* currently selected option when the select is blurred, e.g via tab key or clicking the mouse
|
||||
* outside the select.
|
||||
*
|
||||
* This is due to an ambiguity in the select element specification. See the
|
||||
* [issue on the Firefox bug tracker](https://bugzilla.mozilla.org/show_bug.cgi?id=126379)
|
||||
* for more information, and this
|
||||
* [Github comment for a workaround](https://github.com/angular/angular.js/issues/9134#issuecomment-130800488)
|
||||
*
|
||||
* @example
|
||||
* ### Simple `select` elements with static options
|
||||
*
|
||||
|
||||
+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');
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
+2
-2
@@ -440,7 +440,7 @@ function $SceDelegateProvider() {
|
||||
// If we get here, then we will either sanitize the value or throw an exception.
|
||||
if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) {
|
||||
// we attempt to sanitize non-resource URLs
|
||||
return $$sanitizeUri(maybeTrusted, type === SCE_CONTEXTS.MEDIA_URL);
|
||||
return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL);
|
||||
} else if (type === SCE_CONTEXTS.RESOURCE_URL) {
|
||||
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
|
||||
return maybeTrusted;
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+56
-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);
|
||||
@@ -128,8 +130,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 +137,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 +182,31 @@ 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(element[0], animationEntry.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 +236,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 +399,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 +442,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);
|
||||
|
||||
+8
-1
@@ -387,7 +387,14 @@ 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) {
|
||||
// If the event is triggered on a non-interactive element ...
|
||||
if (nodeBlackList.indexOf(event.target.nodeName) === -1) {
|
||||
// ... prevent the default browser behavior (e.g. scrolling when pressing spacebar)
|
||||
// See https://github.com/angular/angular.js/issues/16664
|
||||
event.preventDefault();
|
||||
}
|
||||
scope.$apply(callback);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+411
-263
@@ -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);
|
||||
return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys);
|
||||
var parsed = parseRouteUrl(url);
|
||||
return $httpBackend.when(method, parsed.regexp, undefined, undefined, parsed.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,8 +1955,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* See {@link ngMock.$httpBackend#expect `expect`} for more info.
|
||||
*/
|
||||
$httpBackend.expectRoute = function(method, url) {
|
||||
var pathObj = parseRoute(url);
|
||||
return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys);
|
||||
var parsed = parseRouteUrl(url);
|
||||
return $httpBackend.expect(method, parsed.regexp, undefined, undefined, parsed.keys);
|
||||
};
|
||||
|
||||
|
||||
@@ -2005,6 +2084,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseRouteUrl(url) {
|
||||
var strippedUrl = stripQueryAndHash(url);
|
||||
var parseOptions = {caseInsensitiveMatch: true, ignoreTrailingSlashes: true};
|
||||
return routeToRegExp(strippedUrl, parseOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function assertArgDefined(args, index, name) {
|
||||
@@ -2013,110 +2098,124 @@ function assertArgDefined(args, index, name) {
|
||||
}
|
||||
}
|
||||
|
||||
function stripQueryAndHash(url) {
|
||||
return url.replace(/[?#].*$/, '');
|
||||
}
|
||||
|
||||
function MockHttpExpectation(method, url, data, headers, keys) {
|
||||
function MockHttpExpectation(expectedMethod, expectedUrl, expectedData, expectedHeaders,
|
||||
expectedKeys) {
|
||||
|
||||
function getUrlParams(u) {
|
||||
var params = u.slice(u.indexOf('?') + 1).split('&');
|
||||
return params.sort();
|
||||
}
|
||||
this.data = expectedData;
|
||||
this.headers = expectedHeaders;
|
||||
|
||||
function compareUrl(u) {
|
||||
return (url.slice(0, url.indexOf('?')) === u.slice(0, u.indexOf('?')) &&
|
||||
getUrlParams(url).join() === getUrlParams(u).join());
|
||||
}
|
||||
|
||||
this.data = data;
|
||||
this.headers = headers;
|
||||
|
||||
this.match = function(m, u, d, h) {
|
||||
if (method !== m) return false;
|
||||
if (!this.matchUrl(u)) return false;
|
||||
if (angular.isDefined(d) && !this.matchData(d)) return false;
|
||||
if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
|
||||
this.match = function(method, url, data, headers) {
|
||||
if (expectedMethod !== method) return false;
|
||||
if (!this.matchUrl(url)) return false;
|
||||
if (angular.isDefined(data) && !this.matchData(data)) return false;
|
||||
if (angular.isDefined(headers) && !this.matchHeaders(headers)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
this.matchUrl = function(u) {
|
||||
if (!url) return true;
|
||||
if (angular.isFunction(url.test)) return url.test(u);
|
||||
if (angular.isFunction(url)) return url(u);
|
||||
return (url === u || compareUrl(u));
|
||||
this.matchUrl = function(url) {
|
||||
if (!expectedUrl) return true;
|
||||
if (angular.isFunction(expectedUrl.test)) return expectedUrl.test(url);
|
||||
if (angular.isFunction(expectedUrl)) return expectedUrl(url);
|
||||
return (expectedUrl === url || compareUrlWithQuery(url));
|
||||
};
|
||||
|
||||
this.matchHeaders = function(h) {
|
||||
if (angular.isUndefined(headers)) return true;
|
||||
if (angular.isFunction(headers)) return headers(h);
|
||||
return angular.equals(headers, h);
|
||||
this.matchHeaders = function(headers) {
|
||||
if (angular.isUndefined(expectedHeaders)) return true;
|
||||
if (angular.isFunction(expectedHeaders)) return expectedHeaders(headers);
|
||||
return angular.equals(expectedHeaders, headers);
|
||||
};
|
||||
|
||||
this.matchData = function(d) {
|
||||
if (angular.isUndefined(data)) return true;
|
||||
if (data && angular.isFunction(data.test)) return data.test(d);
|
||||
if (data && angular.isFunction(data)) return data(d);
|
||||
if (data && !angular.isString(data)) {
|
||||
return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d));
|
||||
this.matchData = function(data) {
|
||||
if (angular.isUndefined(expectedData)) return true;
|
||||
if (expectedData && angular.isFunction(expectedData.test)) return expectedData.test(data);
|
||||
if (expectedData && angular.isFunction(expectedData)) return expectedData(data);
|
||||
if (expectedData && !angular.isString(expectedData)) {
|
||||
return angular.equals(angular.fromJson(angular.toJson(expectedData)), angular.fromJson(data));
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return data == d;
|
||||
return expectedData == data;
|
||||
};
|
||||
|
||||
this.toString = function() {
|
||||
return method + ' ' + url;
|
||||
return expectedMethod + ' ' + expectedUrl;
|
||||
};
|
||||
|
||||
this.params = function(u) {
|
||||
return angular.extend(parseQuery(), pathParams());
|
||||
this.params = function(url) {
|
||||
var queryStr = url.indexOf('?') === -1 ? '' : url.substring(url.indexOf('?') + 1);
|
||||
var strippedUrl = stripQueryAndHash(url);
|
||||
|
||||
function pathParams() {
|
||||
var keyObj = {};
|
||||
if (!url || !angular.isFunction(url.test) || !keys || keys.length === 0) return keyObj;
|
||||
|
||||
var m = url.exec(u);
|
||||
if (!m) return keyObj;
|
||||
for (var i = 1, len = m.length; i < len; ++i) {
|
||||
var key = keys[i - 1];
|
||||
var val = m[i];
|
||||
if (key && val) {
|
||||
keyObj[key.name || key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return keyObj;
|
||||
}
|
||||
|
||||
function parseQuery() {
|
||||
var obj = {}, key_value, key,
|
||||
queryStr = u.indexOf('?') > -1
|
||||
? u.substring(u.indexOf('?') + 1)
|
||||
: '';
|
||||
|
||||
angular.forEach(queryStr.split('&'), function(keyValue) {
|
||||
if (keyValue) {
|
||||
key_value = keyValue.replace(/\+/g,'%20').split('=');
|
||||
key = tryDecodeURIComponent(key_value[0]);
|
||||
if (angular.isDefined(key)) {
|
||||
var val = angular.isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
|
||||
if (!hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = val;
|
||||
} else if (angular.isArray(obj[key])) {
|
||||
obj[key].push(val);
|
||||
} else {
|
||||
obj[key] = [obj[key],val];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
function tryDecodeURIComponent(value) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (e) {
|
||||
// Ignore any invalid uri component
|
||||
}
|
||||
}
|
||||
return angular.extend(extractParamsFromQuery(queryStr), extractParamsFromPath(strippedUrl));
|
||||
};
|
||||
|
||||
function compareUrlWithQuery(url) {
|
||||
var urlWithQueryRe = /^([^?]*)\?(.*)$/;
|
||||
|
||||
var expectedMatch = urlWithQueryRe.exec(expectedUrl);
|
||||
var actualMatch = urlWithQueryRe.exec(url);
|
||||
|
||||
return !!(expectedMatch && actualMatch) &&
|
||||
(expectedMatch[1] === actualMatch[1]) &&
|
||||
(normalizeQuery(expectedMatch[2]) === normalizeQuery(actualMatch[2]));
|
||||
}
|
||||
|
||||
function normalizeQuery(queryStr) {
|
||||
return queryStr.split('&').sort().join('&');
|
||||
}
|
||||
|
||||
function extractParamsFromPath(strippedUrl) {
|
||||
var keyObj = {};
|
||||
|
||||
if (!expectedUrl || !angular.isFunction(expectedUrl.test) ||
|
||||
!expectedKeys || !expectedKeys.length) return keyObj;
|
||||
|
||||
var match = expectedUrl.exec(strippedUrl);
|
||||
if (!match) return keyObj;
|
||||
|
||||
for (var i = 1, len = match.length; i < len; ++i) {
|
||||
var key = expectedKeys[i - 1];
|
||||
var val = match[i];
|
||||
if (key && val) {
|
||||
keyObj[key.name || key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return keyObj;
|
||||
}
|
||||
|
||||
function extractParamsFromQuery(queryStr) {
|
||||
var obj = {},
|
||||
keyValuePairs = queryStr.split('&').
|
||||
filter(angular.identity). // Ignore empty segments.
|
||||
map(function(keyValue) { return keyValue.replace(/\+/g, '%20').split('='); });
|
||||
|
||||
angular.forEach(keyValuePairs, function(pair) {
|
||||
var key = tryDecodeURIComponent(pair[0]);
|
||||
if (angular.isDefined(key)) {
|
||||
var val = angular.isDefined(pair[1]) ? tryDecodeURIComponent(pair[1]) : true;
|
||||
if (!hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = val;
|
||||
} else if (angular.isArray(obj[key])) {
|
||||
obj[key].push(val);
|
||||
} else {
|
||||
obj[key] = [obj[key], val];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function tryDecodeURIComponent(value) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (e) {
|
||||
// Ignore any invalid uri component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createMockXhr() {
|
||||
@@ -2206,39 +2305,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 +2606,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).
|
||||
|
||||
+8
-47
@@ -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,8 @@ function $RouteProvider() {
|
||||
}
|
||||
routes[path] = angular.extend(
|
||||
routeCopy,
|
||||
path && pathRegExp(path, routeCopy)
|
||||
{originalPath: path},
|
||||
path && routeToRegExp(path, routeCopy)
|
||||
);
|
||||
|
||||
// create redirection for trailing slashes
|
||||
@@ -234,8 +236,8 @@ function $RouteProvider() {
|
||||
: path + '/';
|
||||
|
||||
routes[redirectPath] = angular.extend(
|
||||
{redirectTo: path},
|
||||
pathRegExp(redirectPath, routeCopy)
|
||||
{originalPath: path, redirectTo: path},
|
||||
routeToRegExp(redirectPath, routeCopy)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,47 +255,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 +654,7 @@ function $RouteProvider() {
|
||||
|
||||
var nextRoutePromise = $q.resolve(nextRoute);
|
||||
|
||||
$browser.$$incOutstandingRequestCount();
|
||||
$browser.$$incOutstandingRequestCount('$route');
|
||||
|
||||
nextRoutePromise.
|
||||
then(getRedirectionData).
|
||||
@@ -721,7 +682,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 {string} path - The path to parse. (It is assumed to have query and hash stripped off.)
|
||||
* @param {Object} opts - Options.
|
||||
* @return {Object} - An object containing an array of path parameter names (`keys`) and a regular
|
||||
* expression (`regexp`) that can be used to identify a matching URL and extract the path
|
||||
* parameter values.
|
||||
*
|
||||
* @description
|
||||
* Parses the given path, extracting path parameter names and a regular expression to match URLs.
|
||||
*
|
||||
* Originally 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 {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,42 @@ describe('ngHref', function() {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
it('should bind numbers', inject(function($rootScope, $compile) {
|
||||
element = $compile('<a ng-href="{{1234}}"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('1234');
|
||||
}));
|
||||
|
||||
|
||||
it('should bind and sanitize the result of a (custom) toString() function', inject(function($rootScope, $compile) {
|
||||
$rootScope.value = {};
|
||||
element = $compile('<a ng-href="{{value}}"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('[object Object]');
|
||||
|
||||
function SafeClass() {}
|
||||
|
||||
SafeClass.prototype.toString = function() {
|
||||
return 'custom value';
|
||||
};
|
||||
|
||||
$rootScope.value = new SafeClass();
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('custom value');
|
||||
|
||||
function UnsafeClass() {}
|
||||
|
||||
UnsafeClass.prototype.toString = function() {
|
||||
return 'javascript:alert(1);';
|
||||
};
|
||||
|
||||
$rootScope.value = new UnsafeClass();
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('unsafe:javascript:alert(1);');
|
||||
}));
|
||||
|
||||
|
||||
if (isDefined(window.SVGElement)) {
|
||||
describe('SVGAElement', function() {
|
||||
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
|
||||
|
||||
@@ -1530,11 +1530,14 @@ describe('select', function() {
|
||||
['a'],
|
||||
NaN
|
||||
], function(prop) {
|
||||
|
||||
scope.option1 = prop;
|
||||
scope.option2 = 'red';
|
||||
scope.selected = 'NOMATCH';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option1">{{option1}}</option>' +
|
||||
'<option ng-value="option2">{{option2}}</option>' +
|
||||
'</select>');
|
||||
|
||||
scope.$digest();
|
||||
@@ -1571,10 +1574,12 @@ describe('select', function() {
|
||||
NaN
|
||||
], function(prop) {
|
||||
scope.option = prop;
|
||||
scope.option2 = 'red';
|
||||
scope.selected = 'NOMATCH';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option">{{option}}</option>' +
|
||||
'<option ng-value="option2">{{option2}}</option>' +
|
||||
'</select>');
|
||||
|
||||
var selectController = element.controller('select');
|
||||
@@ -1604,7 +1609,7 @@ describe('select', function() {
|
||||
|
||||
expect(scope.selected).toBe(null);
|
||||
expect(element[0].selectedIndex).toBe(0);
|
||||
expect(element.find('option').length).toBe(2);
|
||||
expect(element.find('option').length).toBe(3);
|
||||
expect(element.find('option').eq(0).prop('selected')).toBe(true);
|
||||
expect(element.find('option').eq(0).val()).toBe(unknownValue(prop));
|
||||
expect(element.find('option').eq(1).prop('selected')).toBe(false);
|
||||
@@ -1617,6 +1622,7 @@ describe('select', function() {
|
||||
expect(element.find('option').eq(0).val()).toBe('string:UPDATEDVALUE');
|
||||
});
|
||||
|
||||
|
||||
it('should interact with custom attribute $observe and $set calls', function() {
|
||||
var log = [], optionAttr;
|
||||
|
||||
@@ -1638,26 +1644,43 @@ describe('select', function() {
|
||||
optionAttr.$set('value', 'update');
|
||||
expect(log[1]).toBe('update');
|
||||
expect(element.find('option').eq(1).val()).toBe('string:update');
|
||||
|
||||
});
|
||||
|
||||
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.value = 'def';
|
||||
scope.textvalue = 'ghi';
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.value = 'def';
|
||||
scope.textvalue = 'ghi';
|
||||
|
||||
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.textvalue = 'def';
|
||||
scope.textvalue2 = 'ghi';
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
|
||||
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.textvalue = 'def';
|
||||
scope.textvalue2 = 'ghi';
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
|
||||
it('should select the first option if it is `undefined`', function() {
|
||||
scope.selected = undefined;
|
||||
|
||||
scope.option1 = undefined;
|
||||
scope.option2 = 'red';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option1">{{option1}}</option>' +
|
||||
'<option ng-value="option2">{{option2}}</option>' +
|
||||
'</select>');
|
||||
|
||||
expect(element).toEqualSelect(['undefined:undefined'], 'string:red');
|
||||
});
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
describe('and select[multiple]', function() {
|
||||
|
||||
|
||||
+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;
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('ngAnimate integration tests', function() {
|
||||
ss.destroy();
|
||||
});
|
||||
|
||||
|
||||
it('should cancel a running and started removeClass animation when a follow-up addClass animation adds the same class',
|
||||
inject(function($animate, $rootScope, $$rAF, $document, $rootElement) {
|
||||
|
||||
@@ -316,6 +317,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}">' +
|
||||
@@ -360,6 +363,7 @@ describe('ngAnimate integration tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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) {
|
||||
@@ -396,6 +400,155 @@ 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 remove the prepare classes when different structural animations happen in the same digest', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
|
||||
element = jqLite(
|
||||
// Class animation on parent element is neeeded so the child elements get the prepare class
|
||||
'<div id="outer" ng-class="{blue: cond}" ng-switch="cond">' +
|
||||
'<div id="default" ng-switch-default></div>' +
|
||||
'<div id="truthy" ng-switch-when="true"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
$rootElement.append(element);
|
||||
jqLite($document[0].body).append($rootElement);
|
||||
|
||||
$compile(element)($rootScope);
|
||||
$rootScope.cond = false;
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.cond = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var parent = element;
|
||||
var truthySwitch = jqLite(parent[0].querySelector('#truthy'));
|
||||
var defaultSwitch = jqLite(parent[0].querySelector('#default'));
|
||||
|
||||
expect(parent).not.toHaveClass('blue');
|
||||
expect(parent).toHaveClass('blue-add');
|
||||
expect(truthySwitch).toHaveClass('ng-enter-prepare');
|
||||
expect(defaultSwitch).toHaveClass('ng-leave-prepare');
|
||||
|
||||
$animate.flush();
|
||||
|
||||
expect(parent).toHaveClass('blue');
|
||||
expect(parent).not.toHaveClass('blue-add');
|
||||
expect(truthySwitch).not.toHaveClass('ng-enter-prepare');
|
||||
expect(defaultSwitch).not.toHaveClass('ng-leave-prepare');
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect the element node for caching when animations with the same type happen in the same digest', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
|
||||
ss.addRule('.animate.ng-enter', 'transition:2s linear all;');
|
||||
|
||||
element = jqLite(
|
||||
'<div>' +
|
||||
'<div>' +
|
||||
'<div id="noanimate" ng-if="cond"></div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<div id="animate" class="animate" ng-if="cond"></div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
$rootElement.append(element);
|
||||
jqLite($document[0].body).append($rootElement);
|
||||
|
||||
$compile(element)($rootScope);
|
||||
$rootScope.cond = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var parent = element;
|
||||
var noanimate = jqLite(parent[0].querySelector('#noanimate'));
|
||||
var animate = jqLite(parent[0].querySelector('#animate'));
|
||||
|
||||
expect(noanimate).not.toHaveClass('ng-enter');
|
||||
expect(animate).toHaveClass('ng-enter');
|
||||
|
||||
$animate.closeAndFlush();
|
||||
|
||||
expect(noanimate).not.toHaveClass('ng-enter');
|
||||
expect(animate).not.toHaveClass('ng-enter');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pack level elements into their own RAF flush', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
|
||||
@@ -544,6 +697,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() {
|
||||
|
||||
+93
-79
@@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* globals nodeBlackList false */
|
||||
|
||||
describe('$aria', function() {
|
||||
var scope, $compile, element;
|
||||
|
||||
@@ -922,115 +924,127 @@ 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() {};
|
||||
they('should not prevent default keyboard action if an interactive $type element' +
|
||||
'is nested inside ng-click', nodeBlackList, function(elementType) {
|
||||
function createHTML(type) {
|
||||
return '<' + type + '></' + type + '>';
|
||||
}
|
||||
|
||||
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);
|
||||
compileElement(
|
||||
'<section>' +
|
||||
'<div ng-click="onClick($event)">' + createHTML(elementType) + '</div>' +
|
||||
'</section>');
|
||||
|
||||
scope.$digest();
|
||||
var divElement = element.find('div');
|
||||
var interactiveElement = element.find(elementType);
|
||||
|
||||
clickFn = spyOn(scope, 'someAction');
|
||||
// Use browserTrigger because it supports event bubbling
|
||||
// 13 Enter
|
||||
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 13});
|
||||
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
|
||||
|
||||
var divElement = elements.find('div');
|
||||
var liElement = elements.find('li');
|
||||
clickEvents = [];
|
||||
|
||||
divElement.triggerHandler({type: 'keydown', which: 32});
|
||||
liElement.triggerHandler({type: 'keydown', which: 32});
|
||||
// 32 Space
|
||||
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 32});
|
||||
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
|
||||
}
|
||||
);
|
||||
|
||||
expect(clickFn).toHaveBeenCalledWith('div');
|
||||
expect(clickFn).toHaveBeenCalledWith('li');
|
||||
});
|
||||
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>');
|
||||
|
||||
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 divElement = element.find('div');
|
||||
var liElement = element.find('li');
|
||||
|
||||
var tmpl = '<div ng-click="onClick()" ng-keydown="onKeydown()" tabindex="0"></div>';
|
||||
compileElement(tmpl);
|
||||
divElement.triggerHandler({type: 'keydown', which: 13});
|
||||
liElement.triggerHandler({type: 'keydown', which: 13});
|
||||
divElement.triggerHandler({type: 'keydown', which: 32});
|
||||
liElement.triggerHandler({type: 'keydown', which: 32});
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
|
||||
}
|
||||
);
|
||||
|
||||
expect(scope.onKeydown).toHaveBeenCalled();
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
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>');
|
||||
|
||||
it('should not bind to key events if there is existing ng-keypress', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
scope.onKeypress = jasmine.createSpy('onKeypress');
|
||||
element.triggerHandler({type: eventName, keyCode: 13});
|
||||
element.triggerHandler({type: eventName, keyCode: 32});
|
||||
|
||||
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
+404
-53
@@ -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,
|
||||
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');
|
||||
they('should ignore query params when matching in ' + routeShortcut + ' $prop method', methods,
|
||||
function(method) {
|
||||
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](method, 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(method, 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() {
|
||||
|
||||
@@ -77,5 +77,45 @@ describe('$routeParams', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly extract path params containing hashes and/or question marks', function() {
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.when('/foo/:bar', {});
|
||||
$routeProvider.when('/zoo/:bar/:baz/:qux', {});
|
||||
});
|
||||
|
||||
inject(function($location, $rootScope, $routeParams) {
|
||||
$location.path('/foo/bar?baz');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({bar: 'bar?baz'});
|
||||
|
||||
$location.path('/foo/bar?baz=val');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({bar: 'bar?baz=val'});
|
||||
|
||||
$location.path('/foo/bar#baz');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({bar: 'bar#baz'});
|
||||
|
||||
$location.path('/foo/bar?baz#qux');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({bar: 'bar?baz#qux'});
|
||||
|
||||
$location.path('/foo/bar?baz=val#qux');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({bar: 'bar?baz=val#qux'});
|
||||
|
||||
$location.path('/foo/bar#baz?qux');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({bar: 'bar#baz?qux'});
|
||||
|
||||
$location.path('/zoo/bar?p1=v1#h1/baz?p2=v2#h2/qux?p3=v3#h3');
|
||||
$rootScope.$digest();
|
||||
expect($routeParams).toEqual({
|
||||
bar: 'bar?p1=v1#h1',
|
||||
baz: 'baz?p2=v2#h2',
|
||||
qux: 'qux?p3=v3#h3'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+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