Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75e876424d | |||
| 19fab4a1d7 | |||
| d1293540e1 | |||
| 22f66025db | |||
| 6f8ddb6d43 | |||
| 34590e15d4 | |||
| 83098b9add | |||
| 5d8861fb2f | |||
| b9f7c453e0 | |||
| 750344129e | |||
| 74da034077 | |||
| 91ef94d284 | |||
| ab9b021572 | |||
| b268c0b7b4 | |||
| b0c19f8b06 | |||
| bbc2a0ae48 | |||
| ca53dfcc18 | |||
| ce6a96b0d7 | |||
| d4b359f4b2 | |||
| 8d841c3405 | |||
| 2b285c75f4 | |||
| 6e4464331d | |||
| 2f8db1bf01 | |||
| 838cf4be3c | |||
| de2a56bbc8 | |||
| 55ad192e4a | |||
| 5b4713e43e | |||
| 3fa9aba0cc | |||
| 1bba358a75 | |||
| 7a4124c298 | |||
| 2512a81e09 | |||
| 44c9d1616a | |||
| 5758d73964 | |||
| 6bd6dbff49 | |||
| 7170f9d9ca | |||
| 1c0f721368 | |||
| 2a5a52a76c | |||
| c690946469 | |||
| 87b0055c80 | |||
| 2116857a2a | |||
| 0f58334b7b | |||
| bcc257b459 | |||
| 980fb395e4 | |||
| 62ed26a84f | |||
| 59f1f4e19a | |||
| cb51116dbd | |||
| c1f34e8eeb | |||
| 7bf5429e3b | |||
| d3da55c40f | |||
| 70edec947c | |||
| fe17c0e066 | |||
| e403682444 | |||
| 27d441b0d6 | |||
| 8a944b0872 | |||
| 786a1a4429 | |||
| 8e5c4e92f7 | |||
| 46d24ae4c8 | |||
| 9dd33c09b1 | |||
| fea8240c81 | |||
| 3d2b1be211 | |||
| f08a0c5ad1 | |||
| 6f1e0ba563 | |||
| 540338f9a5 | |||
| 0e6a700807 | |||
| 4fc40bc932 | |||
| 216724b4cb | |||
| 9bd1645970 | |||
| 3397a031a1 | |||
| 5ec5aa7751 | |||
| bf5ac5261d | |||
| 91b7cd9b74 | |||
| 7b2ecf42c6 | |||
| 256d9a948c | |||
| 99fc6cda98 | |||
| 690b69b9cd | |||
| 4262f15e16 | |||
| 3a8d1354ce | |||
| f9387c6890 | |||
| 48d0ffcbc4 | |||
| 3485ba1e2b |
+120
-4
@@ -1,3 +1,97 @@
|
||||
<a name="1.4.8"></a>
|
||||
# 1.4.8 ice-manipulation (2015-11-19)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **$animate:** ensure leave animation calls `close` callback
|
||||
([6bd6dbff](https://github.com/angular/angular.js/commit/6bd6dbff4961a601c03e9465442788781d329ba6),
|
||||
[#12278](https://github.com/angular/angular.js/issues/12278), [#12096](https://github.com/angular/angular.js/issues/12096), [#13054](https://github.com/angular/angular.js/issues/13054))
|
||||
- **$cacheFactory:** check key exists before decreasing cache size count
|
||||
([2a5a52a7](https://github.com/angular/angular.js/commit/2a5a52a76ccf60c6e8c5d881e90e11a2666a6d3c),
|
||||
[#12321](https://github.com/angular/angular.js/issues/12321), [#12329](https://github.com/angular/angular.js/issues/12329))
|
||||
- **$compile:**
|
||||
- bind all directive controllers correctly when using `bindToController`
|
||||
([5d8861fb](https://github.com/angular/angular.js/commit/5d8861fb2f203e8a688b6044cbd1140cd79fd049),
|
||||
[#11343](https://github.com/angular/angular.js/issues/11343), [#11345](https://github.com/angular/angular.js/issues/11345))
|
||||
- evaluate against the correct scope with bindToController on new scope
|
||||
([b9f7c453](https://github.com/angular/angular.js/commit/b9f7c453e00d6938106f414952f74d5e5fdcb993),
|
||||
[#13021](https://github.com/angular/angular.js/issues/13021), [#13025](https://github.com/angular/angular.js/issues/13025))
|
||||
- fix scoping of transclusion directives inside replace directive
|
||||
([74da0340](https://github.com/angular/angular.js/commit/74da03407782d679951cd8f693860cea214f2580),
|
||||
[#12975](https://github.com/angular/angular.js/issues/12975), [#12936](https://github.com/angular/angular.js/issues/12936), [#13244](https://github.com/angular/angular.js/issues/13244))
|
||||
- **$http:** apply `transformResponse` even when `data` is empty
|
||||
([c6909464](https://github.com/angular/angular.js/commit/c690946469e09cfe6b774e63dbe14ace92ce6cb7),
|
||||
[#12976](https://github.com/angular/angular.js/issues/12976), [#12979](https://github.com/angular/angular.js/issues/12979))
|
||||
- **$location:** ensure `$locationChangeSuccess` fires even if URL ends with `#`
|
||||
([6f8ddb6d](https://github.com/angular/angular.js/commit/6f8ddb6d4329441e8d4a856978413aa9b9bd918f),
|
||||
[#12175](https://github.com/angular/angular.js/issues/12175), [#13251](https://github.com/angular/angular.js/issues/13251))
|
||||
- **$parse:** evaluate once simple expressions only once
|
||||
([e4036824](https://github.com/angular/angular.js/commit/e403682444fa08af4f3491badf2f3a10d7595699),
|
||||
[#12983](https://github.com/angular/angular.js/issues/12983), [#13002](https://github.com/angular/angular.js/issues/13002))
|
||||
- **$resource:** allow XHR request to be cancelled via a timeout promise
|
||||
([7170f9d9](https://github.com/angular/angular.js/commit/7170f9d9ca765c578f8d3eb4699860a9330a0a11),
|
||||
[#12657](https://github.com/angular/angular.js/issues/12657), [#12675](https://github.com/angular/angular.js/issues/12675), [#10890](https://github.com/angular/angular.js/issues/10890), [#9332](https://github.com/angular/angular.js/issues/9332))
|
||||
- **$rootScope:** prevent IE9 memory leak when destroying scopes
|
||||
([87b0055c](https://github.com/angular/angular.js/commit/87b0055c80f40589c5bcf3765e59e872bcfae119),
|
||||
[#10706](https://github.com/angular/angular.js/issues/10706), [#11786](https://github.com/angular/angular.js/issues/11786))
|
||||
- **Angular.js:** fix `isArrayLike` for unusual cases
|
||||
([70edec94](https://github.com/angular/angular.js/commit/70edec947c7b189694ae66b129568182e3369cab),
|
||||
[#10186](https://github.com/angular/angular.js/issues/10186), [#8000](https://github.com/angular/angular.js/issues/8000), [#4855](https://github.com/angular/angular.js/issues/4855), [#4751](https://github.com/angular/angular.js/issues/4751), [#10272](https://github.com/angular/angular.js/issues/10272))
|
||||
- **isArrayLike:** handle jQuery objects of length 0
|
||||
([d3da55c4](https://github.com/angular/angular.js/commit/d3da55c40f1e1ddceced5da51e364888ff9d82ff))
|
||||
- **jqLite:**
|
||||
- deregister special `mouseenter` / `mouseleave` events correctly
|
||||
([22f66025](https://github.com/angular/angular.js/commit/22f66025db262417ebb78c1ce1f4d7058dca3fd3),
|
||||
[#12795](https://github.com/angular/angular.js/issues/12795), [#12799](https://github.com/angular/angular.js/issues/12799))
|
||||
- ensure mouseenter works with svg elements on IE
|
||||
([c1f34e8e](https://github.com/angular/angular.js/commit/c1f34e8eeb5105767f6cbf4727b8c5664be2a261),
|
||||
[#10259](https://github.com/angular/angular.js/issues/10259), [#10276](https://github.com/angular/angular.js/issues/10276))
|
||||
- **limitTo:** start at 0 if `begin` is negative and exceeds input length
|
||||
([4fc40bc9](https://github.com/angular/angular.js/commit/4fc40bc9320a1d5902e648b70fa79c7cf7e794c7),
|
||||
[#12775](https://github.com/angular/angular.js/issues/12775), [#12781](https://github.com/angular/angular.js/issues/12781))
|
||||
- **merge:**
|
||||
- ensure that jqlite->jqlite and DOM->DOM
|
||||
([2f8db1bf](https://github.com/angular/angular.js/commit/2f8db1bf01173b546a2868fc7b8b188c2383fbff))
|
||||
- clone elements instead of treating them like simple objects
|
||||
([838cf4be](https://github.com/angular/angular.js/commit/838cf4be3c671903796dbb69d95c0e5ac1516a06),
|
||||
[#12286](https://github.com/angular/angular.js/issues/12286))
|
||||
- **ngAria:** don't add tabindex to radio and checkbox inputs
|
||||
([59f1f4e1](https://github.com/angular/angular.js/commit/59f1f4e19a02e6e6f4c41c15b0e9f3372d85cecc),
|
||||
[#12492](https://github.com/angular/angular.js/issues/12492), [#13095](https://github.com/angular/angular.js/issues/13095))
|
||||
- **ngInput:** change URL_REGEXP to better match RFC3987
|
||||
([cb51116d](https://github.com/angular/angular.js/commit/cb51116dbd225ccfdbc9a565a66a170e65d26331),
|
||||
[#11341](https://github.com/angular/angular.js/issues/11341), [#11381](https://github.com/angular/angular.js/issues/11381))
|
||||
- **ngMock:** reset cache before every test
|
||||
([91b7cd9b](https://github.com/angular/angular.js/commit/91b7cd9b74d72a48d844c5c3e0e9dee03405e0ca),
|
||||
[#13013](https://github.com/angular/angular.js/issues/13013))
|
||||
- **ngOptions:**
|
||||
- skip comments and empty options when looking for options
|
||||
([0f58334b](https://github.com/angular/angular.js/commit/0f58334b7b9a9d3d6ff34e9754961b6f67731fae),
|
||||
[#12190](https://github.com/angular/angular.js/issues/12190), [#13029](https://github.com/angular/angular.js/issues/13029), [#13033](https://github.com/angular/angular.js/issues/13033))
|
||||
- override select option registration to allow compilation of empty option
|
||||
([7b2ecf42](https://github.com/angular/angular.js/commit/7b2ecf42c697eb8d51a0f2d73b324bd900139e05),
|
||||
[#11685](https://github.com/angular/angular.js/issues/11685), [#12972](https://github.com/angular/angular.js/issues/12972), [#12968](https://github.com/angular/angular.js/issues/12968), [#13012](https://github.com/angular/angular.js/issues/13012))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- **$compile:** use static jquery data method to avoid creating new instances
|
||||
([55ad192e](https://github.com/angular/angular.js/commit/55ad192e4ab79295ab15ecaaf8f6b9e7932a0336))
|
||||
- **copy:**
|
||||
- avoid regex in `isTypedArray`
|
||||
([19fab4a1](https://github.com/angular/angular.js/commit/19fab4a1d79d2445795273f1622344353cf4d104))
|
||||
- only validate/clear if the user specifies a destination
|
||||
([d1293540](https://github.com/angular/angular.js/commit/d1293540e13573eb9ea5f90730bb9c9710c345db),
|
||||
[#12068](https://github.com/angular/angular.js/issues/12068))
|
||||
- **merge:** remove unnecessary wrapping of jqLite element
|
||||
([ce6a96b0](https://github.com/angular/angular.js/commit/ce6a96b0d76dd2e5ab2247ca3059d284575bc6f0),
|
||||
[#13236](https://github.com/angular/angular.js/issues/13236))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.4.7"></a>
|
||||
# 1.4.7 dark-luminescence (2015-09-29)
|
||||
|
||||
@@ -29,9 +123,6 @@
|
||||
([7295c60f](https://github.com/angular/angular.js/commit/7295c60ffb9f2e4f32043c538ace740b187f565a),
|
||||
[#12856](https://github.com/angular/angular.js/issues/12856), [#12903](https://github.com/angular/angular.js/issues/12903))
|
||||
- **ngOptions:**
|
||||
- skip comments when looking for option elements
|
||||
([68d4dc5b](https://github.com/angular/angular.js/commit/68d4dc5b71b23e4c7c2650e6da3d7200de99f1ae),
|
||||
[#12190](https://github.com/angular/angular.js/issues/12190))
|
||||
- prevent frozen select ui in IE
|
||||
([dbc69851](https://github.com/angular/angular.js/commit/dbc698517ff620b3a6279f65d4a9b6e3c15087b9),
|
||||
[#11314](https://github.com/angular/angular.js/issues/11314), [#11795](https://github.com/angular/angular.js/issues/11795))
|
||||
@@ -1392,7 +1483,6 @@ mechanism.
|
||||
|
||||
- **ngMessages:** due to [c9a4421f](https://github.com/angular/angular.js/commit/c9a4421fc3c97448527eadef1f42eb2f487ec2e0),
|
||||
|
||||
|
||||
The `ngMessagesInclude` attribute is now its own directive and that must
|
||||
be placed as a **child** element within the element with the ngMessages
|
||||
directive. (Keep in mind that the former behaviour of the
|
||||
@@ -1415,6 +1505,26 @@ end of the container containing the ngMessages directive).
|
||||
</div>
|
||||
```
|
||||
|
||||
- **ngMessages:** due to [c9a4421f](https://github.com/angular/angular.js/commit/c9a4421fc3c97448527eadef1f42eb2f487ec2e0),
|
||||
|
||||
it is no longer possible to use interpolation inside the `ngMessages` attribute expression. This technique
|
||||
is generally not recommended, and can easily break when a directive implementation changes. In cases
|
||||
where a simple expression is not possible, you can delegate accessing the object to a function:
|
||||
|
||||
```html
|
||||
<div ng-messages="ctrl.form['field_{{$index}}'].$error">...</div>
|
||||
```
|
||||
would become
|
||||
```html
|
||||
<div ng-messages="ctrl.getMessages($index)">...</div>
|
||||
```
|
||||
where `ctrl.getMessages()`
|
||||
```javascript
|
||||
ctrl.getMessages = function($index) {
|
||||
return ctrl.form['field_' + $index].$error;
|
||||
}
|
||||
```
|
||||
|
||||
- **$http:** due to [5da1256](https://github.com/angular/angular.js/commit/5da1256fc2812d5b28fb0af0de81256054856369),
|
||||
|
||||
`transformRequest` functions can no longer modify request headers.
|
||||
@@ -1870,6 +1980,12 @@ But in practice this is not what people want and so this change iterates over pr
|
||||
in the order they are returned by Object.keys(obj), which is almost always the order
|
||||
in which the properties were defined.
|
||||
|
||||
- **ngOptions:** due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
|
||||
|
||||
setting the ngOptions attribute expression after the element is compiled, will no longer trigger the ngOptions behavior.
|
||||
This worked previously because the ngOptions logic was part of the select directive, while
|
||||
it is now implemented in the ngOptions directive itself.
|
||||
|
||||
- **select:** due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
|
||||
|
||||
the `select` directive will now use strict comparison of the `ngModel` scope value against `option`
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ chances of your issue being dealt with quickly:
|
||||
* **Angular Version(s)** - is it a regression?
|
||||
* **Browsers and Operating System** - is this a problem with all browsers or only IE8?
|
||||
* **Reproduce the Error** - provide a live example (using [Plunker][plunker] or
|
||||
[JSFiddle][jsfiddle]) or a unambiguous set of steps.
|
||||
[JSFiddle][jsfiddle]) or an unambiguous set of steps.
|
||||
* **Related Issues** - has a similar issue been reported before?
|
||||
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
|
||||
causing the problem (line of code or commit)
|
||||
|
||||
@@ -21,7 +21,7 @@ piece of cake. Best of all?? It makes development fun!
|
||||
|
||||
Building AngularJS
|
||||
---------
|
||||
[Once you have your environment setup](http://docs.angularjs.org/misc/contribute) just run:
|
||||
[Once you have your environment set up](http://docs.angularjs.org/misc/contribute) just run:
|
||||
|
||||
grunt package
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ but the required directive controller is not present on the current DOM element
|
||||
|
||||
To resolve this error ensure that there is no typo in the required controller name and that the required directive controller is present on the current element.
|
||||
|
||||
If the required controller is expected to be on a ancestor element, make sure that you prefix the controller name in the `require` definition with `^`.
|
||||
If the required controller is expected to be on an ancestor element, make sure that you prefix the controller name in the `require` definition with `^`.
|
||||
|
||||
If the required controller is optionally requested, use `?` or `^?` to specify that.
|
||||
|
||||
|
||||
@@ -356,15 +356,15 @@ legacy browsers and hashbang links in modern browser:
|
||||
|
||||
### Example
|
||||
|
||||
Here you can see two `$location` instances, both in **Html5 mode**, but on different browsers, so
|
||||
that you can see the differences. These `$location` services are connected to a fake browsers. Each
|
||||
input represents the address bar of the browser.
|
||||
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
|
||||
a fakeBrowser service, which you don't have to set up in actual projects.
|
||||
|
||||
Note that when you type hashbang url into first browser (or vice versa) it doesn't rewrite /
|
||||
Note that when you type hashbang url into the first browser (or vice versa) it doesn't rewrite /
|
||||
redirect to regular / hashbang url, as this conversion happens only during parsing the initial URL
|
||||
= on page reload.
|
||||
|
||||
In these examples we use `<base href="/base/index.html" />`
|
||||
In these examples we use `<base href="/base/index.html" />`. The inputs represent the address bar of the browser.
|
||||
|
||||
#### Browser in HTML5 mode
|
||||
<example module="html5-mode" name="location-html5-mode">
|
||||
@@ -389,6 +389,7 @@ In these examples we use `<base href="/base/index.html" />`
|
||||
<file name="app.js">
|
||||
angular.module('html5-mode', ['fake-browser', 'address-bar'])
|
||||
|
||||
// Configure the fakeBrowser. Do not set these values in actual projects.
|
||||
.constant('initUrl', 'http://www.example.com/base/path?a=b#h')
|
||||
.constant('baseHref', '/base/index.html')
|
||||
.value('$sniffer', { history: true })
|
||||
@@ -538,6 +539,7 @@ In these examples we use `<base href="/base/index.html" />`
|
||||
<file name="app.js">
|
||||
angular.module('hashbang-mode', ['fake-browser', 'address-bar'])
|
||||
|
||||
// Configure the fakeBrowser. Do not set these values in actual projects.
|
||||
.constant('initUrl', 'http://www.example.com/base/index.html#!/path?a=b#h')
|
||||
.constant('baseHref', '/base/index.html')
|
||||
.value('$sniffer', { history: false })
|
||||
|
||||
@@ -348,8 +348,7 @@ The following example shows how this is done with Angular:
|
||||
|
||||
return {
|
||||
currencies: currencies,
|
||||
convert: convert,
|
||||
refresh: refresh
|
||||
convert: convert
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
|
||||
@@ -587,14 +587,24 @@ want to reuse throughout your app.
|
||||
In this example we will build a directive that displays the current time.
|
||||
Once a second, it updates the DOM to reflect the current time.
|
||||
|
||||
Directives that want to modify the DOM typically use the `link` option.
|
||||
`link` takes a function with the following signature, `function link(scope, element, attrs) { ... }`
|
||||
where:
|
||||
Directives that want to modify the DOM typically use the `link` option to register DOM listeners
|
||||
as well as update the DOM. It is executed after the template has been cloned and is where
|
||||
directive logic will be put.
|
||||
|
||||
`link` takes a function with the following signature,
|
||||
`function link(scope, element, attrs, controller, transcludeFn) { ... }`, where:
|
||||
|
||||
* `scope` is an Angular scope object.
|
||||
* `element` is the jqLite-wrapped element that this directive matches.
|
||||
* `attrs` is a hash object with key-value pairs of normalized attribute names and their
|
||||
corresponding attribute values.
|
||||
* `controller` is the directive's required controller instance(s) or its own controller (if any).
|
||||
The exact value depends on the directive's require property.
|
||||
* `transcludeFn` is a transclude linking function pre-bound to the correct transclusion scope.
|
||||
|
||||
<div class="alert alert-info">
|
||||
For more details on the `link` option refer to the {@link ng.$compile#-link- `$compile` API} page.
|
||||
</div>
|
||||
|
||||
In our `link` function, we want to update the displayed time once a second, or whenever a user
|
||||
changes the time formatting string that our directive binds to. We will use the `$interval` service
|
||||
|
||||
@@ -383,7 +383,7 @@ In the following example we create two directives:
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attrs, ctrl) {
|
||||
var usernames = ['Jim', 'John', 'Jill', 'Jackie'];
|
||||
var usernames = ['Jim', 'John', 'Jill', 'Jackie'];
|
||||
|
||||
ctrl.$asyncValidators.username = function(modelValue, viewValue) {
|
||||
|
||||
|
||||
@@ -170,6 +170,25 @@ other inline messages situated as children within the `ngMessages` container dir
|
||||
Depending on where the `ngMessagesInclude` directive is placed it will be prioritized inline with the other messages
|
||||
before and after it.
|
||||
|
||||
Also due to [c9a4421f](https://github.com/angular/angular.js/commit/c9a4421fc3c97448527eadef1f42eb2f487ec2e0),
|
||||
it is no longer possible to use interpolation inside the `ngMessages` attribute expression. This technique
|
||||
is generally not recommended, and can easily break when a directive implementation changes. In cases
|
||||
where a simple expression is not possible, you can delegate accessing the object to a function:
|
||||
|
||||
```html
|
||||
<div ng-messages="ctrl.form['field_{{$index}}'].$error">...</div>
|
||||
```
|
||||
would become
|
||||
```html
|
||||
<div ng-messages="ctrl.getMessages($index)">...</div>
|
||||
```
|
||||
where `ctrl.getMessages()`
|
||||
```javascript
|
||||
ctrl.getMessages = function($index) {
|
||||
return ctrl.form['field_' + $index].$error;
|
||||
}
|
||||
```
|
||||
|
||||
### ngOptions
|
||||
|
||||
The `ngOptions` directive has also been refactored and as a result some long-standing bugs
|
||||
@@ -189,6 +208,10 @@ But in practice this is not what people want and so this change iterates over pr
|
||||
in the order they are returned by Object.keys(obj), which is almost always the order
|
||||
in which the properties were defined.
|
||||
|
||||
Also due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
|
||||
setting the ngOptions attribute expression after the element is compiled, will no longer trigger the ngOptions behavior.
|
||||
This worked previously because the ngOptions logic was part of the select directive, while
|
||||
it is now implemented in the ngOptions directive itself.
|
||||
|
||||
### select
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ and {@link angular.reloadWithDebugInfo `angular.reloadWithDebugInfo`}.
|
||||
|
||||
## Strict DI Mode
|
||||
|
||||
Using strict di mode in your production application will throw errors when a injectable
|
||||
Using strict di mode in your production application will throw errors when an injectable
|
||||
function is not
|
||||
{@link di#dependency-annotation annotated explicitly}. Strict di mode is intended to help
|
||||
you make sure that your code will work when minified. However, it also will force you to
|
||||
|
||||
@@ -391,7 +391,7 @@ implementing custom event callbacks, or when working with third-party library ca
|
||||
5. The {@link ng.$rootScope.Scope#$watch $watch} list is a set of expressions
|
||||
which may have changed since last iteration. If a change is detected then the `$watch`
|
||||
function is called which typically updates the DOM with the new value.
|
||||
6. Once the Angular {@link ng.$rootScope.Scope#$digest $digest} loop finishes
|
||||
6. Once the Angular {@link ng.$rootScope.Scope#$digest $digest} loop finishes,
|
||||
the execution leaves the Angular and JavaScript context. This is followed by the browser
|
||||
re-rendering the DOM to reflect any changes.
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
<ul doc-tutorial-nav="0"></ul>
|
||||
|
||||
|
||||
You are now ready to build the AngularJS phonecat app. In this step, you will become familiar
|
||||
with the most important source code files, learn how to start the development servers bundled with
|
||||
In this step of the tutorial, you will become familiar with the most important source code files of
|
||||
the AngularJS phonecat app. You will also learn how to start the development servers bundled with
|
||||
angular-seed, and run the application in the browser.
|
||||
|
||||
Before you continue, make sure you have set up your development environment and installed all necessary
|
||||
dependencies, as described in {@link tutorial/index#get-started Get Started}.
|
||||
|
||||
In `angular-phonecat` directory, run this command:
|
||||
In the `angular-phonecat` directory, run this command:
|
||||
|
||||
```
|
||||
git checkout -f step-0
|
||||
|
||||
@@ -195,8 +195,19 @@ to ensure that Karma and its necessary plugins are installed. You can do this by
|
||||
|
||||
To run the tests, and then watch the files for changes: `npm test`.
|
||||
|
||||
* Karma will start a new instance of Chrome browser automatically. Just ignore it and let it run in
|
||||
the background. Karma will use this browser for test execution.
|
||||
* Karma will start new instances of Chrome and Firefox browsers automatically. Just ignore them and
|
||||
let them run in the background. Karma will use these browsers for test execution.
|
||||
* If you only have one of the browsers installed on your machine (either Chrome or Firefox), make
|
||||
sure to update the karma configuration file before running the test. Locate the configuration file
|
||||
in `test/karma.conf.js`, then update the `browsers` property.
|
||||
|
||||
E.g. if you only have Chrome installed:
|
||||
<pre>
|
||||
...
|
||||
browsers: ['Chrome'],
|
||||
...
|
||||
</pre>
|
||||
|
||||
* You should see the following or similar output in the terminal:
|
||||
|
||||
<pre>
|
||||
@@ -250,7 +261,7 @@ browser is limited, which results in your karma tests running extremely slow.
|
||||
<tr><th>row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
|
||||
</table>
|
||||
|
||||
|
||||
Extra points: try and make an 8x8 table using an additional `ng-repeat`.
|
||||
|
||||
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`.
|
||||
|
||||
@@ -37,12 +37,12 @@ We are using [Bower][bower] to install client-side dependencies. This step upda
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "~1.4.0"
|
||||
"angular-route": "1.4.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The new dependency `"angular-route": "~1.4.0"` tells bower to install a version of the
|
||||
The new dependency `"angular-route": "1.4.x"` tells bower to install a version of the
|
||||
angular-route component that is compatible with version 1.4.x. We must tell bower to download
|
||||
and install this dependency.
|
||||
|
||||
|
||||
@@ -32,17 +32,18 @@ We are using [Bower][bower] to install client side dependencies. This step upda
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "~1.3.0",
|
||||
"angular-mocks": "~1.3.0",
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "~1.3.0",
|
||||
"angular-resource": "~1.3.0"
|
||||
"angular-route": "1.4.x",
|
||||
"angular-resource": "1.4.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The new dependency `"angular-resource": "~1.3.0"` tells bower to install a version of the
|
||||
angular-resource component that is compatible with version 1.3.x. We must ask bower to download
|
||||
The new dependency `"angular-resource": "1.4.x"` tells bower to install a version of the
|
||||
angular-resource component that is compatible with version 1.4.x. We must ask bower to download
|
||||
and install this dependency. We can do this by running:
|
||||
|
||||
```
|
||||
|
||||
@@ -36,20 +36,20 @@ We are using [Bower][bower] to install client side dependencies. This step upda
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "~1.3.0",
|
||||
"angular-mocks": "~1.3.0",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "~1.3.0",
|
||||
"angular-resource": "~1.3.0",
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"angular-animate": "~1.3.0"
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "1.4.x",
|
||||
"angular-resource": "1.4.x",
|
||||
"angular-animate": "1.4.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `"angular-animate": "~1.3.0"` tells bower to install a version of the
|
||||
angular-animate component that is compatible with version 1.3.x.
|
||||
* `"jquery": "2.1.1"` tells bower to install the 2.1.1 version of jQuery. Note that this is not an
|
||||
* `"angular-animate": "1.4.x"` tells bower to install a version of the
|
||||
angular-animate component that is compatible with version 1.4.x.
|
||||
* `"jquery": "~2.1.1"` tells bower to install the 2.1.1 version of jQuery. Note that this is not an
|
||||
Angular library, it is the standard jQuery library. We can use bower to install a wide range of 3rd
|
||||
party libraries.
|
||||
|
||||
@@ -111,7 +111,7 @@ __`app/index.html`.__
|
||||
```
|
||||
|
||||
<div class="alert alert-error">
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.3; jQuery 1.x is
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.4; jQuery 1.x is
|
||||
not officially supported.
|
||||
Be sure to load jQuery before all AngularJS scripts, otherwise AngularJS won't detect jQuery and
|
||||
animations will not work as expected.
|
||||
@@ -239,9 +239,9 @@ The name of the starting class is the name of the event that is fired (like `ent
|
||||
The active class name is the same as the starting class's but with an `-active` suffix.
|
||||
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
|
||||
|
||||
In our example above, elements expand from a height of **0** to **120 pixels** when items are added or moved,
|
||||
around and collapsing the items before removing them from the list.
|
||||
There's also a nice fade-in and fade-out effect that also occurs at the same time. All of this is handled
|
||||
In our example above, elements are expanded from a height of **0** to **120 pixels** when they're added to the
|
||||
list and are collapsed back down to **0 pixels** before being removed from the list.
|
||||
There's also a nice fade-in and fade-out effect that occurs at the same time. All of this is handled
|
||||
by the CSS transition declarations at the top of the example code above.
|
||||
|
||||
Although most modern browsers have good support for [CSS transitions](http://caniuse.com/#feat=css-transitions)
|
||||
|
||||
+3134
-9
File diff suppressed because it is too large
Load Diff
Generated
+4868
-56
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -15,7 +15,8 @@
|
||||
"engineStrict": true,
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/npm/check-node-modules.js --purge",
|
||||
"postinstall": "node scripts/npm/copy-npm-shrinkwrap.js"
|
||||
"postinstall": "node scripts/npm/copy-npm-shrinkwrap.js",
|
||||
"commit": "git-cz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-benchpress": "0.x.x",
|
||||
@@ -24,8 +25,10 @@
|
||||
"browserstacktunnel-wrapper": "~1.3.1",
|
||||
"canonical-path": "0.0.2",
|
||||
"cheerio": "^0.17.0",
|
||||
"commitizen": "^2.3.0",
|
||||
"cz-conventional-changelog": "^1.1.4",
|
||||
"dgeni": "^0.4.0",
|
||||
"dgeni-packages": "^0.10.0",
|
||||
"dgeni-packages": "^0.11.0",
|
||||
"event-stream": "~3.1.0",
|
||||
"grunt": "~0.4.2",
|
||||
"grunt-bump": "~0.0.13",
|
||||
@@ -80,5 +83,10 @@
|
||||
"url": "https://github.com/angular/angular.js/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"dependencies": {}
|
||||
"dependencies": {},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ config.specs = [
|
||||
];
|
||||
|
||||
config.capabilities = {
|
||||
browserName: 'chrome',
|
||||
browserName: 'chrome'
|
||||
};
|
||||
|
||||
exports.config = config;
|
||||
|
||||
+113
-94
@@ -198,20 +198,24 @@ msie = document.documentMode;
|
||||
* String ...)
|
||||
*/
|
||||
function isArrayLike(obj) {
|
||||
if (obj == null || isWindow(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// `null`, `undefined` and `window` are not array-like
|
||||
if (obj == null || isWindow(obj)) return false;
|
||||
|
||||
// arrays, strings and jQuery/jqLite objects are array like
|
||||
// * jqLite is either the jQuery or jqLite constructor function
|
||||
// * we have to check the existance of jqLite first as this method is called
|
||||
// via the forEach method when constructing the jqLite object in the first place
|
||||
if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true;
|
||||
|
||||
// Support: iOS 8.2 (not reproducible in simulator)
|
||||
// "length" in obj used to prevent JIT error (gh-11508)
|
||||
var length = "length" in Object(obj) && obj.length;
|
||||
|
||||
if (obj.nodeType === NODE_TYPE_ELEMENT && length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isString(obj) || isArray(obj) || length === 0 ||
|
||||
typeof length === 'number' && length > 0 && (length - 1) in obj;
|
||||
// NodeList objects (with `item` method) and
|
||||
// other objects with suitable length characteristics are array-like
|
||||
return isNumber(length) &&
|
||||
(length >= 0 && (length - 1) in obj || typeof obj.item == 'function');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,6 +360,10 @@ function baseExtend(dst, objs, deep) {
|
||||
dst[key] = new Date(src.valueOf());
|
||||
} else if (isRegExp(src)) {
|
||||
dst[key] = new RegExp(src);
|
||||
} else if (src.nodeName) {
|
||||
dst[key] = src.cloneNode(true);
|
||||
} else if (isElement(src)) {
|
||||
dst[key] = src.clone();
|
||||
} else {
|
||||
if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
|
||||
baseExtend(dst[key], [src], true);
|
||||
@@ -471,7 +479,7 @@ identity.$inject = [];
|
||||
function valueFn(value) {return function() {return value;};}
|
||||
|
||||
function hasCustomToString(obj) {
|
||||
return isFunction(obj.toString) && obj.toString !== Object.prototype.toString;
|
||||
return isFunction(obj.toString) && obj.toString !== toString;
|
||||
}
|
||||
|
||||
|
||||
@@ -670,9 +678,9 @@ function isPromiseLike(obj) {
|
||||
}
|
||||
|
||||
|
||||
var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/;
|
||||
var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/;
|
||||
function isTypedArray(value) {
|
||||
return TYPED_ARRAY_REGEXP.test(toString.call(value));
|
||||
return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
|
||||
}
|
||||
|
||||
|
||||
@@ -794,100 +802,111 @@ function arrayRemove(array, value) {
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
function copy(source, destination, stackSource, stackDest) {
|
||||
if (isWindow(source) || isScope(source)) {
|
||||
throw ngMinErr('cpws',
|
||||
"Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
}
|
||||
if (isTypedArray(destination)) {
|
||||
throw ngMinErr('cpta',
|
||||
"Can't copy! TypedArray destination cannot be mutated.");
|
||||
}
|
||||
function copy(source, destination) {
|
||||
var stackSource = [];
|
||||
var stackDest = [];
|
||||
|
||||
if (!destination) {
|
||||
destination = source;
|
||||
if (isObject(source)) {
|
||||
var index;
|
||||
if (stackSource && (index = stackSource.indexOf(source)) !== -1) {
|
||||
return stackDest[index];
|
||||
}
|
||||
|
||||
// TypedArray, Date and RegExp have specific copy functionality and must be
|
||||
// pushed onto the stack before returning.
|
||||
// Array and other objects create the base object and recurse to copy child
|
||||
// objects. The array/object will be pushed onto the stack when recursed.
|
||||
if (isArray(source)) {
|
||||
return copy(source, [], stackSource, stackDest);
|
||||
} else if (isTypedArray(source)) {
|
||||
destination = new source.constructor(source);
|
||||
} else if (isDate(source)) {
|
||||
destination = new Date(source.getTime());
|
||||
} else if (isRegExp(source)) {
|
||||
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
|
||||
destination.lastIndex = source.lastIndex;
|
||||
} else if (isFunction(source.cloneNode)) {
|
||||
destination = source.cloneNode(true);
|
||||
} else {
|
||||
var emptyObject = Object.create(getPrototypeOf(source));
|
||||
return copy(source, emptyObject, stackSource, stackDest);
|
||||
}
|
||||
|
||||
if (stackDest) {
|
||||
stackSource.push(source);
|
||||
stackDest.push(destination);
|
||||
}
|
||||
if (destination) {
|
||||
if (isTypedArray(destination)) {
|
||||
throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
|
||||
}
|
||||
} else {
|
||||
if (source === destination) throw ngMinErr('cpi',
|
||||
"Can't copy! Source and destination are identical.");
|
||||
|
||||
stackSource = stackSource || [];
|
||||
stackDest = stackDest || [];
|
||||
|
||||
if (isObject(source)) {
|
||||
stackSource.push(source);
|
||||
stackDest.push(destination);
|
||||
if (source === destination) {
|
||||
throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
|
||||
}
|
||||
|
||||
// Empty the destination object
|
||||
if (isArray(destination)) {
|
||||
destination.length = 0;
|
||||
} else {
|
||||
forEach(destination, function(value, key) {
|
||||
if (key !== '$$hashKey') {
|
||||
delete destination[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stackSource.push(source);
|
||||
stackDest.push(destination);
|
||||
return copyRecurse(source, destination);
|
||||
}
|
||||
|
||||
return copyElement(source);
|
||||
|
||||
function copyRecurse(source, destination) {
|
||||
var h = destination.$$hashKey;
|
||||
var result, key;
|
||||
if (isArray(source)) {
|
||||
destination.length = 0;
|
||||
for (var i = 0; i < source.length; i++) {
|
||||
destination.push(copy(source[i], null, stackSource, stackDest));
|
||||
for (var i = 0, ii = source.length; i < ii; i++) {
|
||||
destination.push(copyElement(source[i]));
|
||||
}
|
||||
} else if (isBlankObject(source)) {
|
||||
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
|
||||
for (key in source) {
|
||||
destination[key] = copyElement(source[key]);
|
||||
}
|
||||
} else if (source && typeof source.hasOwnProperty === 'function') {
|
||||
// Slow path, which must rely on hasOwnProperty
|
||||
for (key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
destination[key] = copyElement(source[key]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var h = destination.$$hashKey;
|
||||
if (isArray(destination)) {
|
||||
destination.length = 0;
|
||||
} else {
|
||||
forEach(destination, function(value, key) {
|
||||
delete destination[key];
|
||||
});
|
||||
}
|
||||
if (isBlankObject(source)) {
|
||||
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
|
||||
for (key in source) {
|
||||
destination[key] = copy(source[key], null, stackSource, stackDest);
|
||||
}
|
||||
} else if (source && typeof source.hasOwnProperty === 'function') {
|
||||
// Slow path, which must rely on hasOwnProperty
|
||||
for (key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
destination[key] = copy(source[key], null, stackSource, stackDest);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Slowest path --- hasOwnProperty can't be called as a method
|
||||
for (key in source) {
|
||||
if (hasOwnProperty.call(source, key)) {
|
||||
destination[key] = copy(source[key], null, stackSource, stackDest);
|
||||
}
|
||||
// Slowest path --- hasOwnProperty can't be called as a method
|
||||
for (key in source) {
|
||||
if (hasOwnProperty.call(source, key)) {
|
||||
destination[key] = copyElement(source[key]);
|
||||
}
|
||||
}
|
||||
setHashKey(destination,h);
|
||||
}
|
||||
setHashKey(destination, h);
|
||||
return destination;
|
||||
}
|
||||
|
||||
function copyElement(source) {
|
||||
// Simple values
|
||||
if (!isObject(source)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
// Already copied values
|
||||
var index = stackSource.indexOf(source);
|
||||
if (index !== -1) {
|
||||
return stackDest[index];
|
||||
}
|
||||
|
||||
if (isWindow(source) || isScope(source)) {
|
||||
throw ngMinErr('cpws',
|
||||
"Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
}
|
||||
|
||||
var needsRecurse = false;
|
||||
var destination;
|
||||
|
||||
if (isArray(source)) {
|
||||
destination = [];
|
||||
needsRecurse = true;
|
||||
} else if (isTypedArray(source)) {
|
||||
destination = new source.constructor(source);
|
||||
} else if (isDate(source)) {
|
||||
destination = new Date(source.getTime());
|
||||
} else if (isRegExp(source)) {
|
||||
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
|
||||
destination.lastIndex = source.lastIndex;
|
||||
} else if (isFunction(source.cloneNode)) {
|
||||
destination = source.cloneNode(true);
|
||||
} else {
|
||||
destination = Object.create(getPrototypeOf(source));
|
||||
needsRecurse = true;
|
||||
}
|
||||
|
||||
stackSource.push(source);
|
||||
stackDest.push(destination);
|
||||
|
||||
return needsRecurse
|
||||
? copyRecurse(source, destination)
|
||||
: destination;
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+60
-34
@@ -241,6 +241,14 @@ function jqLiteParseHTML(html, context) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
|
||||
var jqLiteContains = Node.prototype.contains || function(arg) {
|
||||
// jshint bitwise: false
|
||||
return !!(this.compareDocumentPosition(arg) & 16);
|
||||
// jshint bitwise: true
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////
|
||||
function JQLite(element) {
|
||||
if (element instanceof JQLite) {
|
||||
@@ -299,17 +307,23 @@ function jqLiteOff(element, type, fn, unsupported) {
|
||||
delete events[type];
|
||||
}
|
||||
} else {
|
||||
forEach(type.split(' '), function(type) {
|
||||
if (isDefined(fn)) {
|
||||
var listenerFns = events[type];
|
||||
arrayRemove(listenerFns || [], fn);
|
||||
if (listenerFns && listenerFns.length > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
removeEventListenerFn(element, type, handle);
|
||||
delete events[type];
|
||||
var removeHandler = function(type) {
|
||||
var listenerFns = events[type];
|
||||
if (isDefined(fn)) {
|
||||
arrayRemove(listenerFns || [], fn);
|
||||
}
|
||||
if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
|
||||
removeEventListenerFn(element, type, handle);
|
||||
delete events[type];
|
||||
}
|
||||
};
|
||||
|
||||
forEach(type.split(' '), function(type) {
|
||||
removeHandler(type);
|
||||
if (MOUSE_EVENT_MAP[type]) {
|
||||
removeHandler(MOUSE_EVENT_MAP[type]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -764,6 +778,9 @@ function createEventHandler(element, events) {
|
||||
return event.immediatePropagationStopped === true;
|
||||
};
|
||||
|
||||
// Some events have special handlers that wrap the real handler
|
||||
var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper;
|
||||
|
||||
// Copy event handlers in case event handlers array is modified during execution.
|
||||
if ((eventFnsLength > 1)) {
|
||||
eventFns = shallowCopy(eventFns);
|
||||
@@ -771,7 +788,7 @@ function createEventHandler(element, events) {
|
||||
|
||||
for (var i = 0; i < eventFnsLength; i++) {
|
||||
if (!event.isImmediatePropagationStopped()) {
|
||||
eventFns[i].call(element, event);
|
||||
handlerWrapper(element, event, eventFns[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -782,6 +799,22 @@ function createEventHandler(element, events) {
|
||||
return eventHandler;
|
||||
}
|
||||
|
||||
function defaultHandlerWrapper(element, event, handler) {
|
||||
handler.call(element, event);
|
||||
}
|
||||
|
||||
function specialMouseHandlerWrapper(target, event, handler) {
|
||||
// Refer to jQuery's implementation of mouseenter & mouseleave
|
||||
// Read about mouseenter and mouseleave:
|
||||
// http://www.quirksmode.org/js/events_mouse.html#link8
|
||||
var related = event.relatedTarget;
|
||||
// For mousenter/leave call the handler if related is outside the target.
|
||||
// NB: No relatedTarget if the mouse left/entered the browser window
|
||||
if (!related || (related !== target && !jqLiteContains.call(target, related))) {
|
||||
handler.call(target, event);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// Functions iterating traversal.
|
||||
// These functions chain results into a single
|
||||
@@ -810,35 +843,28 @@ forEach({
|
||||
var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type];
|
||||
var i = types.length;
|
||||
|
||||
while (i--) {
|
||||
type = types[i];
|
||||
var addHandler = function(type, specialHandlerWrapper, noEventListener) {
|
||||
var eventFns = events[type];
|
||||
|
||||
if (!eventFns) {
|
||||
events[type] = [];
|
||||
|
||||
if (type === 'mouseenter' || type === 'mouseleave') {
|
||||
// Refer to jQuery's implementation of mouseenter & mouseleave
|
||||
// Read about mouseenter and mouseleave:
|
||||
// http://www.quirksmode.org/js/events_mouse.html#link8
|
||||
|
||||
jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) {
|
||||
var target = this, related = event.relatedTarget;
|
||||
// For mousenter/leave call the handler if related is outside the target.
|
||||
// NB: No relatedTarget if the mouse left/entered the browser window
|
||||
if (!related || (related !== target && !target.contains(related))) {
|
||||
handle(event, type);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
if (type !== '$destroy') {
|
||||
addEventListenerFn(element, type, handle);
|
||||
}
|
||||
eventFns = events[type] = [];
|
||||
eventFns.specialHandlerWrapper = specialHandlerWrapper;
|
||||
if (type !== '$destroy' && !noEventListener) {
|
||||
addEventListenerFn(element, type, handle);
|
||||
}
|
||||
eventFns = events[type];
|
||||
}
|
||||
|
||||
eventFns.push(fn);
|
||||
};
|
||||
|
||||
while (i--) {
|
||||
type = types[i];
|
||||
if (MOUSE_EVENT_MAP[type]) {
|
||||
addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper);
|
||||
addHandler(type, undefined, true);
|
||||
} else {
|
||||
addHandler(type);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ function setupModuleLoader(window) {
|
||||
* @param {string} name constant name
|
||||
* @param {*} object Constant value.
|
||||
* @description
|
||||
* Because the constant are fixed, they get applied before other provide methods.
|
||||
* Because the constants are fixed, they get applied before other provide methods.
|
||||
* See {@link auto.$provide#constant $provide.constant()}.
|
||||
*/
|
||||
constant: invokeLater('$provide', 'constant', 'unshift'),
|
||||
|
||||
@@ -41,7 +41,7 @@ function $AnchorScrollProvider() {
|
||||
* When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
|
||||
* current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
|
||||
* in the
|
||||
* [HTML5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
|
||||
* [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
|
||||
*
|
||||
* It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
|
||||
* match any anchor whenever it changes. This can be disabled by calling
|
||||
|
||||
+1
-1
@@ -285,7 +285,7 @@ var $AnimateProvider = ['$provide', function($provide) {
|
||||
* when an animation is detected (and animations are enabled), $animate will do the heavy lifting
|
||||
* to ensure that animation runs with the triggered DOM operation.
|
||||
*
|
||||
* By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't
|
||||
* By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't
|
||||
* included and only when it is active then the animation hooks that `$animate` triggers will be
|
||||
* functional. Once active then all structural `ng-` directives will trigger animations as they perform
|
||||
* their DOM-related operations (enter, leave and move). Other directives such as `ngClass`,
|
||||
|
||||
@@ -93,9 +93,9 @@ function $CacheFactoryProvider() {
|
||||
|
||||
var size = 0,
|
||||
stats = extend({}, options, {id: cacheId}),
|
||||
data = {},
|
||||
data = createMap(),
|
||||
capacity = (options && options.capacity) || Number.MAX_VALUE,
|
||||
lruHash = {},
|
||||
lruHash = createMap(),
|
||||
freshEnd = null,
|
||||
staleEnd = null;
|
||||
|
||||
@@ -223,6 +223,8 @@ function $CacheFactoryProvider() {
|
||||
delete lruHash[key];
|
||||
}
|
||||
|
||||
if (!(key in data)) return;
|
||||
|
||||
delete data[key];
|
||||
size--;
|
||||
},
|
||||
@@ -237,9 +239,9 @@ function $CacheFactoryProvider() {
|
||||
* Clears the cache object of any entries.
|
||||
*/
|
||||
removeAll: function() {
|
||||
data = {};
|
||||
data = createMap();
|
||||
size = 0;
|
||||
lruHash = {};
|
||||
lruHash = createMap();
|
||||
freshEnd = staleEnd = null;
|
||||
},
|
||||
|
||||
@@ -399,4 +401,3 @@ function $TemplateCacheProvider() {
|
||||
return $cacheFactory('templates');
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
+79
-80
@@ -1240,6 +1240,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
|
||||
},
|
||||
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
|
||||
var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
|
||||
|
||||
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
|
||||
var bindings = $element.data('$binding') || [];
|
||||
@@ -1292,6 +1293,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
return function publicLinkFn(scope, cloneConnectFn, options) {
|
||||
assertArg(scope, 'scope');
|
||||
|
||||
if (previousCompileContext && previousCompileContext.needsNewScope) {
|
||||
// A parent directive did a replace and a directive on this element asked
|
||||
// for transclusion, which caused us to lose a layer of element on which
|
||||
// we could hold the new transclusion scope, so we will create it manually
|
||||
// here.
|
||||
scope = scope.$parent.$new();
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
|
||||
transcludeControllers = options.transcludeControllers,
|
||||
@@ -1437,11 +1446,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
if (nodeLinkFn.scope) {
|
||||
childScope = scope.$new();
|
||||
compile.$$addScopeInfo(jqLite(node), childScope);
|
||||
var destroyBindings = nodeLinkFn.$$destroyBindings;
|
||||
if (destroyBindings) {
|
||||
nodeLinkFn.$$destroyBindings = null;
|
||||
childScope.$on('$destroyed', destroyBindings);
|
||||
}
|
||||
} else {
|
||||
childScope = scope;
|
||||
}
|
||||
@@ -1460,8 +1464,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
childBoundTranscludeFn = null;
|
||||
}
|
||||
|
||||
nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn,
|
||||
nodeLinkFn);
|
||||
nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
|
||||
|
||||
} else if (childLinkFn) {
|
||||
childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
|
||||
@@ -1530,13 +1533,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
});
|
||||
}
|
||||
|
||||
var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
|
||||
if (directiveIsMultiElement(directiveNName)) {
|
||||
if (ngAttrName === directiveNName + 'Start') {
|
||||
attrStartName = name;
|
||||
attrEndName = name.substr(0, name.length - 5) + 'end';
|
||||
name = name.substr(0, name.length - 6);
|
||||
}
|
||||
var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
|
||||
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
|
||||
attrStartName = name;
|
||||
attrEndName = name.substr(0, name.length - 5) + 'end';
|
||||
name = name.substr(0, name.length - 6);
|
||||
}
|
||||
|
||||
nName = directiveNormalize(name.toLowerCase());
|
||||
@@ -1775,7 +1776,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
} else {
|
||||
$template = jqLite(jqLiteClone(compileNode)).contents();
|
||||
$compileNode.empty(); // clear contents
|
||||
childTranscludeFn = compile($template, transcludeFn);
|
||||
childTranscludeFn = compile($template, transcludeFn, undefined,
|
||||
undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1817,8 +1819,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
|
||||
var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));
|
||||
|
||||
if (newIsolateScopeDirective) {
|
||||
markDirectivesAsIsolate(templateDirectives);
|
||||
if (newIsolateScopeDirective || newScopeDirective) {
|
||||
// The original directive caused the current element to be replaced but this element
|
||||
// also needs to have a new scope, so we need to tell the template directives
|
||||
// that they would need to get their scope from further up, if they require transclusion
|
||||
markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective);
|
||||
}
|
||||
directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
|
||||
mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
|
||||
@@ -1971,10 +1976,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
return elementControllers;
|
||||
}
|
||||
|
||||
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn,
|
||||
thisLinkFn) {
|
||||
var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element,
|
||||
attrs;
|
||||
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
|
||||
var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
|
||||
attrs, removeScopeBindingWatches, removeControllerBindingWatches;
|
||||
|
||||
if (compileNode === linkNode) {
|
||||
attrs = templateAttrs;
|
||||
@@ -1984,8 +1988,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
attrs = new Attributes($element, templateAttrs);
|
||||
}
|
||||
|
||||
controllerScope = scope;
|
||||
if (newIsolateScopeDirective) {
|
||||
isolateScope = scope.$new(true);
|
||||
} else if (newScopeDirective) {
|
||||
controllerScope = scope.$parent;
|
||||
}
|
||||
|
||||
if (boundTranscludeFn) {
|
||||
@@ -2006,42 +2013,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
compile.$$addScopeClass($element, true);
|
||||
isolateScope.$$isolateBindings =
|
||||
newIsolateScopeDirective.$$isolateBindings;
|
||||
initializeDirectiveBindings(scope, attrs, isolateScope,
|
||||
isolateScope.$$isolateBindings,
|
||||
newIsolateScopeDirective, isolateScope);
|
||||
}
|
||||
if (elementControllers) {
|
||||
// Initialize bindToController bindings for new/isolate scopes
|
||||
var scopeDirective = newIsolateScopeDirective || newScopeDirective;
|
||||
var bindings;
|
||||
var controllerForBindings;
|
||||
if (scopeDirective && elementControllers[scopeDirective.name]) {
|
||||
bindings = scopeDirective.$$bindings.bindToController;
|
||||
controller = elementControllers[scopeDirective.name];
|
||||
|
||||
if (controller && controller.identifier && bindings) {
|
||||
controllerForBindings = controller;
|
||||
thisLinkFn.$$destroyBindings =
|
||||
initializeDirectiveBindings(scope, attrs, controller.instance,
|
||||
bindings, scopeDirective);
|
||||
}
|
||||
removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
|
||||
isolateScope.$$isolateBindings,
|
||||
newIsolateScopeDirective);
|
||||
if (removeScopeBindingWatches) {
|
||||
isolateScope.$on('$destroy', removeScopeBindingWatches);
|
||||
}
|
||||
for (i in elementControllers) {
|
||||
controller = elementControllers[i];
|
||||
var controllerResult = controller();
|
||||
}
|
||||
|
||||
if (controllerResult !== controller.instance) {
|
||||
// If the controller constructor has a return value, overwrite the instance
|
||||
// from setupControllers and update the element data
|
||||
controller.instance = controllerResult;
|
||||
$element.data('$' + i + 'Controller', controllerResult);
|
||||
if (controller === controllerForBindings) {
|
||||
// Remove and re-install bindToController bindings
|
||||
thisLinkFn.$$destroyBindings();
|
||||
thisLinkFn.$$destroyBindings =
|
||||
initializeDirectiveBindings(scope, attrs, controllerResult, bindings, scopeDirective);
|
||||
}
|
||||
}
|
||||
// Initialize bindToController bindings
|
||||
for (var name in elementControllers) {
|
||||
var controllerDirective = controllerDirectives[name];
|
||||
var controller = elementControllers[name];
|
||||
var bindings = controllerDirective.$$bindings.bindToController;
|
||||
|
||||
if (controller.identifier && bindings) {
|
||||
removeControllerBindingWatches =
|
||||
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
|
||||
}
|
||||
|
||||
var controllerResult = controller();
|
||||
if (controllerResult !== controller.instance) {
|
||||
// If the controller constructor has a return value, overwrite the instance
|
||||
// from setupControllers
|
||||
controller.instance = controllerResult;
|
||||
$element.data('$' + controllerDirective.name + 'Controller', controllerResult);
|
||||
removeControllerBindingWatches && removeControllerBindingWatches();
|
||||
removeControllerBindingWatches =
|
||||
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2101,10 +2100,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
function markDirectivesAsIsolate(directives) {
|
||||
// mark all directives as needing isolate scope.
|
||||
// Depending upon the context in which a directive finds itself it might need to have a new isolated
|
||||
// or child scope created. For instance:
|
||||
// * if the directive has been pulled into a template because another directive with a higher priority
|
||||
// asked for element transclusion
|
||||
// * if the directive itself asks for transclusion but it is at the root of a template and the original
|
||||
// element was replaced. See https://github.com/angular/angular.js/issues/12936
|
||||
function markDirectiveScope(directives, isolateScope, newScope) {
|
||||
for (var j = 0, jj = directives.length; j < jj; j++) {
|
||||
directives[j] = inherit(directives[j], {$$isolateScope: true});
|
||||
directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2251,7 +2255,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs);
|
||||
|
||||
if (isObject(origAsyncDirective.scope)) {
|
||||
markDirectivesAsIsolate(templateDirectives);
|
||||
// the original directive that caused the template to be loaded async required
|
||||
// an isolate scope
|
||||
markDirectiveScope(templateDirectives, true);
|
||||
}
|
||||
directives = templateDirectives.concat(directives);
|
||||
mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
|
||||
@@ -2300,7 +2306,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
childBoundTranscludeFn = boundTranscludeFn;
|
||||
}
|
||||
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement,
|
||||
childBoundTranscludeFn, afterTemplateNodeLinkFn);
|
||||
childBoundTranscludeFn);
|
||||
}
|
||||
linkQueue = null;
|
||||
});
|
||||
@@ -2317,8 +2323,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
|
||||
childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
|
||||
}
|
||||
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn,
|
||||
afterTemplateNodeLinkFn);
|
||||
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2530,7 +2535,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
// Copy over user data (that includes Angular's $scope etc.). Don't copy private
|
||||
// data here because there's no public interface in jQuery to do that and copying over
|
||||
// event listeners (which is the main use of private data) wouldn't work anyway.
|
||||
jqLite(newNode).data(jqLite(firstElementToRemove).data());
|
||||
jqLite.data(newNode, jqLite.data(firstElementToRemove));
|
||||
|
||||
// Remove data of the replaced element. We cannot just call .remove()
|
||||
// on the element it since that would deallocate scope that is needed
|
||||
@@ -2578,9 +2583,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
|
||||
// Set up $watches for isolate scope and controller bindings. This process
|
||||
// only occurs for isolate scopes and new scopes with controllerAs.
|
||||
function initializeDirectiveBindings(scope, attrs, destination, bindings,
|
||||
directive, newScope) {
|
||||
var onNewScopeDestroyed;
|
||||
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
|
||||
var removeWatchCollection = [];
|
||||
forEach(bindings, function(definition, scopeName) {
|
||||
var attrName = definition.attrName,
|
||||
optional = definition.optional,
|
||||
@@ -2642,14 +2646,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
return lastValue = parentValue;
|
||||
};
|
||||
parentValueWatch.$stateful = true;
|
||||
var unwatch;
|
||||
var removeWatch;
|
||||
if (definition.collection) {
|
||||
unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
|
||||
removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
|
||||
} else {
|
||||
unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
|
||||
removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
|
||||
}
|
||||
onNewScopeDestroyed = (onNewScopeDestroyed || []);
|
||||
onNewScopeDestroyed.push(unwatch);
|
||||
removeWatchCollection.push(removeWatch);
|
||||
break;
|
||||
|
||||
case '&':
|
||||
@@ -2665,16 +2668,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
break;
|
||||
}
|
||||
});
|
||||
var destroyBindings = onNewScopeDestroyed ? function destroyBindings() {
|
||||
for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) {
|
||||
onNewScopeDestroyed[i]();
|
||||
|
||||
return removeWatchCollection.length && function removeWatches() {
|
||||
for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
|
||||
removeWatchCollection[i]();
|
||||
}
|
||||
} : noop;
|
||||
if (newScope && destroyBindings !== noop) {
|
||||
newScope.$on('$destroy', destroyBindings);
|
||||
return noop;
|
||||
}
|
||||
return destroyBindings;
|
||||
};
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
|
||||
var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
|
||||
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
|
||||
// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
|
||||
var URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/;
|
||||
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
||||
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
|
||||
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||
|
||||
@@ -34,7 +34,13 @@
|
||||
* @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
|
||||
* make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
|
||||
* @param {string=} onload Expression to evaluate when a new partial is loaded.
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* **Note:** When using onload on SVG elements in IE11, the browser will try to call
|
||||
* a function with the name on the window element, which will usually throw a
|
||||
* "function is undefined" error. To fix this, you can instead use `data-onload` or a
|
||||
* different form that {@link guide/directive#normalization matches} `onload`.
|
||||
* </div>
|
||||
*
|
||||
* @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
|
||||
* $anchorScroll} to scroll the viewport after the content is loaded.
|
||||
*
|
||||
|
||||
@@ -1141,12 +1141,13 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
|
||||
</label><br />
|
||||
</form>
|
||||
<pre>user.name = <span ng-bind="user.name"></span></pre>
|
||||
<pre>user.data = <span ng-bind="user.data"></span></pre>
|
||||
</div>
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('optionsExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.user = { name: 'say', data: '' };
|
||||
$scope.user = { name: 'John', data: '' };
|
||||
|
||||
$scope.cancel = function(e) {
|
||||
if (e.keyCode == 27) {
|
||||
@@ -1161,20 +1162,20 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
|
||||
var other = element(by.model('user.data'));
|
||||
|
||||
it('should allow custom events', function() {
|
||||
input.sendKeys(' hello');
|
||||
input.sendKeys(' Doe');
|
||||
input.click();
|
||||
expect(model.getText()).toEqual('say');
|
||||
expect(model.getText()).toEqual('John');
|
||||
other.click();
|
||||
expect(model.getText()).toEqual('say hello');
|
||||
expect(model.getText()).toEqual('John Doe');
|
||||
});
|
||||
|
||||
it('should $rollbackViewValue when model changes', function() {
|
||||
input.sendKeys(' hello');
|
||||
expect(input.getAttribute('value')).toEqual('say hello');
|
||||
input.sendKeys(' Doe');
|
||||
expect(input.getAttribute('value')).toEqual('John Doe');
|
||||
input.sendKeys(protractor.Key.ESCAPE);
|
||||
expect(input.getAttribute('value')).toEqual('say');
|
||||
expect(input.getAttribute('value')).toEqual('John');
|
||||
other.click();
|
||||
expect(model.getText()).toEqual('say');
|
||||
expect(model.getText()).toEqual('John');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
@@ -1200,7 +1201,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
|
||||
<file name="app.js">
|
||||
angular.module('optionsExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.user = { name: 'say' };
|
||||
$scope.user = { name: 'Igor' };
|
||||
}]);
|
||||
</file>
|
||||
</example>
|
||||
|
||||
@@ -33,19 +33,27 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
*
|
||||
* ## Complex Models (objects or collections)
|
||||
*
|
||||
* **Note:** By default, `ngModel` watches the model by reference, not value. This is important when
|
||||
* binding any input directive to a model that is an object or a collection.
|
||||
* By default, `ngModel` watches the model by reference, not value. This is important to know when
|
||||
* binding the select to a model that is an object or a collection.
|
||||
*
|
||||
* Since this is a common situation for `ngOptions` the directive additionally watches the model using
|
||||
* `$watchCollection` when the select has the `multiple` attribute or when there is a `track by` clause in
|
||||
* the options expression. This allows ngOptions to trigger a re-rendering of the options even if the actual
|
||||
* object/collection has not changed identity but only a property on the object or an item in the collection
|
||||
* changes.
|
||||
* One issue occurs if you want to preselect an option. For example, if you set
|
||||
* the model to an object that is equal to an object in your collection, `ngOptions` won't be able to set the selection,
|
||||
* because the objects are not identical. So by default, you should always reference the item in your collection
|
||||
* for preselections, e.g.: `$scope.selected = $scope.collection[3]`.
|
||||
*
|
||||
* Another solution is to use a `track by` clause, because then `ngOptions` will track the identity
|
||||
* of the item not by reference, but by the result of the `track by` expression. For example, if your
|
||||
* collection items have an id property, you would `track by item.id`.
|
||||
*
|
||||
* A different issue with objects or collections is that ngModel won't detect if an object property or
|
||||
* a collection item changes. For that reason, `ngOptions` additionally watches the model using
|
||||
* `$watchCollection`, when the expression contains a `track by` clause or the the select has the `multiple` attribute.
|
||||
* This allows ngOptions to trigger a re-rendering of the options even if the actual object/collection
|
||||
* has not changed identity, but only a property on the object or an item in the collection changes.
|
||||
*
|
||||
* Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection
|
||||
* if the model is an array). This means that changing a property deeper inside the object/collection that the
|
||||
* first level will not trigger a re-rendering.
|
||||
*
|
||||
* if the model is an array). This means that changing a property deeper than the first level inside the
|
||||
* object/collection will not trigger a re-rendering.
|
||||
*
|
||||
* ## `select` **`as`**
|
||||
*
|
||||
@@ -58,17 +66,13 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* ### `select` **`as`** and **`track by`**
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
|
||||
* Be careful when using `select` **`as`** and **`track by`** in the same expression.
|
||||
* </div>
|
||||
*
|
||||
* Consider the following example:
|
||||
*
|
||||
* ```html
|
||||
* <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"></select>
|
||||
* ```
|
||||
* Given this array of items on the $scope:
|
||||
*
|
||||
* ```js
|
||||
* $scope.values = [{
|
||||
* $scope.items = [{
|
||||
* id: 1,
|
||||
* label: 'aLabel',
|
||||
* subItem: { name: 'aSubItem' }
|
||||
@@ -77,20 +81,33 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* label: 'bLabel',
|
||||
* subItem: { name: 'bSubItem' }
|
||||
* }];
|
||||
*
|
||||
* $scope.selected = { name: 'aSubItem' };
|
||||
* ```
|
||||
*
|
||||
* With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
|
||||
* of the data source (to `item` in this example). To calculate whether an element is selected, we do the
|
||||
* following:
|
||||
* This will work:
|
||||
*
|
||||
* 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
|
||||
* 2. Apply **`track by`** to the already selected value in `ngModel`.
|
||||
* In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
|
||||
* value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
|
||||
* a wrong object, the selected element can't be found, `<select>` is always reset to the "not
|
||||
* selected" option.
|
||||
* ```html
|
||||
* <select ng-options="item as item.label for item in items track by item.id" ng-model="selected"></select>
|
||||
* ```
|
||||
* ```js
|
||||
* $scope.selected = $scope.items[0];
|
||||
* ```
|
||||
*
|
||||
* but this will not work:
|
||||
*
|
||||
* ```html
|
||||
* <select ng-options="item.subItem as item.label for item in items track by item.id" ng-model="selected"></select>
|
||||
* ```
|
||||
* ```js
|
||||
* $scope.selected = $scope.items[0].subItem;
|
||||
* ```
|
||||
*
|
||||
* In both examples, the **`track by`** expression is applied successfully to each `item` in the
|
||||
* `items` array. Because the selected option has been set programmatically in the controller, the
|
||||
* **`track by`** expression is also applied to the `ngModel` value. In the first example, the
|
||||
* `ngModel` value is `items[0]` and the **`track by`** expression evaluates to `items[0].id` with
|
||||
* no issue. In the second example, the `ngModel` value is `items[0].subItem` and the **`track by`**
|
||||
* expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value
|
||||
* is not matched against any `<option>` and the `<select>` appears as having no selected value.
|
||||
*
|
||||
*
|
||||
* @param {string} ngModel Assignable angular expression to data-bind to.
|
||||
@@ -392,11 +409,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
var optionTemplate = document.createElement('option'),
|
||||
optGroupTemplate = document.createElement('optgroup');
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
terminal: true,
|
||||
require: ['select', '?ngModel'],
|
||||
link: function(scope, selectElement, attr, ctrls) {
|
||||
|
||||
function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
|
||||
|
||||
// if ngModel is not defined, we don't need to do anything
|
||||
var ngModelCtrl = ctrls[1];
|
||||
@@ -451,7 +465,6 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
unknownOption.remove();
|
||||
};
|
||||
|
||||
|
||||
// Update the controller methods for multiple selectable options
|
||||
if (!multiple) {
|
||||
|
||||
@@ -626,13 +639,15 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
var emptyOption_ = emptyOption && emptyOption[0];
|
||||
var unknownOption_ = unknownOption && unknownOption[0];
|
||||
|
||||
// We cannot rely on the extracted empty option being the same as the compiled empty option,
|
||||
// because the compiled empty option might have been replaced by a comment because
|
||||
// it had an "element" transclusion directive on it (such as ngIf)
|
||||
if (emptyOption_ || unknownOption_) {
|
||||
while (current &&
|
||||
(current === emptyOption_ ||
|
||||
current === unknownOption_ ||
|
||||
emptyOption_ && emptyOption_.nodeType === NODE_TYPE_COMMENT)) {
|
||||
// Empty options might have directives that transclude
|
||||
// and insert comments (e.g. ngIf)
|
||||
current.nodeType === NODE_TYPE_COMMENT ||
|
||||
current.value === '')) {
|
||||
current = current.nextSibling;
|
||||
}
|
||||
}
|
||||
@@ -729,7 +744,20 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
terminal: true,
|
||||
require: ['select', '?ngModel'],
|
||||
link: {
|
||||
pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) {
|
||||
// Deactivate the SelectController.register method to prevent
|
||||
// option directives from accidentally registering themselves
|
||||
// (and unwanted $destroy handlers etc.)
|
||||
ctrls[0].registerOption = noop;
|
||||
},
|
||||
post: ngOptionsPostLink
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
* Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser
|
||||
* when running `for key in myObj`. It seems that browsers generally follow the strategy of providing
|
||||
* keys in the order in which they were defined, although there are exceptions when keys are deleted
|
||||
* and reinstated. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_issues
|
||||
* and reinstated. See the [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
|
||||
*
|
||||
* If this is not desired, the recommended workaround is to convert your object into an array
|
||||
* that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
|
||||
@@ -53,15 +53,21 @@
|
||||
*
|
||||
* # Tracking and Duplicates
|
||||
*
|
||||
* When the contents of the collection change, `ngRepeat` makes the corresponding changes to the DOM:
|
||||
* `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
|
||||
* the collection. When a change happens, ngRepeat then makes the corresponding changes to the DOM:
|
||||
*
|
||||
* * When an item is added, a new instance of the template is added to the DOM.
|
||||
* * When an item is removed, its template instance is removed from the DOM.
|
||||
* * When items are reordered, their respective templates are reordered in the DOM.
|
||||
*
|
||||
* By default, `ngRepeat` does not allow duplicate items in arrays. This is because when
|
||||
* there are duplicates, it is not possible to maintain a one-to-one mapping between collection
|
||||
* items and DOM elements.
|
||||
* To minimize creation of DOM elements, `ngRepeat` uses a function
|
||||
* to "keep track" of all items in the collection and their corresponding DOM elements.
|
||||
* 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.
|
||||
*
|
||||
* The default tracking function (which tracks items by their identity) does not allow
|
||||
* duplicate items in arrays. This is because when there are duplicates, it is not possible
|
||||
* to maintain a one-to-one mapping between collection items and DOM elements.
|
||||
*
|
||||
* If you do need to repeat duplicate items, you can substitute the default tracking behavior
|
||||
* with your own using the `track by` expression.
|
||||
@@ -74,7 +80,7 @@
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* You may use arbitrary expressions in `track by`, including references to custom functions
|
||||
* You may also use arbitrary expressions in `track by`, including references to custom functions
|
||||
* on the scope:
|
||||
* ```html
|
||||
* <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
|
||||
@@ -82,10 +88,14 @@
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* If you are working with objects that have an identifier property, you can track
|
||||
* <div class="alert alert-success">
|
||||
* If you are working with objects that have an identifier property, you should track
|
||||
* by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
|
||||
* will not have to rebuild the DOM elements for items it has already rendered, even if the
|
||||
* JavaScript objects in the collection have been substituted for new ones:
|
||||
* JavaScript objects in the collection have been substituted for new ones. For large collections,
|
||||
* this signifincantly improves rendering performance. If you don't have a unique identifier,
|
||||
* `track by $index` can also provide a performance boost.
|
||||
* </div>
|
||||
* ```html
|
||||
* <div ng-repeat="model in collection track by model.id">
|
||||
* {{model.name}}
|
||||
|
||||
+58
-53
@@ -2,6 +2,15 @@
|
||||
|
||||
var noopNgModelController = { $setViewValue: noop, $render: noop };
|
||||
|
||||
function chromeHack(optionElement) {
|
||||
// Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
|
||||
// Adding an <option selected="selected"> element to a <select required="required"> should
|
||||
// automatically select the new element
|
||||
if (optionElement[0].hasAttribute('selected')) {
|
||||
optionElement[0].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc type
|
||||
* @name select.SelectController
|
||||
@@ -77,6 +86,8 @@ var SelectController =
|
||||
}
|
||||
var count = optionsMap.get(value) || 0;
|
||||
optionsMap.put(value, count + 1);
|
||||
self.ngModelCtrl.$render();
|
||||
chromeHack(element);
|
||||
};
|
||||
|
||||
// Tell the select control that an option, with the given value, has been removed
|
||||
@@ -98,6 +109,39 @@ var SelectController =
|
||||
self.hasOption = function(value) {
|
||||
return !!optionsMap.get(value);
|
||||
};
|
||||
|
||||
|
||||
self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
|
||||
|
||||
if (interpolateValueFn) {
|
||||
// The value attribute is interpolated
|
||||
var oldVal;
|
||||
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
|
||||
if (isDefined(oldVal)) {
|
||||
self.removeOption(oldVal);
|
||||
}
|
||||
oldVal = newVal;
|
||||
self.addOption(newVal, optionElement);
|
||||
});
|
||||
} else if (interpolateTextFn) {
|
||||
// The text content is interpolated
|
||||
optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
|
||||
optionAttrs.$set('value', newVal);
|
||||
if (oldVal !== newVal) {
|
||||
self.removeOption(oldVal);
|
||||
}
|
||||
self.addOption(newVal, optionElement);
|
||||
});
|
||||
} else {
|
||||
// The value attribute is static
|
||||
self.addOption(optionAttrs.value, optionElement);
|
||||
}
|
||||
|
||||
optionElement.on('$destroy', function() {
|
||||
self.removeOption(optionAttrs.value);
|
||||
self.ngModelCtrl.$render();
|
||||
});
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
@@ -143,6 +187,8 @@ var SelectController =
|
||||
*
|
||||
* @param {string} ngModel Assignable angular expression to data-bind to.
|
||||
* @param {string=} name Property name of the form under which the control is published.
|
||||
* @param {string=} multiple Allows multiple options to be selected. The selected values will be
|
||||
* bound to the model as an array.
|
||||
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
||||
* @param {string=} ngRequired Adds required attribute and required validation constraint to
|
||||
* the element when the ngRequired expression evaluates to true. Use ngRequired instead of required
|
||||
@@ -308,7 +354,13 @@ var selectDirective = function() {
|
||||
restrict: 'E',
|
||||
require: ['select', '?ngModel'],
|
||||
controller: SelectController,
|
||||
link: function(scope, element, attr, ctrls) {
|
||||
priority: 1,
|
||||
link: {
|
||||
pre: selectPreLink
|
||||
}
|
||||
};
|
||||
|
||||
function selectPreLink(scope, element, attr, ctrls) {
|
||||
|
||||
// if ngModel is not defined, we don't need to do anything
|
||||
var ngModelCtrl = ctrls[1];
|
||||
@@ -378,7 +430,6 @@ var selectDirective = function() {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -386,16 +437,6 @@ var selectDirective = function() {
|
||||
// of dynamically created (and destroyed) option elements to their containing select
|
||||
// directive via its controller.
|
||||
var optionDirective = ['$interpolate', function($interpolate) {
|
||||
|
||||
function chromeHack(optionElement) {
|
||||
// Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
|
||||
// Adding an <option selected="selected"> element to a <select required="required"> should
|
||||
// automatically select the new element
|
||||
if (optionElement[0].hasAttribute('selected')) {
|
||||
optionElement[0].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
priority: 100,
|
||||
@@ -403,12 +444,12 @@ var optionDirective = ['$interpolate', function($interpolate) {
|
||||
|
||||
if (isDefined(attr.value)) {
|
||||
// If the value attribute is defined, check if it contains an interpolation
|
||||
var valueInterpolated = $interpolate(attr.value, true);
|
||||
var interpolateValueFn = $interpolate(attr.value, true);
|
||||
} else {
|
||||
// If the value attribute is not defined then we fall back to the
|
||||
// text content of the option element, which may be interpolated
|
||||
var interpolateFn = $interpolate(element.text(), true);
|
||||
if (!interpolateFn) {
|
||||
var interpolateTextFn = $interpolate(element.text(), true);
|
||||
if (!interpolateTextFn) {
|
||||
attr.$set('value', element.text());
|
||||
}
|
||||
}
|
||||
@@ -422,44 +463,8 @@ var optionDirective = ['$interpolate', function($interpolate) {
|
||||
selectCtrl = parent.data(selectCtrlName) ||
|
||||
parent.parent().data(selectCtrlName); // in case we are in optgroup
|
||||
|
||||
function addOption(optionValue) {
|
||||
selectCtrl.addOption(optionValue, element);
|
||||
selectCtrl.ngModelCtrl.$render();
|
||||
chromeHack(element);
|
||||
}
|
||||
|
||||
// Only update trigger option updates if this is an option within a `select`
|
||||
// that also has `ngModel` attached
|
||||
if (selectCtrl && selectCtrl.ngModelCtrl) {
|
||||
|
||||
if (valueInterpolated) {
|
||||
// The value attribute is interpolated
|
||||
var oldVal;
|
||||
attr.$observe('value', function valueAttributeObserveAction(newVal) {
|
||||
if (isDefined(oldVal)) {
|
||||
selectCtrl.removeOption(oldVal);
|
||||
}
|
||||
oldVal = newVal;
|
||||
addOption(newVal);
|
||||
});
|
||||
} else if (interpolateFn) {
|
||||
// The text content is interpolated
|
||||
scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
|
||||
attr.$set('value', newVal);
|
||||
if (oldVal !== newVal) {
|
||||
selectCtrl.removeOption(oldVal);
|
||||
}
|
||||
addOption(newVal);
|
||||
});
|
||||
} else {
|
||||
// The value attribute is static
|
||||
addOption(attr.value);
|
||||
}
|
||||
|
||||
element.on('$destroy', function() {
|
||||
selectCtrl.removeOption(attr.value);
|
||||
selectCtrl.ngModelCtrl.$render();
|
||||
});
|
||||
if (selectCtrl) {
|
||||
selectCtrl.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ function limitToFilter() {
|
||||
if (!isArray(input) && !isString(input)) return input;
|
||||
|
||||
begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
|
||||
begin = (begin < 0 && begin >= -input.length) ? input.length + begin : begin;
|
||||
begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
|
||||
|
||||
if (limit >= 0) {
|
||||
return input.slice(begin, begin + limit);
|
||||
|
||||
+4
-7
@@ -345,9 +345,9 @@ function $HttpProvider() {
|
||||
* Configure `$http` service to return promises without the shorthand methods `success` and `error`.
|
||||
* This should be used to make sure that applications work without these methods.
|
||||
*
|
||||
* Defaults to false. If no value is specified, returns the current configured value.
|
||||
* Defaults to true. If no value is specified, returns the current configured value.
|
||||
*
|
||||
* @param {boolean=} value If true, `$http` will return a normal promise without the `success` and `error` methods.
|
||||
* @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods.
|
||||
*
|
||||
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
|
||||
* otherwise, returns the current configured value.
|
||||
@@ -999,11 +999,8 @@ function $HttpProvider() {
|
||||
function transformResponse(response) {
|
||||
// make a copy since the response must be cacheable
|
||||
var resp = extend({}, response);
|
||||
if (!response.data) {
|
||||
resp.data = response.data;
|
||||
} else {
|
||||
resp.data = transformData(response.data, response.headers, response.status, config.transformResponse);
|
||||
}
|
||||
resp.data = transformData(response.data, response.headers, response.status,
|
||||
config.transformResponse);
|
||||
return (isSuccess(response.status))
|
||||
? resp
|
||||
: $q.reject(resp);
|
||||
|
||||
+5
-5
@@ -574,9 +574,9 @@ var locationPrototype = {
|
||||
* @description
|
||||
* This method is getter / setter.
|
||||
*
|
||||
* Return hash fragment when called without any parameter.
|
||||
* Returns the hash fragment when called without any parameters.
|
||||
*
|
||||
* Change hash fragment when called with parameter and return `$location`.
|
||||
* Changes the hash fragment when called with a parameter and returns `$location`.
|
||||
*
|
||||
*
|
||||
* ```js
|
||||
@@ -597,8 +597,8 @@ var locationPrototype = {
|
||||
* @name $location#replace
|
||||
*
|
||||
* @description
|
||||
* If called, all changes to $location during current `$digest` will be replacing current history
|
||||
* record, instead of adding new one.
|
||||
* If called, all changes to $location during the current `$digest` will replace the current history
|
||||
* record, instead of adding a new one.
|
||||
*/
|
||||
replace: function() {
|
||||
this.$$replace = true;
|
||||
@@ -918,7 +918,7 @@ function $LocationProvider() {
|
||||
var oldUrl = $location.absUrl();
|
||||
var oldState = $location.$$state;
|
||||
var defaultPrevented;
|
||||
|
||||
newUrl = trimEmptyHash(newUrl);
|
||||
$location.$$parse(newUrl);
|
||||
$location.$$state = newState;
|
||||
|
||||
|
||||
+3
-1
@@ -1906,13 +1906,14 @@ function $ParseProvider() {
|
||||
function addInterceptor(parsedExpression, interceptorFn) {
|
||||
if (!interceptorFn) return parsedExpression;
|
||||
var watchDelegate = parsedExpression.$$watchDelegate;
|
||||
var useInputs = false;
|
||||
|
||||
var regularWatch =
|
||||
watchDelegate !== oneTimeLiteralWatchDelegate &&
|
||||
watchDelegate !== oneTimeWatchDelegate;
|
||||
|
||||
var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
|
||||
var value = parsedExpression(scope, locals, assign, inputs);
|
||||
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
|
||||
return interceptorFn(value, scope, locals);
|
||||
} : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
|
||||
var value = parsedExpression(scope, locals, assign, inputs);
|
||||
@@ -1930,6 +1931,7 @@ function $ParseProvider() {
|
||||
// If there is an interceptor, but no watchDelegate then treat the interceptor like
|
||||
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
|
||||
fn.$$watchDelegate = inputsWatchDelegate;
|
||||
useInputs = !parsedExpression.inputs;
|
||||
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
*
|
||||
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
|
||||
*
|
||||
* Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise.
|
||||
*
|
||||
* However, the more traditional CommonJS-style usage is still available, and documented below.
|
||||
*
|
||||
* [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
|
||||
|
||||
+32
-16
@@ -14,15 +14,15 @@
|
||||
* exposed as $$____ properties
|
||||
*
|
||||
* Loop operations are optimized by using while(count--) { ... }
|
||||
* - this means that in order to keep the same order of execution as addition we have to add
|
||||
* - This means that in order to keep the same order of execution as addition we have to add
|
||||
* items to the array at the beginning (unshift) instead of at the end (push)
|
||||
*
|
||||
* Child scopes are created and removed often
|
||||
* - Using an array would be slow since inserts in middle are expensive so we use linked list
|
||||
* - Using an array would be slow since inserts in the middle are expensive; so we use linked lists
|
||||
*
|
||||
* There are few watches then a lot of observers. This is why you don't want the observer to be
|
||||
* implemented in the same way as watch. Watch requires return of initialization function which
|
||||
* are expensive to construct.
|
||||
* There are fewer watches than observers. This is why you don't want the observer to be implemented
|
||||
* in the same way as watch. Watch requires return of the initialization function which is expensive
|
||||
* to construct.
|
||||
*/
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
* Every application has a single root {@link ng.$rootScope.Scope scope}.
|
||||
* All other scopes are descendant scopes of the root scope. Scopes provide separation
|
||||
* between the model and the view, via a mechanism for watching the model for changes.
|
||||
* They also provide an event emission/broadcast and subscription facility. See the
|
||||
* They also provide event emission/broadcast and subscription facility. See the
|
||||
* {@link guide/scope developer guide on scopes}.
|
||||
*/
|
||||
function $RootScopeProvider() {
|
||||
@@ -101,6 +101,29 @@ function $RootScopeProvider() {
|
||||
$event.currentScope.$$destroyed = true;
|
||||
}
|
||||
|
||||
function cleanUpScope($scope) {
|
||||
|
||||
if (msie === 9) {
|
||||
// There is a memory leak in IE9 if all child scopes are not disconnected
|
||||
// completely when a scope is destroyed. So this code will recurse up through
|
||||
// all this scopes children
|
||||
//
|
||||
// See issue https://github.com/angular/angular.js/issues/10706
|
||||
$scope.$$childHead && cleanUpScope($scope.$$childHead);
|
||||
$scope.$$nextSibling && cleanUpScope($scope.$$nextSibling);
|
||||
}
|
||||
|
||||
// The code below works around IE9 and V8's memory leaks
|
||||
//
|
||||
// See:
|
||||
// - https://code.google.com/p/v8/issues/detail?id=2073#c26
|
||||
// - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
|
||||
// - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
|
||||
|
||||
$scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead =
|
||||
$scope.$$childTail = $scope.$root = $scope.$$watchers = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc type
|
||||
* @name $rootScope.Scope
|
||||
@@ -897,16 +920,9 @@ function $RootScopeProvider() {
|
||||
this.$on = this.$watch = this.$watchGroup = function() { return noop; };
|
||||
this.$$listeners = {};
|
||||
|
||||
// All of the code below is bogus code that works around V8's memory leak via optimized code
|
||||
// and inline caches.
|
||||
//
|
||||
// see:
|
||||
// - https://code.google.com/p/v8/issues/detail?id=2073#c26
|
||||
// - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
|
||||
// - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
|
||||
|
||||
this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
|
||||
this.$$childTail = this.$root = this.$$watchers = null;
|
||||
// Disconnect the next sibling to prevent `cleanUpScope` destroying those too
|
||||
this.$$nextSibling = null;
|
||||
cleanUpScope(this);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -484,7 +484,7 @@ function $SceDelegateProvider() {
|
||||
* By default, Angular only loads templates from the same domain and protocol as the application
|
||||
* document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
|
||||
* $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or
|
||||
* protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
|
||||
* protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
|
||||
* them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
|
||||
*
|
||||
* *Please note*:
|
||||
|
||||
@@ -186,6 +186,8 @@ var ANIMATE_TIMER_KEY = '$$animateCss';
|
||||
*
|
||||
* * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied
|
||||
* to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.)
|
||||
* * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and
|
||||
* `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted.
|
||||
* * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both).
|
||||
* * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`).
|
||||
* * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`).
|
||||
|
||||
@@ -139,8 +139,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
return mergeAnimationOptions(element, options, {});
|
||||
}
|
||||
|
||||
function findCallbacks(element, event) {
|
||||
function findCallbacks(parent, element, event) {
|
||||
var targetNode = getDomNode(element);
|
||||
var targetParentNode = getDomNode(parent);
|
||||
|
||||
var matches = [];
|
||||
var entries = callbackRegistry[event];
|
||||
@@ -148,6 +149,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
forEach(entries, function(entry) {
|
||||
if (entry.node.contains(targetNode)) {
|
||||
matches.push(entry.callback);
|
||||
} else if (event === 'leave' && entry.node.contains(targetParentNode)) {
|
||||
matches.push(entry.callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -473,7 +476,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
|
||||
function notifyProgress(runner, event, phase, data) {
|
||||
runInNextPostDigestOrNow(function() {
|
||||
var callbacks = findCallbacks(element, event);
|
||||
var callbacks = findCallbacks(parent, element, event);
|
||||
if (callbacks.length) {
|
||||
// do not optimize this call here to RAF because
|
||||
// we don't know how heavy the callback code here will
|
||||
|
||||
+11
-14
@@ -290,7 +290,7 @@
|
||||
* jQuery(element).fadeOut(1000, doneFn);
|
||||
* }
|
||||
* }
|
||||
* }]
|
||||
* }]);
|
||||
* ```
|
||||
*
|
||||
* The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as
|
||||
@@ -321,7 +321,7 @@
|
||||
* // do some cool animation and call the doneFn
|
||||
* }
|
||||
* }
|
||||
* }]
|
||||
* }]);
|
||||
* ```
|
||||
*
|
||||
* ## CSS + JS Animations Together
|
||||
@@ -343,7 +343,7 @@
|
||||
* jQuery(element).slideIn(1000, doneFn);
|
||||
* }
|
||||
* }
|
||||
* }]
|
||||
* }]);
|
||||
* ```
|
||||
*
|
||||
* ```css
|
||||
@@ -363,16 +363,15 @@
|
||||
* ```js
|
||||
* myModule.animation('.slide', ['$animateCss', function($animateCss) {
|
||||
* return {
|
||||
* enter: function(element, doneFn) {
|
||||
* enter: function(element) {
|
||||
* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
|
||||
* var runner = $animateCss(element, {
|
||||
* return $animateCss(element, {
|
||||
* event: 'enter',
|
||||
* structural: true
|
||||
* }).start();
|
||||
* runner.done(doneFn);
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* }]
|
||||
* }]);
|
||||
* ```
|
||||
*
|
||||
* The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework.
|
||||
@@ -384,19 +383,17 @@
|
||||
* ```js
|
||||
* myModule.animation('.slide', ['$animateCss', function($animateCss) {
|
||||
* return {
|
||||
* enter: function(element, doneFn) {
|
||||
* var runner = $animateCss(element, {
|
||||
* enter: function(element) {
|
||||
* return $animateCss(element, {
|
||||
* event: 'enter',
|
||||
* structural: true,
|
||||
* addClass: 'maroon-setting',
|
||||
* from: { height:0 },
|
||||
* to: { height: 200 }
|
||||
* }).start();
|
||||
*
|
||||
* runner.done(doneFn);
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* }]
|
||||
* }]);
|
||||
* ```
|
||||
*
|
||||
* Now we can fill in the rest via our transition CSS code:
|
||||
|
||||
+2
-1
@@ -235,7 +235,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
}
|
||||
},
|
||||
post: function(scope, elem, attr, ngModel) {
|
||||
var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem);
|
||||
var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem)
|
||||
&& !isNodeOneOf(elem, nodeBlackList);
|
||||
|
||||
function ngAriaWatchModelValue() {
|
||||
return ngModel.$modelValue;
|
||||
|
||||
Vendored
+3
-2
@@ -2271,8 +2271,9 @@ if (window.jasmine || window.mocha) {
|
||||
* @param {...(string|Function|Object)} fns any number of modules which are represented as string
|
||||
* aliases or as anonymous module initialization functions. The modules are used to
|
||||
* configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
|
||||
* object literal is passed they will be registered as values in the module, the key being
|
||||
* the module name and the value being what is returned.
|
||||
* object literal is passed each key-value pair will be registered on the module via
|
||||
* {@link auto.$provide $provide}.value, the key being the string name (or token) to associate
|
||||
* with the value on the injector.
|
||||
*/
|
||||
window.module = angular.mock.module = function() {
|
||||
var moduleFns = Array.prototype.slice.call(arguments, 0);
|
||||
|
||||
@@ -568,8 +568,17 @@ angular.module('ngResource', ['ng']).
|
||||
undefined;
|
||||
|
||||
forEach(action, function(value, key) {
|
||||
if (key != 'params' && key != 'isArray' && key != 'interceptor') {
|
||||
httpConfig[key] = copy(value);
|
||||
switch (key) {
|
||||
default:
|
||||
httpConfig[key] = copy(value);
|
||||
break;
|
||||
case 'params':
|
||||
case 'isArray':
|
||||
case 'interceptor':
|
||||
break;
|
||||
case 'timeout':
|
||||
httpConfig[key] = value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
if (!element) return;
|
||||
|
||||
eventData = eventData || {};
|
||||
var relatedTarget = eventData.relatedTarget || element;
|
||||
var keys = eventData.keys;
|
||||
var x = eventData.x;
|
||||
var y = eventData.y;
|
||||
@@ -84,7 +85,7 @@
|
||||
x = x || 0;
|
||||
y = y || 0;
|
||||
evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'),
|
||||
pressed('alt'), pressed('shift'), pressed('meta'), 0, element);
|
||||
pressed('alt'), pressed('shift'), pressed('meta'), 0, relatedTarget);
|
||||
}
|
||||
|
||||
/* we're unable to change the timeStamp value directly so this
|
||||
|
||||
@@ -313,11 +313,19 @@ describe('angular', function() {
|
||||
it('should throw an exception if a Scope is being copied', inject(function($rootScope) {
|
||||
expect(function() { copy($rootScope.$new()); }).
|
||||
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
expect(function() { copy({child: $rootScope.$new()}, {}); }).
|
||||
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
expect(function() { copy([$rootScope.$new()]); }).
|
||||
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
}));
|
||||
|
||||
it('should throw an exception if a Window is being copied', function() {
|
||||
expect(function() { copy(window); }).
|
||||
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
expect(function() { copy({child: window}); }).
|
||||
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
expect(function() { copy([window], []); }).
|
||||
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
|
||||
});
|
||||
|
||||
it('should throw an exception when source and destination are equivalent', function() {
|
||||
@@ -334,6 +342,11 @@ describe('angular', function() {
|
||||
hashKey(src);
|
||||
dst = copy(src);
|
||||
expect(hashKey(dst)).not.toEqual(hashKey(src));
|
||||
|
||||
src = {foo: {}};
|
||||
hashKey(src.foo);
|
||||
dst = copy(src);
|
||||
expect(hashKey(src.foo)).not.toEqual(hashKey(dst.foo));
|
||||
});
|
||||
|
||||
it('should retain the previous $$hashKey when copying object with hashKey', function() {
|
||||
@@ -461,6 +474,7 @@ describe('angular', function() {
|
||||
});
|
||||
|
||||
describe("extend", function() {
|
||||
|
||||
it('should not copy the private $$hashKey', function() {
|
||||
var src,dst;
|
||||
src = {};
|
||||
@@ -471,6 +485,24 @@ describe('angular', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should copy the properties of the source object onto the destination object', function() {
|
||||
var destination, source;
|
||||
destination = {};
|
||||
source = {foo: true};
|
||||
destination = extend(destination, source);
|
||||
expect(isDefined(destination.foo)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('ISSUE #4751 - should copy the length property of an object source to the destination object', function() {
|
||||
var destination, source;
|
||||
destination = {};
|
||||
source = {radius: 30, length: 0};
|
||||
destination = extend(destination, source);
|
||||
expect(isDefined(destination.length)).toBe(true);
|
||||
expect(isDefined(destination.radius)).toBe(true);
|
||||
});
|
||||
|
||||
it('should retain the previous $$hashKey', function() {
|
||||
var src,dst,h;
|
||||
src = {};
|
||||
@@ -503,6 +535,17 @@ describe('angular', function() {
|
||||
|
||||
expect(dst.date).toBe(src.date);
|
||||
});
|
||||
|
||||
it('should copy elements by reference', function() {
|
||||
var src = { element: document.createElement('div'),
|
||||
jqObject: jqLite("<p><span>s1</span><span>s2</span></p>").find("span") };
|
||||
var dst = {};
|
||||
|
||||
extend(dst, src);
|
||||
|
||||
expect(dst.element).toBe(src.element);
|
||||
expect(dst.jqObject).toBe(src.jqObject);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -593,6 +636,25 @@ describe('angular', function() {
|
||||
expect(isRegExp(dst.regexp)).toBe(true);
|
||||
expect(dst.regexp.toString()).toBe(src.regexp.toString());
|
||||
});
|
||||
|
||||
|
||||
it('should copy(clone) elements', function() {
|
||||
var src = {
|
||||
element: document.createElement('div'),
|
||||
jqObject: jqLite('<p><span>s1</span><span>s2</span></p>').find('span')
|
||||
};
|
||||
var dst = {};
|
||||
|
||||
merge(dst, src);
|
||||
|
||||
expect(dst.element).not.toBe(src.element);
|
||||
expect(dst.jqObject).not.toBe(src.jqObject);
|
||||
|
||||
expect(isElement(dst.element)).toBeTruthy();
|
||||
expect(dst.element.nodeName).toBeDefined(); // i.e it is a DOM element
|
||||
expect(isElement(dst.jqObject)).toBeTruthy();
|
||||
expect(dst.jqObject.nodeName).toBeUndefined(); // i.e it is a jqLite/jQuery object
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1035,6 +1097,42 @@ describe('angular', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isArrayLike', function() {
|
||||
|
||||
it('should return false if passed a number', function() {
|
||||
expect(isArrayLike(10)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if passed an array', function() {
|
||||
expect(isArrayLike([1,2,3,4])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if passed an object', function() {
|
||||
expect(isArrayLike({0:"test", 1:"bob", 2:"tree", length:3})).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if passed arguments object', function() {
|
||||
function test(a,b,c) {
|
||||
expect(isArrayLike(arguments)).toBe(true);
|
||||
}
|
||||
test(1,2,3);
|
||||
});
|
||||
|
||||
it('should return true if passed a nodelist', function() {
|
||||
var nodes = document.body.childNodes;
|
||||
expect(isArrayLike(nodes)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for objects with `length` but no matching indexable items', function() {
|
||||
var obj = {
|
||||
a: 'a',
|
||||
b:'b',
|
||||
length: 10
|
||||
};
|
||||
expect(isArrayLike(obj)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('forEach', function() {
|
||||
it('should iterate over *own* object properties', function() {
|
||||
@@ -1074,6 +1172,11 @@ describe('angular', function() {
|
||||
|
||||
forEach(jqObject, function(value, key) { log.push(key + ':' + value.innerHTML); });
|
||||
expect(log).toEqual(['0:s1', '1:s2']);
|
||||
|
||||
log = [];
|
||||
jqObject = jqLite("<pane></pane>");
|
||||
forEach(jqObject.children(), function(value, key) { log.push(key + ':' + value.innerHTML); });
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global jQuery: true, uid: true */
|
||||
/* global jQuery: true, uid: true, jqCache: true */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
@@ -12,6 +12,7 @@ if (window._jQuery) _jQuery.event.special.change = undefined;
|
||||
if (window.bindJQuery) bindJQuery();
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
// all this stuff is not needed for module tests, where jqlite and publishExternalAPI and jqLite are not global vars
|
||||
if (window.publishExternalAPI) {
|
||||
publishExternalAPI(angular);
|
||||
@@ -28,7 +29,10 @@ beforeEach(function() {
|
||||
|
||||
// reset to jQuery or default to us.
|
||||
bindJQuery();
|
||||
jqLiteCacheSizeInit();
|
||||
|
||||
// Clear the cache to prevent memory leak failures from previous tests
|
||||
// breaking subsequent tests unnecessarily
|
||||
jqCache = jqLite.cache = {};
|
||||
}
|
||||
|
||||
angular.element(document.body).empty().removeData();
|
||||
@@ -84,7 +88,6 @@ afterEach(function() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// copied from Angular.js
|
||||
// we need this method here so that we can run module tests with wrapped angular.js
|
||||
function forEachSorted(obj, iterator, context) {
|
||||
@@ -133,14 +136,7 @@ function dealoc(obj) {
|
||||
|
||||
|
||||
function jqLiteCacheSize() {
|
||||
var size = 0;
|
||||
forEach(jqLite.cache, function() { size++; });
|
||||
return size - jqLiteCacheSize.initSize;
|
||||
}
|
||||
jqLiteCacheSize.initSize = 0;
|
||||
|
||||
function jqLiteCacheSizeInit() {
|
||||
jqLiteCacheSize.initSize = jqLiteCacheSize.initSize + jqLiteCacheSize();
|
||||
return Object.keys(jqLite.cache).length;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+108
-30
@@ -1193,21 +1193,45 @@ describe('jqLite', function() {
|
||||
});
|
||||
|
||||
describe('mouseenter-mouseleave', function() {
|
||||
var root, parent, sibling, child, log;
|
||||
var root, parent, child, log;
|
||||
|
||||
beforeEach(function() {
|
||||
function setup(html, parentNode, childNode) {
|
||||
log = '';
|
||||
root = jqLite('<div>root<p>parent<span>child</span></p><ul></ul></div>');
|
||||
parent = root.find('p');
|
||||
sibling = root.find('ul');
|
||||
child = parent.find('span');
|
||||
root = jqLite(html);
|
||||
parent = root.find(parentNode);
|
||||
child = parent.find(childNode);
|
||||
|
||||
parent.on('mouseenter', function() { log += 'parentEnter;'; });
|
||||
parent.on('mouseleave', function() { log += 'parentLeave;'; });
|
||||
|
||||
child.on('mouseenter', function() { log += 'childEnter;'; });
|
||||
child.on('mouseleave', function() { log += 'childLeave;'; });
|
||||
});
|
||||
}
|
||||
|
||||
function browserMoveTrigger(from, to) {
|
||||
var fireEvent = function(type, element, relatedTarget) {
|
||||
var evnt;
|
||||
evnt = document.createEvent('MouseEvents');
|
||||
|
||||
var originalPreventDefault = evnt.preventDefault,
|
||||
appWindow = window,
|
||||
fakeProcessDefault = true,
|
||||
finalProcessDefault;
|
||||
|
||||
evnt.preventDefault = function() {
|
||||
fakeProcessDefault = false;
|
||||
return originalPreventDefault.apply(evnt, arguments);
|
||||
};
|
||||
|
||||
var x = 0, y = 0;
|
||||
evnt.initMouseEvent(type, true, true, window, 0, x, y, x, y, false, false,
|
||||
false, false, 0, relatedTarget);
|
||||
|
||||
element.dispatchEvent(evnt);
|
||||
};
|
||||
fireEvent('mouseout', from[0], to[0]);
|
||||
fireEvent('mouseover', to[0], from[0]);
|
||||
}
|
||||
|
||||
afterEach(function() {
|
||||
dealoc(root);
|
||||
@@ -1215,30 +1239,8 @@ describe('jqLite', function() {
|
||||
|
||||
it('should fire mouseenter when coming from outside the browser window', function() {
|
||||
if (window.jQuery) return;
|
||||
var browserMoveTrigger = function(from, to) {
|
||||
var fireEvent = function(type, element, relatedTarget) {
|
||||
var evnt;
|
||||
evnt = document.createEvent('MouseEvents');
|
||||
|
||||
var originalPreventDefault = evnt.preventDefault,
|
||||
appWindow = window,
|
||||
fakeProcessDefault = true,
|
||||
finalProcessDefault;
|
||||
|
||||
evnt.preventDefault = function() {
|
||||
fakeProcessDefault = false;
|
||||
return originalPreventDefault.apply(evnt, arguments);
|
||||
};
|
||||
|
||||
var x = 0, y = 0;
|
||||
evnt.initMouseEvent(type, true, true, window, 0, x, y, x, y, false, false,
|
||||
false, false, 0, relatedTarget);
|
||||
|
||||
element.dispatchEvent(evnt);
|
||||
};
|
||||
fireEvent('mouseout', from[0], to[0]);
|
||||
fireEvent('mouseover', to[0], from[0]);
|
||||
};
|
||||
setup('<div>root<p>parent<span>child</span></p><ul></ul></div>', 'p', 'span');
|
||||
|
||||
browserMoveTrigger(root, parent);
|
||||
expect(log).toEqual('parentEnter;');
|
||||
@@ -1253,6 +1255,28 @@ describe('jqLite', function() {
|
||||
expect(log).toEqual('parentEnter;childEnter;childLeave;parentLeave;');
|
||||
|
||||
});
|
||||
|
||||
it('should fire the mousenter on SVG elements', function() {
|
||||
if (window.jQuery) return;
|
||||
|
||||
setup(
|
||||
'<div>' +
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"' +
|
||||
' viewBox="0 0 18.75 18.75"' +
|
||||
' width="18.75"' +
|
||||
' height="18.75"' +
|
||||
' version="1.1">' +
|
||||
' <path d="M0,0c0,4.142,3.358,7.5,7.5,7.5s7.5-3.358,7.5-7.5-3.358-7.5-7.5-7.5-7.5,3.358-7.5,7.5"' +
|
||||
' fill-rule="nonzero"' +
|
||||
' fill="#CCC"' +
|
||||
' ng-attr-fill="{{data.color || \'#CCC\'}}"/>' +
|
||||
'</svg>' +
|
||||
'</div>',
|
||||
'svg', 'path');
|
||||
|
||||
browserMoveTrigger(parent, child);
|
||||
expect(log).toEqual('childEnter;');
|
||||
});
|
||||
});
|
||||
|
||||
// Only run this test for jqLite and not normal jQuery
|
||||
@@ -1407,6 +1431,60 @@ describe('jqLite', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should correctly deregister the mouseenter/mouseleave listeners', function() {
|
||||
var aElem = jqLite(a);
|
||||
var onMouseenter = jasmine.createSpy('onMouseenter');
|
||||
var onMouseleave = jasmine.createSpy('onMouseleave');
|
||||
|
||||
aElem.on('mouseenter', onMouseenter);
|
||||
aElem.on('mouseleave', onMouseleave);
|
||||
aElem.off('mouseenter', onMouseenter);
|
||||
aElem.off('mouseleave', onMouseleave);
|
||||
aElem.on('mouseenter', onMouseenter);
|
||||
aElem.on('mouseleave', onMouseleave);
|
||||
|
||||
browserTrigger(a, 'mouseover', {relatedTarget: b});
|
||||
expect(onMouseenter).toHaveBeenCalledOnce();
|
||||
|
||||
browserTrigger(a, 'mouseout', {relatedTarget: b});
|
||||
expect(onMouseleave).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
it('should call a `mouseenter/leave` listener only once when `mouseenter/leave` and `mouseover/out` '
|
||||
+ 'are triggered simultaneously', function() {
|
||||
var aElem = jqLite(a);
|
||||
var onMouseenter = jasmine.createSpy('mouseenter');
|
||||
var onMouseleave = jasmine.createSpy('mouseleave');
|
||||
|
||||
aElem.on('mouseenter', onMouseenter);
|
||||
aElem.on('mouseleave', onMouseleave);
|
||||
|
||||
browserTrigger(a, 'mouseenter', {relatedTarget: b});
|
||||
browserTrigger(a, 'mouseover', {relatedTarget: b});
|
||||
expect(onMouseenter).toHaveBeenCalledOnce();
|
||||
|
||||
browserTrigger(a, 'mouseleave', {relatedTarget: b});
|
||||
browserTrigger(a, 'mouseout', {relatedTarget: b});
|
||||
expect(onMouseleave).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call a `mouseenter/leave` listener when manually triggering the event', function() {
|
||||
var aElem = jqLite(a);
|
||||
var onMouseenter = jasmine.createSpy('mouseenter');
|
||||
var onMouseleave = jasmine.createSpy('mouseleave');
|
||||
|
||||
aElem.on('mouseenter', onMouseenter);
|
||||
aElem.on('mouseleave', onMouseleave);
|
||||
|
||||
aElem.triggerHandler('mouseenter');
|
||||
expect(onMouseenter).toHaveBeenCalledOnce();
|
||||
|
||||
aElem.triggerHandler('mouseleave');
|
||||
expect(onMouseleave).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
it('should deregister specific listener within the listener and call subsequent listeners', function() {
|
||||
var aElem = jqLite(a),
|
||||
clickSpy = jasmine.createSpy('click'),
|
||||
|
||||
@@ -133,6 +133,19 @@ describe('$cacheFactory', function() {
|
||||
expect(cache.info().size).toBe(0);
|
||||
}));
|
||||
|
||||
it('should only decrement size when an element is actually removed via remove', inject(function($cacheFactory) {
|
||||
cache.put('foo', 'bar');
|
||||
expect(cache.info().size).toBe(1);
|
||||
|
||||
cache.remove('undefined');
|
||||
expect(cache.info().size).toBe(1);
|
||||
|
||||
cache.remove('hasOwnProperty');
|
||||
expect(cache.info().size).toBe(1);
|
||||
|
||||
cache.remove('foo');
|
||||
expect(cache.info().size).toBe(0);
|
||||
}));
|
||||
|
||||
it('should return cache id', inject(function($cacheFactory) {
|
||||
expect(cache.info().id).toBe('test');
|
||||
|
||||
@@ -947,6 +947,14 @@ describe('$compile', function() {
|
||||
expect(child).toHaveClass('log'); // merged from replace directive template
|
||||
}));
|
||||
|
||||
it('should interpolate the values once per digest',
|
||||
inject(function($compile, $rootScope, log) {
|
||||
element = $compile('<div>{{log("A")}} foo {{::log("B")}}</div>')($rootScope);
|
||||
$rootScope.log = log;
|
||||
$rootScope.$digest();
|
||||
expect(log).toEqual('A; B; A; B');
|
||||
}));
|
||||
|
||||
it('should update references to replaced jQuery context', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('foo', function() {
|
||||
@@ -4406,6 +4414,301 @@ describe('$compile', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should bind to multiple directives controllers via object notation (no scope)', function() {
|
||||
var controller1Called = false;
|
||||
var controller2Called = false;
|
||||
module(function($compileProvider, $controllerProvider) {
|
||||
$compileProvider.directive('foo', valueFn({
|
||||
bindToController: {
|
||||
'data': '=fooData',
|
||||
'str': '@fooStr',
|
||||
'fn': '&fooFn'
|
||||
},
|
||||
controllerAs: 'fooCtrl',
|
||||
controller: function() {
|
||||
expect(this.data).toEqualData({'foo': 'bar', 'baz': 'biz'});
|
||||
expect(this.str).toBe('Hello, world!');
|
||||
expect(this.fn()).toBe('called!');
|
||||
controller1Called = true;
|
||||
}
|
||||
}));
|
||||
$compileProvider.directive('bar', valueFn({
|
||||
bindToController: {
|
||||
'data': '=barData',
|
||||
'str': '@barStr',
|
||||
'fn': '&barFn'
|
||||
},
|
||||
controllerAs: 'barCtrl',
|
||||
controller: function() {
|
||||
expect(this.data).toEqualData({'foo2': 'bar2', 'baz2': 'biz2'});
|
||||
expect(this.str).toBe('Hello, second world!');
|
||||
expect(this.fn()).toBe('second called!');
|
||||
controller2Called = true;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$rootScope.fn = valueFn('called!');
|
||||
$rootScope.string = 'world';
|
||||
$rootScope.data = {'foo': 'bar','baz': 'biz'};
|
||||
$rootScope.fn2 = valueFn('second called!');
|
||||
$rootScope.string2 = 'second world';
|
||||
$rootScope.data2 = {'foo2': 'bar2', 'baz2': 'biz2'};
|
||||
element = $compile(
|
||||
'<div ' +
|
||||
'foo ' +
|
||||
'foo-data="data" ' +
|
||||
'foo-str="Hello, {{string}}!" ' +
|
||||
'foo-fn="fn()" ' +
|
||||
'bar ' +
|
||||
'bar-data="data2" ' +
|
||||
'bar-str="Hello, {{string2}}!" ' +
|
||||
'bar-fn="fn2()" > ' +
|
||||
'</div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(controller1Called).toBe(true);
|
||||
expect(controller2Called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should bind to multiple directives controllers via object notation (new iso scope)', function() {
|
||||
var controller1Called = false;
|
||||
var controller2Called = false;
|
||||
module(function($compileProvider, $controllerProvider) {
|
||||
$compileProvider.directive('foo', valueFn({
|
||||
bindToController: {
|
||||
'data': '=fooData',
|
||||
'str': '@fooStr',
|
||||
'fn': '&fooFn'
|
||||
},
|
||||
scope: {},
|
||||
controllerAs: 'fooCtrl',
|
||||
controller: function() {
|
||||
expect(this.data).toEqualData({'foo': 'bar', 'baz': 'biz'});
|
||||
expect(this.str).toBe('Hello, world!');
|
||||
expect(this.fn()).toBe('called!');
|
||||
controller1Called = true;
|
||||
}
|
||||
}));
|
||||
$compileProvider.directive('bar', valueFn({
|
||||
bindToController: {
|
||||
'data': '=barData',
|
||||
'str': '@barStr',
|
||||
'fn': '&barFn'
|
||||
},
|
||||
controllerAs: 'barCtrl',
|
||||
controller: function() {
|
||||
expect(this.data).toEqualData({'foo2': 'bar2', 'baz2': 'biz2'});
|
||||
expect(this.str).toBe('Hello, second world!');
|
||||
expect(this.fn()).toBe('second called!');
|
||||
controller2Called = true;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$rootScope.fn = valueFn('called!');
|
||||
$rootScope.string = 'world';
|
||||
$rootScope.data = {'foo': 'bar','baz': 'biz'};
|
||||
$rootScope.fn2 = valueFn('second called!');
|
||||
$rootScope.string2 = 'second world';
|
||||
$rootScope.data2 = {'foo2': 'bar2', 'baz2': 'biz2'};
|
||||
element = $compile(
|
||||
'<div ' +
|
||||
'foo ' +
|
||||
'foo-data="data" ' +
|
||||
'foo-str="Hello, {{string}}!" ' +
|
||||
'foo-fn="fn()" ' +
|
||||
'bar ' +
|
||||
'bar-data="data2" ' +
|
||||
'bar-str="Hello, {{string2}}!" ' +
|
||||
'bar-fn="fn2()" > ' +
|
||||
'</div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(controller1Called).toBe(true);
|
||||
expect(controller2Called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should bind to multiple directives controllers via object notation (new scope)', function() {
|
||||
var controller1Called = false;
|
||||
var controller2Called = false;
|
||||
module(function($compileProvider, $controllerProvider) {
|
||||
$compileProvider.directive('foo', valueFn({
|
||||
bindToController: {
|
||||
'data': '=fooData',
|
||||
'str': '@fooStr',
|
||||
'fn': '&fooFn'
|
||||
},
|
||||
scope: true,
|
||||
controllerAs: 'fooCtrl',
|
||||
controller: function() {
|
||||
expect(this.data).toEqualData({'foo': 'bar', 'baz': 'biz'});
|
||||
expect(this.str).toBe('Hello, world!');
|
||||
expect(this.fn()).toBe('called!');
|
||||
controller1Called = true;
|
||||
}
|
||||
}));
|
||||
$compileProvider.directive('bar', valueFn({
|
||||
bindToController: {
|
||||
'data': '=barData',
|
||||
'str': '@barStr',
|
||||
'fn': '&barFn'
|
||||
},
|
||||
scope: true,
|
||||
controllerAs: 'barCtrl',
|
||||
controller: function() {
|
||||
expect(this.data).toEqualData({'foo2': 'bar2', 'baz2': 'biz2'});
|
||||
expect(this.str).toBe('Hello, second world!');
|
||||
expect(this.fn()).toBe('second called!');
|
||||
controller2Called = true;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$rootScope.fn = valueFn('called!');
|
||||
$rootScope.string = 'world';
|
||||
$rootScope.data = {'foo': 'bar','baz': 'biz'};
|
||||
$rootScope.fn2 = valueFn('second called!');
|
||||
$rootScope.string2 = 'second world';
|
||||
$rootScope.data2 = {'foo2': 'bar2', 'baz2': 'biz2'};
|
||||
element = $compile(
|
||||
'<div ' +
|
||||
'foo ' +
|
||||
'foo-data="data" ' +
|
||||
'foo-str="Hello, {{string}}!" ' +
|
||||
'foo-fn="fn()" ' +
|
||||
'bar ' +
|
||||
'bar-data="data2" ' +
|
||||
'bar-str="Hello, {{string2}}!" ' +
|
||||
'bar-fn="fn2()" > ' +
|
||||
'</div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(controller1Called).toBe(true);
|
||||
expect(controller2Called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should evaluate against the correct scope, when using `bindToController` (new scope)',
|
||||
function() {
|
||||
module(function($compileProvider, $controllerProvider) {
|
||||
$controllerProvider.register({
|
||||
'ParentCtrl': function() {
|
||||
this.value1 = 'parent1';
|
||||
this.value2 = 'parent2';
|
||||
this.value3 = function() { return 'parent3'; };
|
||||
},
|
||||
'ChildCtrl': function() {
|
||||
this.value1 = 'child1';
|
||||
this.value2 = 'child2';
|
||||
this.value3 = function() { return 'child3'; };
|
||||
}
|
||||
});
|
||||
|
||||
$compileProvider.directive('child', valueFn({
|
||||
scope: true,
|
||||
controller: 'ChildCtrl as ctrl',
|
||||
bindToController: {
|
||||
fromParent1: '@',
|
||||
fromParent2: '=',
|
||||
fromParent3: '&'
|
||||
},
|
||||
template: ''
|
||||
}));
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
element = $compile(
|
||||
'<div ng-controller="ParentCtrl as ctrl">' +
|
||||
'<child ' +
|
||||
'from-parent-1="{{ ctrl.value1 }}" ' +
|
||||
'from-parent-2="ctrl.value2" ' +
|
||||
'from-parent-3="ctrl.value3">' +
|
||||
'</child>' +
|
||||
'</div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var parentCtrl = element.controller('ngController');
|
||||
var childCtrl = element.find('child').controller('child');
|
||||
|
||||
expect(childCtrl.fromParent1).toBe(parentCtrl.value1);
|
||||
expect(childCtrl.fromParent1).not.toBe(childCtrl.value1);
|
||||
expect(childCtrl.fromParent2).toBe(parentCtrl.value2);
|
||||
expect(childCtrl.fromParent2).not.toBe(childCtrl.value2);
|
||||
expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3());
|
||||
expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3());
|
||||
|
||||
childCtrl.fromParent2 = 'modified';
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(parentCtrl.value2).toBe('modified');
|
||||
expect(childCtrl.value2).toBe('child2');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should evaluate against the correct scope, when using `bindToController` (new iso scope)',
|
||||
function() {
|
||||
module(function($compileProvider, $controllerProvider) {
|
||||
$controllerProvider.register({
|
||||
'ParentCtrl': function() {
|
||||
this.value1 = 'parent1';
|
||||
this.value2 = 'parent2';
|
||||
this.value3 = function() { return 'parent3'; };
|
||||
},
|
||||
'ChildCtrl': function() {
|
||||
this.value1 = 'child1';
|
||||
this.value2 = 'child2';
|
||||
this.value3 = function() { return 'child3'; };
|
||||
}
|
||||
});
|
||||
|
||||
$compileProvider.directive('child', valueFn({
|
||||
scope: {},
|
||||
controller: 'ChildCtrl as ctrl',
|
||||
bindToController: {
|
||||
fromParent1: '@',
|
||||
fromParent2: '=',
|
||||
fromParent3: '&'
|
||||
},
|
||||
template: ''
|
||||
}));
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope) {
|
||||
element = $compile(
|
||||
'<div ng-controller="ParentCtrl as ctrl">' +
|
||||
'<child ' +
|
||||
'from-parent-1="{{ ctrl.value1 }}" ' +
|
||||
'from-parent-2="ctrl.value2" ' +
|
||||
'from-parent-3="ctrl.value3">' +
|
||||
'</child>' +
|
||||
'</div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var parentCtrl = element.controller('ngController');
|
||||
var childCtrl = element.find('child').controller('child');
|
||||
|
||||
expect(childCtrl.fromParent1).toBe(parentCtrl.value1);
|
||||
expect(childCtrl.fromParent1).not.toBe(childCtrl.value1);
|
||||
expect(childCtrl.fromParent2).toBe(parentCtrl.value2);
|
||||
expect(childCtrl.fromParent2).not.toBe(childCtrl.value2);
|
||||
expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3());
|
||||
expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3());
|
||||
|
||||
childCtrl.fromParent2 = 'modified';
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(parentCtrl.value2).toBe('modified');
|
||||
expect(childCtrl.value2).toBe('child2');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should put controller in scope when controller identifier present but not using controllerAs', function() {
|
||||
var controllerCalled = false;
|
||||
var myCtrl;
|
||||
@@ -5628,6 +5931,62 @@ describe('$compile', function() {
|
||||
});
|
||||
});
|
||||
|
||||
//see issue https://github.com/angular/angular.js/issues/12936
|
||||
it('should use the proper scope when it is on the root element of a replaced directive template', function() {
|
||||
module(function() {
|
||||
directive('isolate', valueFn({
|
||||
scope: {},
|
||||
replace: true,
|
||||
template: '<div trans>{{x}}</div>',
|
||||
link: function(scope, element, attr, ctrl) {
|
||||
scope.x = 'iso';
|
||||
}
|
||||
}));
|
||||
directive('trans', valueFn({
|
||||
transclude: 'content',
|
||||
link: function(scope, element, attr, ctrl, $transclude) {
|
||||
$transclude(function(clone) {
|
||||
element.append(clone);
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($rootScope, $compile) {
|
||||
element = $compile('<isolate></isolate>')($rootScope);
|
||||
$rootScope.x = 'root';
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toEqual('iso');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//see issue https://github.com/angular/angular.js/issues/12936
|
||||
it('should use the proper scope when it is on the root element of a replaced directive template with child scope', function() {
|
||||
module(function() {
|
||||
directive('child', valueFn({
|
||||
scope: true,
|
||||
replace: true,
|
||||
template: '<div trans>{{x}}</div>',
|
||||
link: function(scope, element, attr, ctrl) {
|
||||
scope.x = 'child';
|
||||
}
|
||||
}));
|
||||
directive('trans', valueFn({
|
||||
transclude: 'content',
|
||||
link: function(scope, element, attr, ctrl, $transclude) {
|
||||
$transclude(function(clone) {
|
||||
element.append(clone);
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($rootScope, $compile) {
|
||||
element = $compile('<child></child>')($rootScope);
|
||||
$rootScope.x = 'root';
|
||||
$rootScope.$apply();
|
||||
expect(element.text()).toEqual('child');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not leak if two "element" transclusions are on the same element (with debug info)', function() {
|
||||
|
||||
@@ -2537,8 +2537,18 @@ describe('input', function() {
|
||||
describe('URL_REGEXP', function() {
|
||||
/* global URL_REGEXP: false */
|
||||
it('should validate url', function() {
|
||||
// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
|
||||
expect(URL_REGEXP.test('http://server:123/path')).toBe(true);
|
||||
expect(URL_REGEXP.test('https://server:123/path')).toBe(true);
|
||||
expect(URL_REGEXP.test('file:///home/user')).toBe(true);
|
||||
expect(URL_REGEXP.test('mailto:user@example.com?subject=Foo')).toBe(true);
|
||||
expect(URL_REGEXP.test('r2-d2.c3-p0://localhost/foo')).toBe(true);
|
||||
expect(URL_REGEXP.test('abc:/foo')).toBe(true);
|
||||
expect(URL_REGEXP.test('http:')).toBe(false);
|
||||
expect(URL_REGEXP.test('a@B.c')).toBe(false);
|
||||
expect(URL_REGEXP.test('a_B.c')).toBe(false);
|
||||
expect(URL_REGEXP.test('0scheme://example.com')).toBe(false);
|
||||
expect(URL_REGEXP.test('http://example.com:9999/~~``')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
describe('ngOptions', function() {
|
||||
|
||||
var scope, formElement, element, $compile;
|
||||
var scope, formElement, element, $compile, linkLog;
|
||||
|
||||
function compile(html) {
|
||||
formElement = jqLite('<form name="form">' + html + '</form>');
|
||||
@@ -104,6 +104,53 @@ describe('ngOptions', function() {
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(module(function($compileProvider, $provide) {
|
||||
linkLog = [];
|
||||
|
||||
$compileProvider
|
||||
.directive('customSelect', function() {
|
||||
return {
|
||||
restrict: "E",
|
||||
replace: true,
|
||||
scope: {
|
||||
ngModel: '=',
|
||||
options: '='
|
||||
},
|
||||
templateUrl: 'select_template.html',
|
||||
link: function(scope, $element, attributes) {
|
||||
scope.selectable_options = scope.options;
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.directive('oCompileContents', function() {
|
||||
return {
|
||||
link: function(scope, element) {
|
||||
linkLog.push('linkCompileContents');
|
||||
$compile(element.contents())(scope);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$provide.decorator('ngOptionsDirective', function($delegate) {
|
||||
|
||||
var origPreLink = $delegate[0].link.pre;
|
||||
var origPostLink = $delegate[0].link.post;
|
||||
|
||||
$delegate[0].compile = function() {
|
||||
return {
|
||||
pre: origPreLink,
|
||||
post: function() {
|
||||
linkLog.push('linkNgOptions');
|
||||
origPostLink.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return $delegate;
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($rootScope, _$compile_) {
|
||||
scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed
|
||||
$compile = _$compile_;
|
||||
@@ -2110,6 +2157,57 @@ describe('ngOptions', function() {
|
||||
option = element.find('option').eq(0);
|
||||
expect(option.text()).toBe('A');
|
||||
});
|
||||
|
||||
|
||||
it('should be possible to use ngIf in the blank option when values are available upon linking',
|
||||
function() {
|
||||
var options;
|
||||
|
||||
scope.values = [{name: 'A'}];
|
||||
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
|
||||
|
||||
scope.$apply('isBlank = true');
|
||||
|
||||
options = element.find('option');
|
||||
expect(options.length).toBe(2);
|
||||
expect(options.eq(0).val()).toBe('');
|
||||
expect(options.eq(0).text()).toBe('blank');
|
||||
|
||||
scope.$apply('isBlank = false');
|
||||
|
||||
options = element.find('option');
|
||||
expect(options.length).toBe(1);
|
||||
expect(options.eq(0).text()).toBe('A');
|
||||
}
|
||||
);
|
||||
|
||||
it('should not throw when a directive compiles the blank option before ngOptions is linked', function() {
|
||||
expect(function() {
|
||||
createSelect({
|
||||
'o-compile-contents': '',
|
||||
'name': 'select',
|
||||
'ng-model': 'value',
|
||||
'ng-options': 'item for item in items'
|
||||
}, true);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(linkLog).toEqual(['linkCompileContents', 'linkNgOptions']);
|
||||
});
|
||||
|
||||
|
||||
it('should not throw with a directive that replaces', inject(function($templateCache, $httpBackend) {
|
||||
$templateCache.put('select_template.html', '<select ng-options="option as option for option in selectable_options"> <option value="">This is a test</option> </select>');
|
||||
|
||||
scope.options = ['a', 'b', 'c', 'd'];
|
||||
|
||||
expect(function() {
|
||||
element = $compile('<custom-select ng-model="value" options="options"></custom-select>')(scope);
|
||||
scope.$digest();
|
||||
}).not.toThrow();
|
||||
|
||||
dealoc(element);
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -612,6 +612,15 @@ describe('ngRepeat', function() {
|
||||
expect(element.text()).toEqual('misko:m:0|shyam:s:1|frodo:f:2|');
|
||||
});
|
||||
|
||||
it('should expose iterator offset as $index when iterating over objects with length key value 0', function() {
|
||||
element = $compile(
|
||||
'<ul>' +
|
||||
'<li ng-repeat="(key, val) in items">{{key}}:{{val}}:{{$index}}|</li>' +
|
||||
'</ul>')(scope);
|
||||
scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f', 'length':0};
|
||||
scope.$digest();
|
||||
expect(element.text()).toEqual('misko:m:0|shyam:s:1|frodo:f:2|length:0:3|');
|
||||
});
|
||||
|
||||
it('should expose iterator position as $first, $middle and $last when iterating over arrays',
|
||||
function() {
|
||||
|
||||
@@ -140,16 +140,19 @@ describe('Filter: limitTo', function() {
|
||||
|
||||
it('should return an empty array if Y exceeds input length', function() {
|
||||
expect(limitTo(items, '3', 12)).toEqual([]);
|
||||
expect(limitTo(items, 4, '-12')).toEqual([]);
|
||||
expect(limitTo(items, -3, '12')).toEqual([]);
|
||||
expect(limitTo(items, '-4', -12)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty string if Y exceeds input length', function() {
|
||||
expect(limitTo(str, '3', 12)).toEqual("");
|
||||
expect(limitTo(str, 4, '-12')).toEqual("");
|
||||
expect(limitTo(str, -3, '12')).toEqual("");
|
||||
expect(limitTo(str, '-4', -12)).toEqual("");
|
||||
});
|
||||
|
||||
it('should start at 0 if Y is negative and exceeds input length', function() {
|
||||
expect(limitTo(items, 4, '-12')).toEqual(['a', 'b', 'c', 'd']);
|
||||
expect(limitTo(items, '-4', -12)).toEqual(['e', 'f', 'g', 'h']);
|
||||
expect(limitTo(str, 4, '-12')).toEqual("tuvw");
|
||||
expect(limitTo(str, '-4', -12)).toEqual("wxyz");
|
||||
});
|
||||
|
||||
it('should return the entire string beginning from Y if X is positive and X+Y exceeds input length', function() {
|
||||
|
||||
@@ -1391,6 +1391,25 @@ describe('$http', function() {
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
expect(callback.mostRecentCall.args[0]).toBe('RESP-FIRST:V1');
|
||||
});
|
||||
|
||||
|
||||
it('should apply `transformResponse` even if the response data is empty', function(data) {
|
||||
var callback = jasmine.createSpy('transformResponse');
|
||||
var config = {transformResponse: callback};
|
||||
|
||||
$httpBackend.expect('GET', '/url1').respond(200, undefined);
|
||||
$httpBackend.expect('GET', '/url2').respond(200, null);
|
||||
$httpBackend.expect('GET', '/url3').respond(200, '');
|
||||
$http.get('/url1', config);
|
||||
$http.get('/url2', config);
|
||||
$http.get('/url3', config);
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(callback.callCount).toBe(3);
|
||||
expect(callback.calls[0].args[0]).toBe(undefined);
|
||||
expect(callback.calls[1].args[0]).toBe(null);
|
||||
expect(callback.calls[2].args[0]).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2141,6 +2141,31 @@ describe('$location', function() {
|
||||
})
|
||||
);
|
||||
|
||||
it('should fire $locationChangeSuccess when browser location changes to URL which ends with #',
|
||||
inject(function($location, $browser, $rootScope, $log) {
|
||||
$location.url('/somepath');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browser.url()).toEqual('http://server/#/somepath');
|
||||
expect($location.url()).toEqual('/somepath');
|
||||
|
||||
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
|
||||
$log.info('start', newUrl, oldUrl);
|
||||
});
|
||||
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
|
||||
$log.info('after', newUrl, oldUrl);
|
||||
});
|
||||
|
||||
$browser.url('http://server/#');
|
||||
$browser.poll();
|
||||
|
||||
expect($log.info.logs.shift()).
|
||||
toEqual(['start', 'http://server/', 'http://server/#/somepath']);
|
||||
expect($log.info.logs.shift()).
|
||||
toEqual(['after', 'http://server/', 'http://server/#/somepath']);
|
||||
})
|
||||
);
|
||||
|
||||
it('should allow redirect during browser url change',
|
||||
inject(function($location, $browser, $rootScope, $log) {
|
||||
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
|
||||
|
||||
@@ -1211,6 +1211,36 @@ describe('Scope', function() {
|
||||
expect(child.parentModel).toBe('parent');
|
||||
expect(child.childModel).toBe('child');
|
||||
}));
|
||||
|
||||
|
||||
if (msie === 9) {
|
||||
// See issue https://github.com/angular/angular.js/issues/10706
|
||||
it('should completely disconnect all child scopes on IE9', inject(function($rootScope) {
|
||||
var parent = $rootScope.$new(),
|
||||
child1 = parent.$new(),
|
||||
child2 = parent.$new(),
|
||||
grandChild1 = child1.$new(),
|
||||
grandChild2 = child1.$new();
|
||||
|
||||
child1.$destroy();
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(isDisconnected(parent)).toBe(false);
|
||||
expect(isDisconnected(child1)).toBe(true);
|
||||
expect(isDisconnected(child2)).toBe(false);
|
||||
expect(isDisconnected(grandChild1)).toBe(true);
|
||||
expect(isDisconnected(grandChild2)).toBe(true);
|
||||
|
||||
function isDisconnected($scope) {
|
||||
return $scope.$$nextSibling === null &&
|
||||
$scope.$$prevSibling === null &&
|
||||
$scope.$$childHead === null &&
|
||||
$scope.$$childTail === null &&
|
||||
$scope.$root === null &&
|
||||
$scope.$$watchers === null;
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1781,5 +1781,86 @@ describe("animations", function() {
|
||||
expect(isElementRemoved).toBe(true);
|
||||
}));
|
||||
|
||||
it('leave : should trigger a callback for an leave animation',
|
||||
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
|
||||
|
||||
var callbackTriggered = false;
|
||||
$animate.on('leave', jqLite($document[0].body), function() {
|
||||
callbackTriggered = true;
|
||||
});
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
$rootElement.append(element);
|
||||
$animate.leave(element, $rootElement);
|
||||
$rootScope.$digest();
|
||||
|
||||
$$rAF.flush();
|
||||
|
||||
expect(callbackTriggered).toBe(true);
|
||||
}));
|
||||
|
||||
it('leave : should not fire a callback if the element is outside of the given container',
|
||||
inject(function($animate, $rootScope, $$rAF, $rootElement) {
|
||||
|
||||
var callbackTriggered = false;
|
||||
var innerContainer = jqLite('<div></div>');
|
||||
$rootElement.append(innerContainer);
|
||||
|
||||
$animate.on('leave', innerContainer,
|
||||
function(element, phase, data) {
|
||||
callbackTriggered = true;
|
||||
});
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
$rootElement.append(element);
|
||||
$animate.leave(element, $rootElement);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(callbackTriggered).toBe(false);
|
||||
}));
|
||||
|
||||
it('leave : should fire a `start` callback when the animation starts with the matching element',
|
||||
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
|
||||
var capturedState;
|
||||
var capturedElement;
|
||||
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
|
||||
capturedState = phase;
|
||||
capturedElement = element;
|
||||
});
|
||||
|
||||
$rootElement.append(element);
|
||||
$animate.leave(element, $rootElement);
|
||||
$rootScope.$digest();
|
||||
$$rAF.flush();
|
||||
|
||||
expect(capturedState).toBe('start');
|
||||
expect(capturedElement).toBe(element);
|
||||
}));
|
||||
|
||||
it('leave : should fire a `close` callback when the animation ends with the matching element',
|
||||
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
|
||||
var capturedState;
|
||||
var capturedElement;
|
||||
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
|
||||
capturedState = phase;
|
||||
capturedElement = element;
|
||||
});
|
||||
|
||||
$rootElement.append(element);
|
||||
var runner = $animate.leave(element, $rootElement);
|
||||
$rootScope.$digest();
|
||||
runner.end();
|
||||
$$rAF.flush();
|
||||
|
||||
expect(capturedState).toBe('close');
|
||||
expect(capturedElement).toBe(element);
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
+12
-10
@@ -616,16 +616,18 @@ describe('$aria', function() {
|
||||
describe('tabindex', function() {
|
||||
beforeEach(injectScopeAndCompiler);
|
||||
|
||||
it('should not attach to native controls', function() {
|
||||
var element = [
|
||||
$compile("<button ng-click='something'></button>")(scope),
|
||||
$compile("<a ng-href='#/something'>")(scope),
|
||||
$compile("<input ng-model='val'>")(scope),
|
||||
$compile("<textarea ng-model='val'></textarea>")(scope),
|
||||
$compile("<select ng-model='val'></select>")(scope),
|
||||
$compile("<details ng-model='val'></details>")(scope)
|
||||
];
|
||||
expectAriaAttrOnEachElement(element, 'tabindex', undefined);
|
||||
they('should not attach to native control $prop', {
|
||||
'button': "<button ng-click='something'></button>",
|
||||
'a': "<a ng-href='#/something'>",
|
||||
'input[text]': "<input type='text' ng-model='val'>",
|
||||
'input[radio]': "<input type='radio' ng-model='val'>",
|
||||
'input[checkbox]': "<input type='checkbox' ng-model='val'>",
|
||||
'textarea': "<textarea ng-model='val'></textarea>",
|
||||
'select': "<select ng-model='val'></select>",
|
||||
'details': "<details ng-model='val'></details>"
|
||||
}, function(html) {
|
||||
compileElement(html);
|
||||
expect(element.attr('tabindex')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not attach to random ng-model elements', function() {
|
||||
|
||||
@@ -156,7 +156,7 @@ describe("resource", function() {
|
||||
});
|
||||
|
||||
|
||||
it('should ignore slashes of undefinend parameters', function() {
|
||||
it('should ignore slashes of undefined parameters', function() {
|
||||
var R = $resource('/Path/:a/:b/:c');
|
||||
|
||||
$httpBackend.when('GET', '/Path').respond('{}');
|
||||
@@ -181,7 +181,7 @@ describe("resource", function() {
|
||||
R.get({a:6, b:7, c:8});
|
||||
});
|
||||
|
||||
it('should not ignore leading slashes of undefinend parameters that have non-slash trailing sequence', function() {
|
||||
it('should not ignore leading slashes of undefined parameters that have non-slash trailing sequence', function() {
|
||||
var R = $resource('/Path/:a.foo/:b.bar/:c.baz');
|
||||
|
||||
$httpBackend.when('GET', '/Path/.foo/.bar.baz').respond('{}');
|
||||
@@ -242,7 +242,7 @@ describe("resource", function() {
|
||||
});
|
||||
|
||||
it('should not encode @ in url params', function() {
|
||||
//encodeURIComponent is too agressive and doesn't follow http://www.ietf.org/rfc/rfc3986.txt
|
||||
//encodeURIComponent is too aggressive and doesn't follow http://www.ietf.org/rfc/rfc3986.txt
|
||||
//with regards to the character set (pchar) allowed in path segments
|
||||
//so we need this test to make sure that we don't over-encode the params and break stuff like
|
||||
//buzz api which uses @self
|
||||
@@ -1308,7 +1308,7 @@ describe("resource", function() {
|
||||
});
|
||||
|
||||
describe('resource', function() {
|
||||
var $httpBackend, $resource;
|
||||
var $httpBackend, $resource, $q;
|
||||
|
||||
beforeEach(module(function($exceptionHandlerProvider) {
|
||||
$exceptionHandlerProvider.mode('log');
|
||||
@@ -1319,6 +1319,7 @@ describe('resource', function() {
|
||||
beforeEach(inject(function($injector) {
|
||||
$httpBackend = $injector.get('$httpBackend');
|
||||
$resource = $injector.get('$resource');
|
||||
$q = $injector.get('$q');
|
||||
}));
|
||||
|
||||
|
||||
@@ -1356,5 +1357,34 @@ describe('resource', function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('should cancel the request if timeout promise is resolved', function() {
|
||||
var canceler = $q.defer();
|
||||
|
||||
$httpBackend.when('GET', '/CreditCard').respond({data: '123'});
|
||||
|
||||
var CreditCard = $resource('/CreditCard', {}, {
|
||||
query: {
|
||||
method: 'GET',
|
||||
timeout: canceler.promise
|
||||
}
|
||||
});
|
||||
|
||||
CreditCard.query();
|
||||
|
||||
canceler.resolve();
|
||||
expect($httpBackend.flush).toThrow(new Error("No pending request to flush !"));
|
||||
|
||||
canceler = $q.defer();
|
||||
CreditCard = $resource('/CreditCard', {}, {
|
||||
query: {
|
||||
method: 'GET',
|
||||
timeout: canceler.promise
|
||||
}
|
||||
});
|
||||
|
||||
CreditCard.query();
|
||||
expect($httpBackend.flush).not.toThrow();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user