Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7989c7d24a | |||
| 5c36f466e1 | |||
| f8151afd90 | |||
| 74120eaa0f | |||
| b370fac4fc | |||
| 23fc73081f | |||
| e5e69d9b90 | |||
| fa722447f8 | |||
| 81d10e819e | |||
| 809ca94e1c | |||
| 824eab9029 | |||
| d503dfe99b | |||
| e4d33917e3 | |||
| 6326e2028b | |||
| 8aed2047f0 | |||
| f60b6b0938 | |||
| eea7113abe | |||
| c8f34e7f6b | |||
| 011fa39c2a | |||
| 58d0e8945d | |||
| 2bbced212e | |||
| 5a8ad8fe32 | |||
| 41d5938883 | |||
| 5480d2a80b | |||
| 95adf3a4d8 | |||
| cc315ef4cc | |||
| 41c233ada1 | |||
| 46091f811b | |||
| fde2ccb3f5 | |||
| 1cc2ad2443 | |||
| 1aa46fac62 | |||
| 5bde02a8ca | |||
| d07e9f77f1 | |||
| aa21c521eb | |||
| bd14a90610 | |||
| 9f1b9849d8 | |||
| 47f159cdf3 | |||
| 99eb123d79 | |||
| 6515adc118 | |||
| b7aff92354 | |||
| 6b3b00e095 | |||
| 921f7ce49e | |||
| 17eee57c52 | |||
| 4fc3ee8040 | |||
| 39d3ae80d9 | |||
| 480f2f33c1 | |||
| 9c9a89f7ff | |||
| 73194009a9 | |||
| 162f41a1ab | |||
| 7c82c4f837 | |||
| 97b1371199 | |||
| 95d1768c77 | |||
| c3d99d68da | |||
| 303a683081 | |||
| a0e8c45880 | |||
| 870547d185 | |||
| 0d1f8a0532 | |||
| b94600d71e | |||
| 3e5a4ef86c | |||
| efec0c358d | |||
| 1f59de35c9 |
@@ -1,3 +1,59 @@
|
||||
# <angular/> 0.9.7 sonic-scream (2010-12-10) #
|
||||
|
||||
### Bug Fixes
|
||||
- $defer service should always call $eval on the root scope after a callback runs (issue #189)
|
||||
- fix for failed assignments of form obj[0].name=value (issue #169)
|
||||
- significant parser improvements that resulted in lower memory usage
|
||||
(commit 23fc73081feb640164615930b36ef185c23a3526)
|
||||
|
||||
### Docs
|
||||
- small docs improvements (mainly docs for the $resource service)
|
||||
|
||||
### Breaking changes
|
||||
- Angular expressions in the view used to support regular expressions. This feature was rarely
|
||||
used and added unnecessary complexity. It not a good idea to have regexps in the view anyway,
|
||||
so we removed this support. If you had any regexp in your views, you will have to move them to
|
||||
your controllers. (commit e5e69d9b90850eb653883f52c76e28dd870ee067)
|
||||
|
||||
|
||||
# <angular/> 0.9.6 night-vision (2010-12-06) #
|
||||
|
||||
### Security
|
||||
- several improvements in the HTML sanitizer code to prevent code execution via `href`s and other
|
||||
attributes.
|
||||
Commits:
|
||||
- 41d5938883a3d06ffe8a88a51efd8d1896f7d747
|
||||
- 2bbced212e2ee93948c45360fee00b2e3f960392
|
||||
|
||||
### Docs
|
||||
- set up http://docs.angularjs.org domain, the docs for the latest release will from now on be
|
||||
deployed here.
|
||||
- docs app UI polishing with dual scrolling and other improvements
|
||||
|
||||
### Bug Fixes
|
||||
- `select` widget now behaves correctly when it's `option` items are created via `ng:repeat`
|
||||
(issue #170)
|
||||
- fix for async xhr cache issue #152 by adding `$browser.defer` and `$defer` service
|
||||
|
||||
### Breaking Changes
|
||||
- Fix for issue #152 might break some tests that were relying on the incorrect behavior. The
|
||||
breakage will usually affect code that tests resources, xhr or services/widgets build on top of
|
||||
these. All that is typically needed to resolve the issue is adding a call to
|
||||
`$browser.defer.flush()` in your test just before the point where you expect all cached
|
||||
resource/xhr requests to return any results. Please see 011fa39c2a0b5da843395b538fc4e52e5ade8287
|
||||
for more info.
|
||||
- The HTML sanitizer is slightly more strinct now. Please see info in the "Security" section above.
|
||||
|
||||
|
||||
# <angular/> 0.9.5 turkey-blast (2010-11-25) #
|
||||
|
||||
### Docs
|
||||
- 99% of the content from the angular wiki is now in the docs
|
||||
|
||||
### Api
|
||||
- added `angular.Array.limitTo` to make it easy to select first or last few items of an array
|
||||
|
||||
|
||||
# <angular/> 0.9.4 total-recall (2010-11-18) #
|
||||
|
||||
### Docs
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular.directive
|
||||
@namespace Namespace for all directives.
|
||||
|
||||
@description
|
||||
A directive is an HTML attribute that you can use in an existing HTML element type or in a
|
||||
DOM element type that you create as {@link angular.widget}, to modify that element's
|
||||
properties. You can use any number of directives per element.
|
||||
|
||||
For example, you can add the ng:bind directive as an attribute of an HTML span element, as in
|
||||
`<span ng:bind="1+2"></span>`. How does this work? The compiler passes the attribute value
|
||||
`1+2` to the ng:bind extension, which in turn tells the {@link angular.scope} to watch that
|
||||
expression and report changes. On any change it sets the span text to the expression value.
|
||||
|
||||
Here's how to define {@link angular.directive.ng:bind ng:bind}:
|
||||
<pre>
|
||||
angular.directive('ng:bind', function(expression, compiledElement) {
|
||||
var compiler = this;
|
||||
return function(linkElement) {
|
||||
var currentScope = this;
|
||||
currentScope.$watch(expression, function(value) {
|
||||
linkElement.text(value);
|
||||
});
|
||||
};
|
||||
});
|
||||
</pre>
|
||||
|
||||
# Directive vs. Attribute Widget
|
||||
Both [attribute widgets](#!angular.widget) and directives can compile a DOM element
|
||||
attribute. So why have two different ways to do the same thing? The answer is that order
|
||||
matters, but we have no control over the order in which attributes are read. To solve this
|
||||
we apply attribute widget before the directive.
|
||||
|
||||
For example, consider this piece of HTML, which uses the directives `ng:repeat`, `ng:init`,
|
||||
and `ng:bind`:
|
||||
<pre>
|
||||
<ul ng:init="people=['mike', 'mary']">
|
||||
<li ng:repeat="person in people" ng:init="a=a+1" ng:bind="person"></li>
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
Notice that the order of execution matters here. We need to execute
|
||||
{@link angular.directive.ng:repeat ng:repeat} before we run the
|
||||
{@link angular.directive.ng:init ng:init} and `ng:bind` on the `<li/>;`. This is because we
|
||||
want to run the `ng:init="a=a+1` and `ng:bind="person"` once for each person in people. We
|
||||
could not have used directive to create this template because attributes are read in an
|
||||
unspecified order and there is no way of guaranteeing that the repeater attribute would
|
||||
execute first. Using the `ng:repeat` attribute directive ensures that we can transform the
|
||||
DOM element into a template.
|
||||
|
||||
Widgets run before directives. Widgets may manipulate the DOM whereas directives are not
|
||||
expected to do so, and so they run last.
|
||||
@@ -0,0 +1,43 @@
|
||||
@workInProgress
|
||||
@ngdoc function
|
||||
@name angular.element
|
||||
@function
|
||||
|
||||
@description
|
||||
Wraps a raw DOM element or HTML string as [jQuery](http://jquery.com) element.
|
||||
`angular.element` is either an alias for [jQuery](http://api.jquery.com/jQuery/) function if
|
||||
jQuery is loaded or a function that wraps the element or string in angular's jQuery lite
|
||||
implementation.
|
||||
|
||||
Real jQuery always takes precedence if it was loaded before angular.
|
||||
|
||||
Angular's jQuery lite implementation is a tiny API-compatible subset of jQuery which allows
|
||||
angular to manipulate DOM. The functions implemented are usually just the basic versions of
|
||||
them and might not support arguments and invocation styles.
|
||||
|
||||
NOTE: All element references in angular are always wrapped with jQuery (lite) and are never
|
||||
raw DOM references.
|
||||
|
||||
Angular's jQuery lite implements these functions:
|
||||
|
||||
- [addClass()](http://api.jquery.com/addClass/)
|
||||
- [after()](http://api.jquery.com/after/)
|
||||
- [append()](http://api.jquery.com/append/)
|
||||
- [attr()](http://api.jquery.com/attr/)
|
||||
- [bind()](http://api.jquery.com/bind/)
|
||||
- [children()](http://api.jquery.com/children/)
|
||||
- [clone()](http://api.jquery.com/clone/)
|
||||
- [css()](http://api.jquery.com/css/)
|
||||
- [data()](http://api.jquery.com/data/)
|
||||
- [hasClass()](http://api.jquery.com/hasClass/)
|
||||
- [parent()](http://api.jquery.com/parent/)
|
||||
- [remove()](http://api.jquery.com/remove/)
|
||||
- [removeAttr()](http://api.jquery.com/removeAttr/)
|
||||
- [removeClass()](http://api.jquery.com/removeClass/)
|
||||
- [removeData()](http://api.jquery.com/removeData/)
|
||||
- [replaceWith()](http://api.jquery.com/replaceWith/)
|
||||
- [text()](http://api.jquery.com/text/)
|
||||
- [trigger()](http://api.jquery.com/trigger/)
|
||||
|
||||
@param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery.
|
||||
@returns {Object} jQuery object.
|
||||
@@ -0,0 +1,76 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular.filter
|
||||
@namespace Namespace for all filters.
|
||||
@description
|
||||
# Overview
|
||||
Filters are a standard way to format your data for display to the user. For example, you
|
||||
might have the number 1234.5678 and would like to display it as US currency: $1,234.57.
|
||||
Filters allow you to do just that. In addition to transforming the data, filters also modify
|
||||
the DOM. This allows the filters to for example apply css styles to the filtered output if
|
||||
certain conditions were met.
|
||||
|
||||
|
||||
# Standard Filters
|
||||
|
||||
The Angular framework provides a standard set of filters for common operations, including:
|
||||
{@link angular.filter.currency currency}, {@link angular.filter.json json},
|
||||
{@link angular.filter.number number}, and {@link angular.filter.html html}. You can also add
|
||||
your own filters.
|
||||
|
||||
|
||||
# Syntax
|
||||
|
||||
Filters can be part of any {@link angular.scope} evaluation but are typically used with
|
||||
{{bindings}}. Filters typically transform the data to a new data type, formating the data in
|
||||
the process. Filters can be chained and take optional arguments. Here are few examples:
|
||||
|
||||
* No filter: {{1234.5678}} => 1234.5678
|
||||
* Number filter: {{1234.5678|number}} => 1,234.57. Notice the “,” and rounding to two
|
||||
significant digits.
|
||||
* Filter with arguments: {{1234.5678|number:5}} => 1,234.56780. Filters can take optional
|
||||
arguments, separated by colons in a binding. To number, the argument “5” requests 5 digits
|
||||
to the right of the decimal point.
|
||||
|
||||
|
||||
# Writing your own Filters
|
||||
|
||||
Writing your own filter is very easy: just define a JavaScript function on `angular.filter`.
|
||||
The framework passes in the input value as the first argument to your function. Any filter
|
||||
arguments are passed in as additional function arguments.
|
||||
|
||||
You can use these variables in the function:
|
||||
|
||||
* `this` — The current scope.
|
||||
* `this.$element` — The DOM element containing the binding. This allows the filter to manipulate
|
||||
the DOM in addition to transforming the input.
|
||||
|
||||
|
||||
@exampleDescription
|
||||
The following example filter reverses a text string. In addition, it conditionally makes the
|
||||
text upper-case (to demonstrate optional arguments) and assigns color (to demonstrate DOM
|
||||
modification).
|
||||
|
||||
@example
|
||||
<script type="text/javascript">
|
||||
angular.filter('reverse', function(input, uppercase, color) {
|
||||
var out = "";
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
out = input.charAt(i) + out;
|
||||
}
|
||||
if (uppercase) {
|
||||
out = out.toUpperCase();
|
||||
}
|
||||
if (color) {
|
||||
this.$element.css('color', color);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
</script>
|
||||
|
||||
<input name="text" type="text" value="hello" /><br>
|
||||
No filter: {{text}}<br>
|
||||
Reverse: {{text|reverse}}<br>
|
||||
Reverse + uppercase: {{text|reverse:true}}<br>
|
||||
Reverse + uppercase + blue: {{text|reverse:true:"blue"}}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular.formatter
|
||||
@namespace Namespace for all formats.
|
||||
@description
|
||||
# Overview
|
||||
The formatters are responsible for translating user readable text in an input widget to a
|
||||
data model stored in an application.
|
||||
|
||||
# Writting your own Formatter
|
||||
Writing your own formatter is easy. Just register a pair of JavaScript functions with
|
||||
`angular.formatter`. One function for parsing user input text to the stored form,
|
||||
and one for formatting the stored data to user-visible text.
|
||||
|
||||
Here is an example of a "reverse" formatter: The data is stored in uppercase and in
|
||||
reverse, while it is displayed in lower case and non-reversed. User edits are
|
||||
automatically parsed into the internal form and data changes are automatically
|
||||
formatted to the viewed form.
|
||||
|
||||
<pre>
|
||||
function reverse(text) {
|
||||
var reversed = [];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
reversed.unshift(text.charAt(i));
|
||||
}
|
||||
return reversed.join('');
|
||||
}
|
||||
|
||||
angular.formatter('reverse', {
|
||||
parse: function(value){
|
||||
return reverse(value||'').toUpperCase();
|
||||
},
|
||||
format: function(value){
|
||||
return reverse(value||'').toLowerCase();
|
||||
}
|
||||
});
|
||||
</pre>
|
||||
|
||||
@example
|
||||
<script type="text/javascript">
|
||||
function reverse(text) {
|
||||
var reversed = [];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
reversed.unshift(text.charAt(i));
|
||||
}
|
||||
return reversed.join('');
|
||||
}
|
||||
|
||||
angular.formatter('reverse', {
|
||||
parse: function(value){
|
||||
return reverse(value||'').toUpperCase();
|
||||
},
|
||||
format: function(value){
|
||||
return reverse(value||'').toLowerCase();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Formatted:
|
||||
<input type="text" name="data" value="angular" ng:format="reverse"/>
|
||||
<br/>
|
||||
|
||||
Stored:
|
||||
<input type="text" name="data"/><br/>
|
||||
<pre>{{data}}</pre>
|
||||
|
||||
|
||||
@scenario
|
||||
it('should store reverse', function(){
|
||||
expect(element('.doc-example input:first').val()).toEqual('angular');
|
||||
expect(element('.doc-example input:last').val()).toEqual('RALUGNA');
|
||||
|
||||
this.addFutureAction('change to XYZ', function($window, $document, done){
|
||||
$document.elements('.doc-example input:last').val('XYZ').trigger('change');
|
||||
done();
|
||||
});
|
||||
expect(element('.doc-example input:first').val()).toEqual('zyx');
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular
|
||||
@namespace The exported angular namespace.
|
||||
@@ -0,0 +1,159 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular.service
|
||||
|
||||
@description
|
||||
# Overview
|
||||
Services are substituable objects, which are wired together using dependency injection.
|
||||
Each service could have dependencies (other services), which are passed in constructor.
|
||||
Because JS is dynamicaly typed language, dependency injection can not use static types
|
||||
to satisfy these dependencies, so each service must explicitely define its dependencies.
|
||||
This is done by `$inject` property.
|
||||
|
||||
For now, life time of all services is the same as the life time of page.
|
||||
|
||||
|
||||
# Built-in services
|
||||
The Angular framework provides a standard set of services for common operations.
|
||||
You can write your own services and rewrite these standard services as well.
|
||||
Like other core angular variables, the built-in services always start with $.
|
||||
|
||||
* `angular.service.$browser`
|
||||
* `angular.service.$window`
|
||||
* `angular.service.$document`
|
||||
* `angular.service.$location`
|
||||
* `angular.service.$log`
|
||||
* `angular.service.$exceptionHandler`
|
||||
* `angular.service.$hover`
|
||||
* `angular.service.$invalidWidgets`
|
||||
* `angular.service.$route`
|
||||
* `angular.service.$xhr`
|
||||
* `angular.service.$xhr.error`
|
||||
* `angular.service.$xhr.bulk`
|
||||
* `angular.service.$xhr.cache`
|
||||
* `angular.service.$resource`
|
||||
* `angular.service.$cookies`
|
||||
* `angular.service.$cookieStore`
|
||||
|
||||
# Writing your own custom services
|
||||
Angular provides only set of basic services, so you will probably need to write your custom
|
||||
service very soon. To do so, you need to write a factory function and register this function
|
||||
to angular's dependency injector. This factory function must return an object - your service
|
||||
(it is not called with new operator).
|
||||
|
||||
**angular.service** has three parameters:
|
||||
|
||||
- `{string} name` - Name of the service
|
||||
- `{function()} factory` - Factory function (called just once by DI)
|
||||
- `{Object} config` - Hash of configuration (`$inject`, `$creation`)
|
||||
|
||||
If your service requires - depends on other services, you need to specify them
|
||||
in config hash - property $inject. This property is an array of strings (service names).
|
||||
These dependencies will be passed as parameters to the factory function by DI.
|
||||
This approach is very useful when testing, as you can inject mocks/stubs/dummies.
|
||||
|
||||
Here is an example of very simple service. This service requires $window service (it's
|
||||
passed as a parameter to factory function) and it's just a function.
|
||||
|
||||
This service simple stores all notifications and after third one, it displays all of them by
|
||||
window alert.
|
||||
<pre>
|
||||
angular.service('notify', function(win) {
|
||||
var msgs = [];
|
||||
return function(msg) {
|
||||
msgs.push(msg);
|
||||
if (msgs.length == 3) {
|
||||
win.alert(msgs.join("\n"));
|
||||
msgs = [];
|
||||
}
|
||||
};
|
||||
}, {$inject: ['$window']});
|
||||
</pre>
|
||||
|
||||
And here is a unit test for this service. We use Jasmine spy (mock) instead of real browser's alert.
|
||||
<pre>
|
||||
var mock, notify;
|
||||
|
||||
beforeEach(function() {
|
||||
mock = {alert: jasmine.createSpy()};
|
||||
notify = angular.service('notify')(mock);
|
||||
});
|
||||
|
||||
it('should not alert first two notifications', function() {
|
||||
notify('one');
|
||||
notify('two');
|
||||
expect(mock.alert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should alert all after third notification', function() {
|
||||
notify('one');
|
||||
notify('two');
|
||||
notify('three');
|
||||
expect(mock.alert).toHaveBeenCalledWith("one\ntwo\nthree");
|
||||
});
|
||||
|
||||
it('should clear messages after alert', function() {
|
||||
notify('one');
|
||||
notify('two');
|
||||
notify('third');
|
||||
notify('more');
|
||||
notify('two');
|
||||
notify('third');
|
||||
expect(mock.alert.callCount).toEqual(2);
|
||||
expect(mock.alert.mostRecentCall.args).toEqual(["more\ntwo\nthird"]);
|
||||
});
|
||||
</pre>
|
||||
|
||||
# Injecting services into controllers
|
||||
Using services in a controllers is very similar to using service in other service.
|
||||
Again, we will use dependency injection.
|
||||
|
||||
JavaScript is dynamic language, so DI is not able to figure out which services to inject by
|
||||
static types (like in static typed languages). Therefore you must specify the service name
|
||||
by the `$inject` property - it's an array that contains strings with names of services to be
|
||||
injected. The name must match the id that service has been registered as with angular.
|
||||
The order of the services in the array matters, because this order will be used when calling
|
||||
the factory function with injected parameters. The names of parameters in factory function
|
||||
don't matter, but by convention they match the service ids.
|
||||
<pre>
|
||||
function myController($loc, $log) {
|
||||
this.firstMethod = function() {
|
||||
// use $location service
|
||||
$loc.setHash();
|
||||
};
|
||||
this.secondMethod = function() {
|
||||
// use $log service
|
||||
$log.info('...');
|
||||
};
|
||||
}
|
||||
// which services to inject ?
|
||||
myController.$inject = ['$location', '$log'];
|
||||
</pre>
|
||||
|
||||
@example
|
||||
<script type="text/javascript">
|
||||
angular.service('notify', function(win) {
|
||||
var msgs = [];
|
||||
return function(msg) {
|
||||
msgs.push(msg);
|
||||
if (msgs.length == 3) {
|
||||
win.alert(msgs.join("\n"));
|
||||
msgs = [];
|
||||
}
|
||||
};
|
||||
}, {$inject: ['$window']});
|
||||
|
||||
function myController(notifyService) {
|
||||
this.callNotify = function(msg) {
|
||||
notifyService(msg);
|
||||
};
|
||||
}
|
||||
|
||||
myController.$inject = ['notify'];
|
||||
</script>
|
||||
|
||||
<div ng:controller="myController">
|
||||
<p>Let's try this simple notify service, injected into the controller...</p>
|
||||
<input ng:init="message='test'" type="text" name="message" />
|
||||
<button ng:click="callNotify(message);">NOTIFY</button>
|
||||
</div>
|
||||
@@ -0,0 +1,73 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular.validator
|
||||
@namespace Namespace for all filters.
|
||||
@description
|
||||
# Overview
|
||||
Validators are a standard way to check the user input against a specific criteria. For
|
||||
example, you might need to check that an input field contains a well-formed phone number.
|
||||
|
||||
# Syntax
|
||||
Attach a validator on user input widgets using the `ng:validate` attribute.
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
Change me: <input type="text" name="number" ng:validate="integer" value="123">
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should validate the default number string', function() {
|
||||
expect(element('input[name=number]').attr('class')).
|
||||
not().toMatch(/ng-validation-error/);
|
||||
});
|
||||
it('should not validate "foo"', function() {
|
||||
input('number').enter('foo');
|
||||
expect(element('input[name=number]').attr('class')).
|
||||
toMatch(/ng-validation-error/);
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
|
||||
# Writing your own Validators
|
||||
Writing your own validator is easy. To make a function available as a
|
||||
validator, just define the JavaScript function on the `angular.validator`
|
||||
object. <angular/> passes in the input to validate as the first argument
|
||||
to your function. Any additional validator arguments are passed in as
|
||||
additional arguments to your function.
|
||||
|
||||
You can use these variables in the function:
|
||||
|
||||
* `this` — The current scope.
|
||||
* `this.$element` — The DOM element containing the binding. This allows the filter to manipulate
|
||||
the DOM in addition to transforming the input.
|
||||
|
||||
In this example we have written a upsTrackingNo validator.
|
||||
It marks the input text "valid" only when the user enters a well-formed
|
||||
UPS tracking number.
|
||||
|
||||
@css ng-validation-error
|
||||
When validation fails, this css class is applied to the binding, making its borders red by
|
||||
default.
|
||||
|
||||
@example
|
||||
<script>
|
||||
angular.validator('upsTrackingNo', function(input, format) {
|
||||
var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$");
|
||||
return input.match(regexp)?"":"The format must match " + format;
|
||||
});
|
||||
</script>
|
||||
<input type="text" name="trackNo" size="40"
|
||||
ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'"
|
||||
value="1Z 123 456 78 9012 345 6"/>
|
||||
|
||||
@scenario
|
||||
it('should validate correct UPS tracking number', function() {
|
||||
expect(element('input[name=trackNo]').attr('class')).
|
||||
not().toMatch(/ng-validation-error/);
|
||||
});
|
||||
|
||||
it('should not validate in correct UPS tracking number', function() {
|
||||
input('trackNo').enter('foo');
|
||||
expect(element('input[name=trackNo]').attr('class')).
|
||||
toMatch(/ng-validation-error/);
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular.widget
|
||||
@namespace Namespace for all widgets.
|
||||
@description
|
||||
# Overview
|
||||
Widgets allow you to create DOM elements that the browser doesn't
|
||||
already understand. You create the widget in your namespace and
|
||||
assign it behavior. You can only bind one widget per DOM element
|
||||
(unlike directives, in which you can use any number per DOM
|
||||
element). Widgets are expected to manipulate the DOM tree by
|
||||
adding new elements whereas directives are expected to only modify
|
||||
element properties.
|
||||
|
||||
Widgets come in two flavors: element and attribute.
|
||||
|
||||
# Element Widget
|
||||
Let's say we would like to create a new element type in the
|
||||
namespace `my` that can watch an expression and alert() the user
|
||||
with each new value.
|
||||
|
||||
<pre>
|
||||
<my:watch exp="name"/>
|
||||
</pre>
|
||||
|
||||
You can implement `my:watch` like this:
|
||||
<pre>
|
||||
angular.widget('my:watch', function(compileElement) {
|
||||
var compiler = this;
|
||||
var exp = compileElement.attr('exp');
|
||||
return function(linkElement) {
|
||||
var currentScope = this;
|
||||
currentScope.$watch(exp, function(value){
|
||||
alert(value);
|
||||
}};
|
||||
};
|
||||
});
|
||||
</pre>
|
||||
|
||||
# Attribute Widget
|
||||
Let's implement the same widget, but this time as an attribute
|
||||
that can be added to any existing DOM element.
|
||||
<pre>
|
||||
<div my-watch="name">text</div>
|
||||
</pre>
|
||||
You can implement `my:watch` attribute like this:
|
||||
<pre>
|
||||
angular.widget('@my:watch', function(expression, compileElement) {
|
||||
var compiler = this;
|
||||
return function(linkElement) {
|
||||
var currentScope = this;
|
||||
currentScope.$watch(expression, function(value){
|
||||
alert(value);
|
||||
});
|
||||
};
|
||||
});
|
||||
</pre>
|
||||
|
||||
@example
|
||||
<script>
|
||||
angular.widget('my:time', function(compileElement){
|
||||
compileElement.css('display', 'block');
|
||||
return function(linkElement){
|
||||
function update(){
|
||||
linkElement.text('Current time is: ' + new Date());
|
||||
setTimeout(update, 1000);
|
||||
}
|
||||
update();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<my:time></my:time>
|
||||
|
||||
+83
-36
@@ -16,39 +16,29 @@ var keywordPages = [];
|
||||
var SRC_DIR = "docs/";
|
||||
var OUTPUT_DIR = "build/docs/";
|
||||
var NEW_LINE = /\n\r?/;
|
||||
var TEMPLATES = {};
|
||||
var start = now();
|
||||
|
||||
function now(){ return new Date().getTime(); }
|
||||
var work = callback.chain(function () {
|
||||
console.log('Parsing Angular Reference Documentation');
|
||||
mkdirPath(OUTPUT_DIR, work.waitFor(function(){
|
||||
findJsFiles('src', work.waitMany(function(file) {
|
||||
//console.log('reading', file, '...');
|
||||
findNgDoc(file, work.waitMany(function(doc) {
|
||||
parseNgDoc(doc);
|
||||
processNgDoc(documentation, doc);
|
||||
}));
|
||||
findJsFiles('src', work.waitMany(function(file) {
|
||||
//console.log('reading', file, '...');
|
||||
findNgDocInJsFile(file, work.waitMany(function(doc) {
|
||||
parseNgDoc(doc);
|
||||
processNgDoc(documentation, doc);
|
||||
}));
|
||||
}));
|
||||
findNgDocInDir(SRC_DIR, work.waitMany(function(doc){
|
||||
parseNgDoc(doc);
|
||||
processNgDoc(documentation, doc);
|
||||
}));
|
||||
loadTemplates(TEMPLATES, work.waitFor());
|
||||
mkdirPath(OUTPUT_DIR, work.waitFor());
|
||||
}).onError(function(err){
|
||||
console.log('ERROR:', err.stack || err);
|
||||
}).onDone(function(){
|
||||
keywordPages.sort(function(a,b){
|
||||
// supper ugly comparator that orders all utility methods and objects before all the other stuff
|
||||
// like widgets, directives, services, etc.
|
||||
// Mother of all beatiful code please forgive me for the sin that this code certainly is.
|
||||
|
||||
if (a.name === b.name) return 0;
|
||||
if (a.name === 'angular') return -1;
|
||||
if (b.name === 'angular') return 1;
|
||||
|
||||
function namespacedName(page) {
|
||||
return (page.name.match(/\./g).length === 1 && page.type !== 'overview' ? '0' : '1') + page.name;
|
||||
}
|
||||
|
||||
var namespacedA = namespacedName(a),
|
||||
namespacedB = namespacedName(b);
|
||||
|
||||
return namespacedA < namespacedB ? -1 : 1;
|
||||
});
|
||||
keywordPages.sort(keywordSort);
|
||||
writeDoc(documentation.pages);
|
||||
mergeTemplate('docs-data.js', 'docs-data.js', {JSON:JSON.stringify(keywordPages)}, callback.chain());
|
||||
mergeTemplate('docs-scenario.js', 'docs-scenario.js', documentation, callback.chain());
|
||||
@@ -58,7 +48,7 @@ var work = callback.chain(function () {
|
||||
mergeTemplate('docs.js', 'docs.js', documentation, callback.chain());
|
||||
mergeTemplate('doc_widgets.css', 'doc_widgets.css', documentation, callback.chain());
|
||||
mergeTemplate('doc_widgets.js', 'doc_widgets.js', documentation, callback.chain());
|
||||
console.log('DONE');
|
||||
console.log('DONE', now() - start, 'ms.');
|
||||
});
|
||||
if (!this.testmode) work();
|
||||
////////////////////
|
||||
@@ -163,7 +153,7 @@ function markdownTag(doc, name, value) {
|
||||
replace(/\<\/pre\>/gmi, '</pre></div>');
|
||||
}
|
||||
|
||||
R_LINK = /{@link ([^\s}]+)((\s|\n)+(.+?))?\s*}/m
|
||||
var R_LINK = /{@link ([^\s}]+)((\s|\n)+(.+?))?\s*}/m;
|
||||
// 1 123 3 4 42
|
||||
|
||||
function markdown(text) {
|
||||
@@ -218,7 +208,7 @@ function propertyTag(doc, name, value) {
|
||||
}
|
||||
|
||||
function returnsTag(doc, name, value) {
|
||||
var match = value.match(/^{(\S+)}\s+(.*)?/);
|
||||
var match = value.match(/^{(\S+)}\s+([\s\S]*)?/);
|
||||
|
||||
if (match) {
|
||||
var tag = {
|
||||
@@ -252,9 +242,10 @@ var TAG = {
|
||||
element: valueTag,
|
||||
methodOf: valueTag,
|
||||
name: function(doc, name, value) {
|
||||
var parts = value.split(/\./);
|
||||
doc.name = value;
|
||||
doc.shortName = value.split(/\./).pop();
|
||||
doc.depth = value.split(/\./).length - 1;
|
||||
doc.shortName = parts.pop();
|
||||
doc.depth = parts.length;
|
||||
},
|
||||
param: function(doc, name, value){
|
||||
doc.param = doc.param || [];
|
||||
@@ -312,7 +303,7 @@ function parseNgDoc(doc){
|
||||
}
|
||||
}
|
||||
|
||||
function findNgDoc(file, callback) {
|
||||
function findNgDocInJsFile(file, callback) {
|
||||
fs.readFile(file, callback.waitFor(function(err, content){
|
||||
var lines = content.toString().split(NEW_LINE);
|
||||
var doc;
|
||||
@@ -345,6 +336,22 @@ function findNgDoc(file, callback) {
|
||||
}));
|
||||
}
|
||||
|
||||
function loadTemplates(cache, callback){
|
||||
fs.readdir('docs', callback.waitFor(function(err, files){
|
||||
if (err) return this.error(err);
|
||||
files.forEach(function(file){
|
||||
var match = file.match(/^(.*)\.template$/);
|
||||
if (match) {
|
||||
fs.readFile(SRC_DIR + file, callback.waitFor(function(err, content){
|
||||
if (err) return this.error(err);
|
||||
cache[match[1]] = content.toString();
|
||||
}));
|
||||
}
|
||||
});
|
||||
callback();
|
||||
}));
|
||||
};
|
||||
|
||||
function findJsFiles(dir, callback){
|
||||
fs.readdir(dir, callback.waitFor(function(err, files){
|
||||
if (err) return this.error(err);
|
||||
@@ -364,7 +371,7 @@ function findJsFiles(dir, callback){
|
||||
|
||||
function processNgDoc(documentation, doc) {
|
||||
if (!doc.ngdoc) return;
|
||||
console.log('Found:', doc.ngdoc + ':' + doc.name);
|
||||
//console.log('Found:', doc.ngdoc + ':' + doc.name);
|
||||
|
||||
documentation.byName[doc.name] = doc;
|
||||
|
||||
@@ -384,10 +391,50 @@ function processNgDoc(documentation, doc) {
|
||||
}
|
||||
}
|
||||
|
||||
function writeDoc(pages) {
|
||||
function writeDoc(pages, callback) {
|
||||
pages.forEach(function(doc) {
|
||||
mergeTemplate(
|
||||
doc.ngdoc + '.template',
|
||||
doc.name + '.html', doc, callback.chain());
|
||||
var template = TEMPLATES[doc.ngdoc];
|
||||
if (!template) throw new Error("No template for:" + doc.ngdoc);
|
||||
var content = mustache.to_html(template, doc);
|
||||
fs.writeFile(OUTPUT_DIR + doc.name + '.html', content, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function findNgDocInDir(directory, docNotify) {
|
||||
fs.readdir(directory, docNotify.waitFor(function(err, files){
|
||||
if (err) return this.error(err);
|
||||
files.forEach(function(file){
|
||||
console.log(file);
|
||||
if (!file.match(/\.ngdoc$/)) return;
|
||||
fs.readFile(directory + file, docNotify.waitFor(function(err, content){
|
||||
if (err) return this.error(err);
|
||||
docNotify({
|
||||
raw:{
|
||||
text:content.toString(),
|
||||
file: directory + file,
|
||||
line: 1}
|
||||
});
|
||||
}));
|
||||
});
|
||||
docNotify.done();
|
||||
}));
|
||||
}
|
||||
|
||||
function keywordSort(a,b){
|
||||
// supper ugly comparator that orders all utility methods and objects before all the other stuff
|
||||
// like widgets, directives, services, etc.
|
||||
// Mother of all beautiful code please forgive me for the sin that this code certainly is.
|
||||
|
||||
if (a.name === b.name) return 0;
|
||||
if (a.name === 'angular') return -1;
|
||||
if (b.name === 'angular') return 1;
|
||||
|
||||
function namespacedName(page) {
|
||||
return (page.name.match(/\./g).length === 1 && page.type !== 'overview' ? '0' : '1') + page.name;
|
||||
}
|
||||
|
||||
var namespacedA = namespacedName(a),
|
||||
namespacedB = namespacedName(b);
|
||||
|
||||
return namespacedA < namespacedB ? -1 : 1;
|
||||
}
|
||||
|
||||
+112
-33
@@ -1,3 +1,5 @@
|
||||
/* Common Style */
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
@@ -5,28 +7,49 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#page {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
#sidebar,
|
||||
#section {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-left: 1em;
|
||||
margin-top: 0.5em;
|
||||
|
||||
/* Main Layout */
|
||||
|
||||
#header {
|
||||
height: 3.5em;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
float: right;
|
||||
#sidebar,
|
||||
#main {
|
||||
position: absolute;
|
||||
top: 3.5em;
|
||||
bottom: 0;
|
||||
margin-top: 1px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 13.8em;
|
||||
padding: 0.8em 0em 1.5em 0.8em;
|
||||
}
|
||||
|
||||
#main {
|
||||
left: 14.6em;
|
||||
right: 0;
|
||||
padding: 1em;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#api-list {
|
||||
position: absolute;
|
||||
top: 3em;
|
||||
bottom: 1em;
|
||||
overflow-y: scroll;
|
||||
padding-right: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
/* App Header */
|
||||
|
||||
#header {
|
||||
background-color: #F2C200;
|
||||
border-bottom: 1px solid #957800;
|
||||
@@ -42,7 +65,7 @@ a {
|
||||
}
|
||||
|
||||
#header .angular {
|
||||
font-family: Courier New, monospace;
|
||||
font-family: "Courier New", monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -55,36 +78,48 @@ a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#section {
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#section h1 {
|
||||
/* Main main Style */
|
||||
|
||||
#main h1 {
|
||||
font-family: monospace;
|
||||
margin-top: 0;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #CCC;
|
||||
}
|
||||
|
||||
#section h2 {
|
||||
#main h2 {
|
||||
margin-top: 1.8em;
|
||||
}
|
||||
|
||||
#section h1 + h2 {
|
||||
#main h1 + h2 {
|
||||
margin-top: 1.3em;
|
||||
}
|
||||
|
||||
#section h3 {
|
||||
#main h3 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
padding: 10px 10px 20px 10px;
|
||||
background-color: #EEE;
|
||||
.main-title {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/* Searchbox & Sidebar Style */
|
||||
|
||||
#search-box, #sidebar {
|
||||
border-right: 1px solid #DDD;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
#search-box {
|
||||
width: 16em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#sidebar a {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -93,16 +128,12 @@ a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#sidebar input {
|
||||
width: 175px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#sidebar ul {
|
||||
list-style-type: none;
|
||||
/*TODO(esprehn): Can we just reset globally and not break examples?*/
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 0 0.8em 0 0;
|
||||
width: 13em;
|
||||
}
|
||||
|
||||
#sidebar ul li {
|
||||
@@ -152,6 +183,9 @@ a {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
||||
/* Warning and Info Banners */
|
||||
|
||||
.deprecated {
|
||||
border: 2px solid red;
|
||||
}
|
||||
@@ -170,8 +204,53 @@ a {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
|
||||
/* Feedback Link */
|
||||
|
||||
#feedback {
|
||||
float: right;
|
||||
width: 10em;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Live Example Style */
|
||||
|
||||
.doc-example-live table td {
|
||||
padding: 0 1.5em;
|
||||
}
|
||||
|
||||
|
||||
/* Scrollbars */
|
||||
|
||||
::-webkit-scrollbar{
|
||||
width:0.8em;
|
||||
margin: 0.2em 0em;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:hover{
|
||||
background-color:#eee;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb{
|
||||
min-height:0.8em;
|
||||
min-width:0.8em;
|
||||
-webkit-border-radius:0.5em;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover{
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active{
|
||||
background-color:#888;
|
||||
}
|
||||
|
||||
#sidebar::-webkit-scrollbar {
|
||||
background-color:#eee;
|
||||
}
|
||||
|
||||
#main::-webkit-scrollbar {
|
||||
background-color:#fff;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ function DocsController($location, $browser, $window) {
|
||||
};
|
||||
|
||||
this.afterPartialLoaded = function() {
|
||||
$window.scroll(0,0);
|
||||
SyntaxHighlighter.highlight();
|
||||
};
|
||||
|
||||
|
||||
+26
-28
@@ -1,5 +1,7 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:ng="http://angularjs.org/" xmlns:doc="http://docs.angularjs.org/" ng:controller="DocsController">
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org/"
|
||||
xmlns:doc="http://docs.angularjs.org/"
|
||||
ng:controller="DocsController">
|
||||
<head>
|
||||
<title ng:bind-template="<angular/>: {{getTitle()}}"><angular/></title>
|
||||
|
||||
@@ -10,38 +12,34 @@
|
||||
<link rel="stylesheet" href="http://alexgorbatchev.com/pub/sh/current/styles/shCore.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="http://alexgorbatchev.com/pub/sh/current/styles/shThemeDefault.css" type="text/css"/>
|
||||
|
||||
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js"></script>
|
||||
<script type="text/javascript" src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js"></script>
|
||||
<script type="text/javascript" src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
|
||||
<script src="http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js"></script>
|
||||
<script src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js"></script>
|
||||
<script src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../angular.min.js" ng:autobind></script>
|
||||
<script type="text/javascript" src="docs.js"></script>
|
||||
<script type="text/javascript" src="doc_widgets.js"></script>
|
||||
<script type="text/javascript" src="docs-data.js"></script>
|
||||
<script src="../angular.min.js" ng:autobind></script>
|
||||
<script src="docs.js"></script>
|
||||
<script src="doc_widgets.js"></script>
|
||||
<script src="docs-data.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<body style="display:none;" ng:show="true">
|
||||
<div id="header">
|
||||
<h1>
|
||||
<span class="section-title">{{getTitle()}}</span>
|
||||
<a href="index.html"><span class="angular"><angular/></span> Docs</a>
|
||||
<span class="main-title">{{getTitle()}}</span>
|
||||
<a href="#"><span class="angular"><angular/></span> Docs</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div id="page">
|
||||
<div id="sidebar" class="nav">
|
||||
<div class="doc-list">
|
||||
<input type="text" name="filterText" placeholder="search documentaiton"/>
|
||||
<ul>
|
||||
<li ng:repeat="page in pages.$filter(filterText)" ng:class="getClass(page)">
|
||||
<a href="{{getUrl(page)}}" ng:click="">{{page.name | short}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="section">
|
||||
<a id="feedback" ng:href="{{getFeedbackUrl()}}">Report an Issue or Ask a Question</a>
|
||||
<ng:include src="getCurrentPartial()" onload="afterPartialLoaded()"></ng:include>
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
<input type="text" name="search" id="search-box" placeholder="search the docs"/>
|
||||
<ul id="api-list">
|
||||
<li ng:repeat="page in pages.$filter(search)" ng:class="getClass(page)">
|
||||
<a href="{{getUrl(page)}}" ng:click="">{{page.name | short}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="main">
|
||||
<a id="feedback" ng:href="{{getFeedbackUrl()}}">Report an Issue or Ask a Question</a>
|
||||
<ng:include src="getCurrentPartial()" onload="afterPartialLoaded()"></ng:include>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,6 +19,11 @@ describe('collect', function(){
|
||||
'<pre>\nangular.k\n</pre>' +
|
||||
'<p>asdf x</p>');
|
||||
});
|
||||
|
||||
it('should replace text between two <pre></pre> tags', function() {
|
||||
expect(collect.markdown('<pre>x</pre># One<pre>b</pre>')).
|
||||
toEqual('<pre>x</pre><h1>One</h1><pre>b</pre>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('processNgDoc', function() {
|
||||
@@ -177,6 +182,12 @@ describe('collect', function(){
|
||||
TAG.returns(doc, 'returns', '{string} descrip *tion*');
|
||||
expect(doc.returns).toEqual({type: 'string', description: 'descrip <em>tion</em>'});
|
||||
});
|
||||
|
||||
it('should support multiline content', function() {
|
||||
TAG.returns(doc, 'returns', '{string} description\n new line\n another line');
|
||||
expect(doc.returns).
|
||||
toEqual({type: 'string', description: 'description\n new line\n another line'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('@description', function(){
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
|
||||
<body>
|
||||
<span ng:init='x = {d:3}; x1 = {bar:[x,5]}; x1.bar[0].d = 4'>
|
||||
<input name="x1.bar[0].d" type="text"></input>
|
||||
<input name="x.d" type="text"></input>
|
||||
<span> {{x1}} -- {{x1.bar[0].d}}</span>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
<head>
|
||||
<script type="text/javascript" src="../lib/jquery/jquery-1.4.2.js"></script>
|
||||
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
|
||||
<head>
|
||||
<body>
|
||||
<select name='selection0' style="display:block;">
|
||||
<option ng:repeat='value in ["FOO","BAR"]'">{{value}}</option>
|
||||
</select>
|
||||
{{selection0}} <-- FOO should be shown here
|
||||
|
||||
<hr/>
|
||||
|
||||
<select ng:init="selection1='ignore'" name='selection1' style="display:block;">
|
||||
<option ng:repeat='value in ["FOO","BAR"]' ng:bind-attr="{selected:'{{value==\'BAR\'}}'}">{{value}}</option>
|
||||
</select>
|
||||
{{selection1}} <-- BAR should be shown here
|
||||
|
||||
<hr/>
|
||||
|
||||
<select ng:init="selection2=1" name="selection2" style="display:block;">
|
||||
<option value="{{$index}}" ng:repeat="opt in ['zero', 'one']">{{opt}}</option>
|
||||
</select>
|
||||
{{selection2}} <-- 1 should be shown here
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
|
||||
<body>
|
||||
<textarea name="html" rows="10" cols="100"></textarea>
|
||||
<div>{{html|html}}</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
java -jar lib/jstestdriver/JsTestDriver.jar --port 9876 --browserTimeout 20000
|
||||
|
||||
+333
-433
@@ -53,6 +53,9 @@ function fromCharCode(code) { return String.fromCharCode(code); }
|
||||
var _undefined = undefined,
|
||||
_null = null,
|
||||
$$element = '$element',
|
||||
$$update = '$update',
|
||||
$$scope = '$scope',
|
||||
$$validate = '$validate',
|
||||
$angular = 'angular',
|
||||
$array = 'array',
|
||||
$boolean = 'boolean',
|
||||
@@ -69,6 +72,8 @@ var _undefined = undefined,
|
||||
$number = 'number',
|
||||
$object = 'object',
|
||||
$string = 'string',
|
||||
$value = 'value',
|
||||
$selected = 'selected',
|
||||
$undefined = 'undefined',
|
||||
NG_EXCEPTION = 'ng-exception',
|
||||
NG_VALIDATION_ERROR = 'ng-validation-error',
|
||||
@@ -87,439 +92,51 @@ var _undefined = undefined,
|
||||
push = Array.prototype.push,
|
||||
error = window[$console] ? bind(window[$console], window[$console]['error'] || noop) : noop,
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular
|
||||
* @namespace The exported angular namespace.
|
||||
*/
|
||||
angular = window[$angular] || (window[$angular] = {}),
|
||||
angularTextMarkup = extensionMap(angular, 'markup'),
|
||||
angularAttrMarkup = extensionMap(angular, 'attrMarkup'),
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.directive
|
||||
* @namespace Namespace for all directives.
|
||||
*
|
||||
* @description
|
||||
* A directive is an HTML attribute that you can use in an existing HTML element type or in a
|
||||
* DOM element type that you create as {@link angular.widget}, to modify that element's
|
||||
* properties. You can use any number of directives per element.
|
||||
*
|
||||
* For example, you can add the ng:bind directive as an attribute of an HTML span element, as in
|
||||
* `<span ng:bind="1+2"></span>`. How does this work? The compiler passes the attribute value
|
||||
* `1+2` to the ng:bind extension, which in turn tells the {@link angular.scope} to watch that
|
||||
* expression and report changes. On any change it sets the span text to the expression value.
|
||||
*
|
||||
* Here's how to define {@link angular.directive.ng:bind ng:bind}:
|
||||
* <pre>
|
||||
angular.directive('ng:bind', function(expression, compiledElement) {
|
||||
var compiler = this;
|
||||
return function(linkElement) {
|
||||
var currentScope = this;
|
||||
currentScope.$watch(expression, function(value) {
|
||||
linkElement.text(value);
|
||||
});
|
||||
};
|
||||
});
|
||||
* </pre>
|
||||
*
|
||||
* # Directive vs. Attribute Widget
|
||||
* Both [attribute widgets](#!angular.widget) and directives can compile a DOM element
|
||||
* attribute. So why have two different ways to do the same thing? The answer is that order
|
||||
* matters, but we have no control over the order in which attributes are read. To solve this
|
||||
* we apply attribute widget before the directive.
|
||||
*
|
||||
* For example, consider this piece of HTML, which uses the directives `ng:repeat`, `ng:init`,
|
||||
* and `ng:bind`:
|
||||
* <pre>
|
||||
<ul ng:init="people=['mike', 'mary']">
|
||||
<li ng:repeat="person in people" ng:init="a=a+1" ng:bind="person"></li>
|
||||
</ul>
|
||||
* </pre>
|
||||
*
|
||||
* Notice that the order of execution matters here. We need to execute
|
||||
* {@link angular.directive.ng:repeat ng:repeat} before we run the
|
||||
* {@link angular.directive.ng:init ng:init} and `ng:bind` on the `<li/>;`. This is because we
|
||||
* want to run the `ng:init="a=a+1` and `ng:bind="person"` once for each person in people. We
|
||||
* could not have used directive to create this template because attributes are read in an
|
||||
* unspecified order and there is no way of guaranteeing that the repeater attribute would
|
||||
* execute first. Using the `ng:repeat` attribute directive ensures that we can transform the
|
||||
* DOM element into a template.
|
||||
*
|
||||
* Widgets run before directives. Widgets may manipulate the DOM whereas directives are not
|
||||
* expected to do so, and so they run last.
|
||||
*/
|
||||
/** @name angular.directive */
|
||||
angularDirective = extensionMap(angular, 'directive'),
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.widget
|
||||
* @namespace Namespace for all widgets.
|
||||
* @description
|
||||
* # Overview
|
||||
* Widgets allow you to create DOM elements that the browser doesn't
|
||||
* already understand. You create the widget in your namespace and
|
||||
* assign it behavior. You can only bind one widget per DOM element
|
||||
* (unlike directives, in which you can use any number per DOM
|
||||
* element). Widgets are expected to manipulate the DOM tree by
|
||||
* adding new elements whereas directives are expected to only modify
|
||||
* element properties.
|
||||
*
|
||||
* Widgets come in two flavors: element and attribute.
|
||||
*
|
||||
* # Element Widget
|
||||
* Let's say we would like to create a new element type in the
|
||||
* namespace `my` that can watch an expression and alert() the user
|
||||
* with each new value.
|
||||
*
|
||||
* <pre>
|
||||
* <my:watch exp="name"/>
|
||||
* </pre>
|
||||
*
|
||||
* You can implement `my:watch` like this:
|
||||
* <pre>
|
||||
* angular.widget('my:watch', function(compileElement) {
|
||||
* var compiler = this;
|
||||
* var exp = compileElement.attr('exp');
|
||||
* return function(linkElement) {
|
||||
* var currentScope = this;
|
||||
* currentScope.$watch(exp, function(value){
|
||||
* alert(value);
|
||||
* }};
|
||||
* };
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* # Attribute Widget
|
||||
* Let's implement the same widget, but this time as an attribute
|
||||
* that can be added to any existing DOM element.
|
||||
* <pre>
|
||||
* <div my-watch="name">text</div>
|
||||
* </pre>
|
||||
* You can implement `my:watch` attribute like this:
|
||||
* <pre>
|
||||
* angular.widget('@my:watch', function(expression, compileElement) {
|
||||
* var compiler = this;
|
||||
* return function(linkElement) {
|
||||
* var currentScope = this;
|
||||
* currentScope.$watch(expression, function(value){
|
||||
* alert(value);
|
||||
* });
|
||||
* };
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @example
|
||||
* <script>
|
||||
* angular.widget('my:time', function(compileElement){
|
||||
* compileElement.css('display', 'block');
|
||||
* return function(linkElement){
|
||||
* function update(){
|
||||
* linkElement.text('Current time is: ' + new Date());
|
||||
* setTimeout(update, 1000);
|
||||
* }
|
||||
* update();
|
||||
* };
|
||||
* });
|
||||
* </script>
|
||||
* <my:time></my:time>
|
||||
*/
|
||||
/** @name angular.widget */
|
||||
angularWidget = extensionMap(angular, 'widget', lowercase),
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.validator
|
||||
* @namespace Namespace for all filters.
|
||||
* @description
|
||||
* # Overview
|
||||
* Validators are a standard way to check the user input against a specific criteria. For
|
||||
* example, you might need to check that an input field contains a well-formed phone number.
|
||||
*
|
||||
* # Syntax
|
||||
* Attach a validator on user input widgets using the `ng:validate` attribute.
|
||||
*
|
||||
* <doc:example>
|
||||
* <doc:source>
|
||||
* Change me: <input type="text" name="number" ng:validate="integer" value="123">
|
||||
* </doc:source>
|
||||
* <doc:scenario>
|
||||
* it('should validate the default number string', function() {
|
||||
* expect(element('input[name=number]').attr('class')).
|
||||
* not().toMatch(/ng-validation-error/);
|
||||
* });
|
||||
* it('should not validate "foo"', function() {
|
||||
* input('number').enter('foo');
|
||||
* expect(element('input[name=number]').attr('class')).
|
||||
* toMatch(/ng-validation-error/);
|
||||
* });
|
||||
* </doc:scenario>
|
||||
* </doc:example>
|
||||
*
|
||||
*
|
||||
* # Writing your own Validators
|
||||
* Writing your own validator is easy. To make a function available as a
|
||||
* validator, just define the JavaScript function on the `angular.validator`
|
||||
* object. <angular/> passes in the input to validate as the first argument
|
||||
* to your function. Any additional validator arguments are passed in as
|
||||
* additional arguments to your function.
|
||||
*
|
||||
* You can use these variables in the function:
|
||||
*
|
||||
* * `this` — The current scope.
|
||||
* * `this.$element` — The DOM element containing the binding. This allows the filter to manipulate
|
||||
* the DOM in addition to transforming the input.
|
||||
*
|
||||
* In this example we have written a upsTrackingNo validator.
|
||||
* It marks the input text "valid" only when the user enters a well-formed
|
||||
* UPS tracking number.
|
||||
*
|
||||
* @css ng-validation-error
|
||||
* When validation fails, this css class is applied to the binding, making its borders red by
|
||||
* default.
|
||||
*
|
||||
* @example
|
||||
* <script>
|
||||
* angular.validator('upsTrackingNo', function(input, format) {
|
||||
* var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$");
|
||||
* return input.match(regexp)?"":"The format must match " + format;
|
||||
* });
|
||||
* </script>
|
||||
* <input type="text" name="trackNo" size="40"
|
||||
* ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'"
|
||||
* value="1Z 123 456 78 9012 345 6"/>
|
||||
*
|
||||
* @scenario
|
||||
* it('should validate correct UPS tracking number', function() {
|
||||
* expect(element('input[name=trackNo]').attr('class')).
|
||||
* not().toMatch(/ng-validation-error/);
|
||||
* });
|
||||
*
|
||||
* it('should not validate in correct UPS tracking number', function() {
|
||||
* input('trackNo').enter('foo');
|
||||
* expect(element('input[name=trackNo]').attr('class')).
|
||||
* toMatch(/ng-validation-error/);
|
||||
* });
|
||||
*
|
||||
*/
|
||||
/** @name angular.validator */
|
||||
angularValidator = extensionMap(angular, 'validator'),
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.filter
|
||||
* @namespace Namespace for all filters.
|
||||
* @description
|
||||
* # Overview
|
||||
* Filters are a standard way to format your data for display to the user. For example, you
|
||||
* might have the number 1234.5678 and would like to display it as US currency: $1,234.57.
|
||||
* Filters allow you to do just that. In addition to transforming the data, filters also modify
|
||||
* the DOM. This allows the filters to for example apply css styles to the filtered output if
|
||||
* certain conditions were met.
|
||||
*
|
||||
*
|
||||
* # Standard Filters
|
||||
*
|
||||
* The Angular framework provides a standard set of filters for common operations, including:
|
||||
* {@link angular.filter.currency currency}, {@link angular.filter.json json},
|
||||
* {@link angular.filter.number number}, and {@link angular.filter.html html}. You can also add
|
||||
* your own filters.
|
||||
*
|
||||
*
|
||||
* # Syntax
|
||||
*
|
||||
* Filters can be part of any {@link angular.scope} evaluation but are typically used with
|
||||
* {{bindings}}. Filters typically transform the data to a new data type, formating the data in
|
||||
* the process. Filters can be chained and take optional arguments. Here are few examples:
|
||||
*
|
||||
* * No filter: {{1234.5678}} => 1234.5678
|
||||
* * Number filter: {{1234.5678|number}} => 1,234.57. Notice the “,” and rounding to two
|
||||
* significant digits.
|
||||
* * Filter with arguments: {{1234.5678|number:5}} => 1,234.56780. Filters can take optional
|
||||
* arguments, separated by colons in a binding. To number, the argument “5” requests 5 digits
|
||||
* to the right of the decimal point.
|
||||
*
|
||||
*
|
||||
* # Writing your own Filters
|
||||
*
|
||||
* Writing your own filter is very easy: just define a JavaScript function on `angular.filter`.
|
||||
* The framework passes in the input value as the first argument to your function. Any filter
|
||||
* arguments are passed in as additional function arguments.
|
||||
*
|
||||
* You can use these variables in the function:
|
||||
*
|
||||
* * `this` — The current scope.
|
||||
* * `this.$element` — The DOM element containing the binding. This allows the filter to manipulate
|
||||
* the DOM in addition to transforming the input.
|
||||
*
|
||||
*
|
||||
* @exampleDescription
|
||||
* The following example filter reverses a text string. In addition, it conditionally makes the
|
||||
* text upper-case (to demonstrate optional arguments) and assigns color (to demonstrate DOM
|
||||
* modification).
|
||||
*
|
||||
* @example
|
||||
<script type="text/javascript">
|
||||
angular.filter('reverse', function(input, uppercase, color) {
|
||||
var out = "";
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
out = input.charAt(i) + out;
|
||||
}
|
||||
if (uppercase) {
|
||||
out = out.toUpperCase();
|
||||
}
|
||||
if (color) {
|
||||
this.$element.css('color', color);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
</script>
|
||||
|
||||
<input name="text" type="text" value="hello" /><br>
|
||||
No filter: {{text}}<br>
|
||||
Reverse: {{text|reverse}}<br>
|
||||
Reverse + uppercase: {{text|reverse:true}}<br>
|
||||
Reverse + uppercase + blue: {{text|reverse:true:"blue"}}
|
||||
|
||||
*/
|
||||
/** @name angular.fileter */
|
||||
angularFilter = extensionMap(angular, 'filter'),
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.formatter
|
||||
* @namespace Namespace for all formats.
|
||||
* @description
|
||||
* # Overview
|
||||
* The formatters are responsible for translating user readable text in an input widget to a
|
||||
* data model stored in an application.
|
||||
*
|
||||
* # Writting your own Formatter
|
||||
* Writing your own formatter is easy. Just register a pair of JavaScript functions with
|
||||
* `angular.formatter`. One function for parsing user input text to the stored form,
|
||||
* and one for formatting the stored data to user-visible text.
|
||||
*
|
||||
* Here is an example of a "reverse" formatter: The data is stored in uppercase and in
|
||||
* reverse, while it is displayed in lower case and non-reversed. User edits are
|
||||
* automatically parsed into the internal form and data changes are automatically
|
||||
* formatted to the viewed form.
|
||||
*
|
||||
* <pre>
|
||||
* function reverse(text) {
|
||||
* var reversed = [];
|
||||
* for (var i = 0; i < text.length; i++) {
|
||||
* reversed.unshift(text.charAt(i));
|
||||
* }
|
||||
* return reversed.join('');
|
||||
* }
|
||||
*
|
||||
* angular.formatter('reverse', {
|
||||
* parse: function(value){
|
||||
* return reverse(value||'').toUpperCase();
|
||||
* },
|
||||
* format: function(value){
|
||||
* return reverse(value||'').toLowerCase();
|
||||
* }
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @example
|
||||
* <script type="text/javascript">
|
||||
* function reverse(text) {
|
||||
* var reversed = [];
|
||||
* for (var i = 0; i < text.length; i++) {
|
||||
* reversed.unshift(text.charAt(i));
|
||||
* }
|
||||
* return reversed.join('');
|
||||
* }
|
||||
*
|
||||
* angular.formatter('reverse', {
|
||||
* parse: function(value){
|
||||
* return reverse(value||'').toUpperCase();
|
||||
* },
|
||||
* format: function(value){
|
||||
* return reverse(value||'').toLowerCase();
|
||||
* }
|
||||
* });
|
||||
* </script>
|
||||
*
|
||||
* Formatted:
|
||||
* <input type="text" name="data" value="angular" ng:format="reverse"/>
|
||||
* <br/>
|
||||
*
|
||||
* Stored:
|
||||
* <input type="text" name="data"/><br/>
|
||||
* <pre>{{data}}</pre>
|
||||
*
|
||||
*
|
||||
* @scenario
|
||||
* it('should store reverse', function(){
|
||||
* expect(element('.doc-example input:first').val()).toEqual('angular');
|
||||
* expect(element('.doc-example input:last').val()).toEqual('RALUGNA');
|
||||
*
|
||||
* this.addFutureAction('change to XYZ', function($window, $document, done){
|
||||
* $document.elements('.doc-example input:last').val('XYZ').trigger('change');
|
||||
* done();
|
||||
* });
|
||||
* expect(element('.doc-example input:first').val()).toEqual('zyx');
|
||||
* });
|
||||
*/
|
||||
/** @name angular.formatter */
|
||||
angularFormatter = extensionMap(angular, 'formatter'),
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.service
|
||||
*
|
||||
* @description
|
||||
* # Overview
|
||||
* Services are substituable objects, which are wired together using dependency injection.
|
||||
* Each service could have dependencies (other services), which are passed in constructor.
|
||||
* Because JS is dynamicaly typed language, dependency injection can not use static types
|
||||
* to satisfy these dependencies, so each service must explicitely define its dependencies.
|
||||
* This is done by $inject property.
|
||||
*
|
||||
* For now, life time of all services is the same as the life time of page.
|
||||
*
|
||||
*
|
||||
* # Standard services
|
||||
* The Angular framework provides a standard set of services for common operations.
|
||||
* You can write your own services and rewrite these standard services as well.
|
||||
* Like other core angular variables, standard services always start with $.
|
||||
*
|
||||
* * `angular.service.$window`
|
||||
* * `angular.service.$document`
|
||||
* * `angular.service.$location`
|
||||
* * `angular.service.$log`
|
||||
* * `angular.service.$exceptionHandler`
|
||||
* * `angular.service.$hover`
|
||||
* * `angular.service.$invalidWidgets`
|
||||
* * `angular.service.$route`
|
||||
* * `angular.service.$xhr`
|
||||
* * `angular.service.$xhr.error`
|
||||
* * `angular.service.$xhr.bulk`
|
||||
* * `angular.service.$xhr.cache`
|
||||
* * `angular.service.$resource`
|
||||
* * `angular.service.$cookies`
|
||||
* * `angular.service.$cookieStore`
|
||||
*
|
||||
* # Writing your own services
|
||||
* <pre>
|
||||
* angular.service('notify', function(location) {
|
||||
* this.one = function() {
|
||||
* }
|
||||
* }, {$inject: ['$location']});
|
||||
* </pre>
|
||||
*
|
||||
* # Using services in controller
|
||||
*/
|
||||
/** @name angular.service */
|
||||
angularService = extensionMap(angular, 'service'),
|
||||
angularCallbacks = extensionMap(angular, 'callbacks'),
|
||||
nodeName,
|
||||
rngScript = /^(|.*\/)angular(-.*?)?(\.min)?.js(\?[^#]*)?(#(.*))?$/;
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.foreach
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Invokes the `iterator` function once for each item in `obj` collection. The collection can either
|
||||
* be an object or an array. The `iterator` function is invoked with `iterator(value, key)`, where
|
||||
* `value` is the value of an object property or an array element and `key` is the object property
|
||||
* key or array element index. Optionally, `context` can be specified for the iterator function.
|
||||
*
|
||||
<pre>
|
||||
var values = {name: 'misko', gender: 'male'};
|
||||
var log = [];
|
||||
angular.foreach(values, function(value, key){
|
||||
this.push(key + ': ' + value);
|
||||
}, log);
|
||||
expect(log).toEqual(['name: misko', 'gender:male']);
|
||||
</pre>
|
||||
*
|
||||
* @param {Object|Array} obj Object to iterate over.
|
||||
* @param {function()} iterator Iterator function.
|
||||
* @param {Object} context Object to become context (`this`) for the iterator function.
|
||||
* @returns {Objet|Array} Reference to `obj`.
|
||||
*/
|
||||
function foreach(obj, iterator, context) {
|
||||
var key;
|
||||
if (obj) {
|
||||
@@ -565,6 +182,19 @@ function formatError(arg) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.extend
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Extends the destination object `dst` by copying all of the properties from the `src` objects to
|
||||
* `dst`. You can specify multiple `src` objects.
|
||||
*
|
||||
* @param {Object} dst The destination object.
|
||||
* @param {...Object} src The source object(s).
|
||||
*/
|
||||
function extend(dst) {
|
||||
foreach(arguments, function(obj){
|
||||
if (obj !== dst) {
|
||||
@@ -576,14 +206,53 @@ function extend(dst) {
|
||||
return dst;
|
||||
}
|
||||
|
||||
|
||||
function inherit(parent, extra) {
|
||||
return extend(new (extend(function(){}, {prototype:parent}))(), extra);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.noop
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Empty function that performs no operation whatsoever. This function is useful when writing code
|
||||
* in the functional style.
|
||||
<pre>
|
||||
function foo(callback) {
|
||||
var result = calculateResult();
|
||||
(callback || angular.noop)(result);
|
||||
}
|
||||
</pre>
|
||||
*/
|
||||
function noop() {}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.identity
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* A function that does nothing except for returning its first argument. This function is useful
|
||||
* when writing code in the functional style.
|
||||
*
|
||||
<pre>
|
||||
function transformer(transformationFn, value) {
|
||||
return (transformationFn || identity)(value);
|
||||
};
|
||||
</pre>
|
||||
*/
|
||||
function identity($) {return $;}
|
||||
|
||||
|
||||
function valueFn(value) {return function(){ return value; };}
|
||||
|
||||
|
||||
function extensionMap(angular, name, transform) {
|
||||
var extPoint;
|
||||
return angular[name] || (extPoint = angular[name] = function (name, fn, prop){
|
||||
@@ -614,14 +283,129 @@ function jqLiteWrap(element) {
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isUndefined
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if a reference is undefined.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is undefined.
|
||||
*/
|
||||
function isUndefined(value){ return typeof value == $undefined; }
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isDefined
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if a reference is defined.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is defined.
|
||||
*/
|
||||
function isDefined(value){ return typeof value != $undefined; }
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isObject
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if a reference is an `Object`. Unlike in JavaScript `null`s are not considered to be
|
||||
* objects.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is an `Object` but not `null`.
|
||||
*/
|
||||
function isObject(value){ return value!=_null && typeof value == $object;}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isString
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if a reference is a `String`.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is a `String`.
|
||||
*/
|
||||
function isString(value){ return typeof value == $string;}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isNumber
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if a reference is a `Number`.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is a `Number`.
|
||||
*/
|
||||
function isNumber(value){ return typeof value == $number;}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isDate
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if value is a date.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is a `Date`.
|
||||
*/
|
||||
function isDate(value){ return value instanceof Date; }
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isArray
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if a reference is an `Array`.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is an `Array`.
|
||||
*/
|
||||
function isArray(value) { return value instanceof Array; }
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.isFunction
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Checks if a reference is a `Function`.
|
||||
*
|
||||
* @param {*} value Reference to check.
|
||||
* @returns {boolean} True if `value` is a `Function`.
|
||||
*/
|
||||
function isFunction(value){ return typeof value == $function;}
|
||||
|
||||
|
||||
function isBoolean(value) { return typeof value == $boolean;}
|
||||
function isTextNode(node) { return nodeName(node) == '#text'; }
|
||||
function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; }
|
||||
@@ -675,6 +459,27 @@ function map(obj, iterator, context) {
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Object.size
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Determines the number of elements in an array or number of properties of an object.
|
||||
*
|
||||
* Note: this function is used to augment the Object type in angular expressions. See
|
||||
* {@link angular.Object} for more info.
|
||||
*
|
||||
* @param {Object|Array} obj Object or array to inspect.
|
||||
* @returns {number} The size of `obj` or `0` if `obj` is not an object or array.
|
||||
*
|
||||
* @example
|
||||
* Number of items in array: {{ [1,2].$size() }}<br/>
|
||||
* Number of items in object: {{ {a:1, b:2, c:3}.$size() }}<br/>
|
||||
*/
|
||||
function size(obj) {
|
||||
var size = 0;
|
||||
if (obj) {
|
||||
@@ -714,19 +519,40 @@ function isLeafNode (node) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies stuff.
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Object.copy
|
||||
* @function
|
||||
*
|
||||
* If destination is not provided and source is an object or an array, a copy is created & returned,
|
||||
* otherwise the source is returned.
|
||||
* @description
|
||||
* Creates a deep copy of `source`.
|
||||
*
|
||||
* If destination is provided, all of its properties will be deleted and if source is an object or
|
||||
* an array, all of its members will be copied into the destination object. Finally the destination
|
||||
* is returned just for kicks.
|
||||
* If `destination` is not provided and `source` is an object or an array, a copy is created &
|
||||
* returned, otherwise the `source` is returned.
|
||||
*
|
||||
* @param {*} source The source to be used during copy.
|
||||
* Can be any type including primitives, null and undefined.
|
||||
* @param {(Object|Array)=} destination Optional destination into which the source is copied
|
||||
* @returns {*}
|
||||
* If `destination` is provided, all of its properties will be deleted.
|
||||
*
|
||||
* If `source` is an object or an array, all of its members will be copied into the `destination`
|
||||
* object.
|
||||
*
|
||||
* Note: this function is used to augment the Object type in angular expressions. See
|
||||
* {@link angular.Object} for more info.
|
||||
*
|
||||
* @param {*} source The source to be used to make a copy.
|
||||
* Can be any type including primitives, `null` and `undefined`.
|
||||
* @param {(Object|Array)=} destination Optional destination into which the source is copied.
|
||||
* @returns {*} The copy or updated `destination` if `destination` was specified.
|
||||
*
|
||||
* @example
|
||||
Salutation: <input type="text" name="master.salutation" value="Hello" /><br/>
|
||||
Name: <input type="text" name="master.name" value="world"/><br/>
|
||||
<button ng:click="form = master.$copy()">copy</button>
|
||||
<hr/>
|
||||
|
||||
Master is <span ng:hide="master.$equals(form)">NOT</span> same as form.
|
||||
|
||||
<pre>master={{master}}</pre>
|
||||
<pre>form={{form}}</pre>
|
||||
*/
|
||||
function copy(source, destination){
|
||||
if (!destination) {
|
||||
@@ -760,6 +586,42 @@ function copy(source, destination){
|
||||
return destination;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Object.equals
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Determines if two objects or value are equivalent.
|
||||
*
|
||||
* To be equivalent, they must pass `==` comparison or be of the same type and have all their
|
||||
* properties pass `==` comparison.
|
||||
*
|
||||
* Supports values types, arrays and objects.
|
||||
*
|
||||
* For objects `function` properties and properties that start with `$` are not considered during
|
||||
* comparisons.
|
||||
*
|
||||
* Note: this function is used to augment the Object type in angular expressions. See
|
||||
* {@link angular.Object} for more info.
|
||||
*
|
||||
* @param {*} o1 Object or value to compare.
|
||||
* @param {*} o2 Object or value to compare.
|
||||
* @returns {boolean} True if arguments are equal.
|
||||
*
|
||||
* @example
|
||||
Salutation: <input type="text" name="master.salutation" value="Hello" /><br/>
|
||||
Name: <input type="text" name="master.name" value="world"/><br/>
|
||||
<button ng:click="form = master.$copy()">copy</button>
|
||||
<hr/>
|
||||
|
||||
Master is <span ng:hide="master.$equals(form)">NOT</span> same as form.
|
||||
|
||||
<pre>master={{master}}</pre>
|
||||
<pre>form={{form}}</pre>
|
||||
*/
|
||||
function equals(o1, o2) {
|
||||
if (o1 == o2) return true;
|
||||
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
|
||||
@@ -823,6 +685,23 @@ function concat(array1, array2, index) {
|
||||
return array1.concat(slice.call(array2, index, array2.length));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.bind
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Returns function which calls function `fn` bound to `self` (`self` becomes the `this` for `fn`).
|
||||
* Optional `args` can be supplied which are prebound to the function, also known as
|
||||
* [function currying](http://en.wikipedia.org/wiki/Currying).
|
||||
*
|
||||
* @param {Object} self Context in which `fn` should be evaluated in.
|
||||
* @param {function()} fn Function to be bound.
|
||||
* @param {...*} args Optional arguments to be prebound to the `fn` function call.
|
||||
* @returns {function()} Function that wraps the `fn` with all the specified bindings.
|
||||
*/
|
||||
function bind(self, fn) {
|
||||
var curryArgs = arguments.length > 2 ? slice.call(arguments, 2, arguments.length) : [];
|
||||
if (typeof fn == $function) {
|
||||
@@ -832,7 +711,7 @@ function bind(self, fn) {
|
||||
return arguments.length ? fn.apply(self, arguments) : fn.call(self);
|
||||
};
|
||||
} else {
|
||||
// in IE, native methods ore not functions and so they can not be bound (but they don't need to be)
|
||||
// in IE, native methods are not functions and so they can not be bound (but they don't need to be)
|
||||
return fn;
|
||||
}
|
||||
}
|
||||
@@ -860,10 +739,31 @@ function merge(src, dst) {
|
||||
}
|
||||
}
|
||||
|
||||
function compile(element, existingScope) {
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.compile
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Compiles a piece of HTML or DOM into a {@link angular.scope scope} object.
|
||||
<pre>
|
||||
var scope1 = angular.compile(window.document);
|
||||
scope1.$init();
|
||||
|
||||
var scope2 = angular.compile('<div ng:click="clicked = true">click me</div>');
|
||||
scope2.$init();
|
||||
</pre>
|
||||
*
|
||||
* @param {string|DOMElement} element Element to compile.
|
||||
* @param {Object=} parentScope Scope to become the parent scope of the newly compiled scope.
|
||||
* @returns {Object} Compiled scope object.
|
||||
*/
|
||||
function compile(element, parentScope) {
|
||||
var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget),
|
||||
$element = jqLite(element);
|
||||
return compiler.compile($element)($element, existingScope);
|
||||
return compiler.compile($element)($element, parentScope);
|
||||
}
|
||||
/////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ angularService('$browser', function($log){
|
||||
jqLite(window.document),
|
||||
jqLite(window.document.getElementsByTagName('head')[0]),
|
||||
XHR,
|
||||
$log);
|
||||
$log,
|
||||
window.setTimeout);
|
||||
browserSingleton.startPoller(50, function(delay, fn){setTimeout(delay,fn);});
|
||||
browserSingleton.bind();
|
||||
}
|
||||
|
||||
+45
-14
@@ -8,7 +8,7 @@ var XHR = window.XMLHttpRequest || function () {
|
||||
throw new Error("This browser does not support XMLHttpRequest.");
|
||||
};
|
||||
|
||||
function Browser(location, document, head, XHR, $log) {
|
||||
function Browser(location, document, head, XHR, $log, setTimeout) {
|
||||
var self = this;
|
||||
self.isMock = false;
|
||||
|
||||
@@ -19,6 +19,28 @@ function Browser(location, document, head, XHR, $log) {
|
||||
var outstandingRequestCount = 0;
|
||||
var outstandingRequestCallbacks = [];
|
||||
|
||||
|
||||
/**
|
||||
* Executes the `fn` function (supports currying) and decrements the `outstandingRequestCallbacks`
|
||||
* counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
|
||||
*/
|
||||
function completeOutstandingRequest(fn) {
|
||||
try {
|
||||
fn.apply(null, slice.call(arguments, 1));
|
||||
} finally {
|
||||
outstandingRequestCount--;
|
||||
if (outstandingRequestCount === 0) {
|
||||
while(outstandingRequestCallbacks.length) {
|
||||
try {
|
||||
outstandingRequestCallbacks.pop()();
|
||||
} catch (e) {
|
||||
$log.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc method
|
||||
@@ -58,19 +80,7 @@ function Browser(location, document, head, XHR, $log) {
|
||||
outstandingRequestCount ++;
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
try {
|
||||
callback(xhr.status || 200, xhr.responseText);
|
||||
} finally {
|
||||
outstandingRequestCount--;
|
||||
if (outstandingRequestCount === 0) {
|
||||
while(outstandingRequestCallbacks.length) {
|
||||
try {
|
||||
outstandingRequestCallbacks.pop()();
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
completeOutstandingRequest(callback, xhr.status || 200, xhr.responseText);
|
||||
}
|
||||
};
|
||||
xhr.send(post || '');
|
||||
@@ -250,6 +260,27 @@ function Browser(location, document, head, XHR, $log) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc
|
||||
* @name angular.service.$browser#defer
|
||||
* @methodOf angular.service.$browser
|
||||
*
|
||||
* @description
|
||||
* Executes a fn asynchroniously via `setTimeout(fn, 0)`.
|
||||
*
|
||||
* Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using
|
||||
* `setTimeout` in tests, the fns are queued in an array, which can be programaticaly flushed via
|
||||
* `$browser.defer.flush()`.
|
||||
*
|
||||
* @param {function()} fn A function, who's execution should be defered.
|
||||
*/
|
||||
self.defer = function(fn) {
|
||||
outstandingRequestCount++;
|
||||
setTimeout(function() { completeOutstandingRequest(fn); }, 0);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Misc API
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -30,6 +30,7 @@ Template.prototype = {
|
||||
if (this.newScope) {
|
||||
childScope = createScope(scope);
|
||||
scope.$onEval(childScope.$eval);
|
||||
element.data($$scope, childScope);
|
||||
}
|
||||
foreach(this.inits, function(fn) {
|
||||
queue.push(function() {
|
||||
@@ -68,6 +69,17 @@ Template.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Function walks up the element chain looking for the scope associated with the give element.
|
||||
*/
|
||||
function retrieveScope(element) {
|
||||
var scope;
|
||||
while (element && !(scope = element.data($$scope))) {
|
||||
element = element.parent();
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
//Compiler
|
||||
//////////////////////////////////
|
||||
@@ -97,6 +109,7 @@ Compiler.prototype = {
|
||||
element = jqLite(element);
|
||||
var scope = parentScope && parentScope.$eval ?
|
||||
parentScope : createScope(parentScope);
|
||||
element.data($$scope, scope);
|
||||
return extend(scope, {
|
||||
$element:element,
|
||||
$init: function() {
|
||||
|
||||
+16
-5
@@ -1,9 +1,20 @@
|
||||
/**
|
||||
* Create an inject method
|
||||
* @param providerScope provider's "this"
|
||||
* @param providers a function(name) which returns provider function
|
||||
* @param cache place where instances are saved for reuse
|
||||
* @returns {Function}
|
||||
* @ngdoc function
|
||||
* @name angular.injector
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Creates an inject function that can be used for dependency injection.
|
||||
*
|
||||
* @param {Object=} [providerScope={}] provider's `this`
|
||||
* @param {Object.<string, function()>=} [providers=angular.service] Map of provider (factory)
|
||||
* function.
|
||||
* @param {Object.<string, function()>=} [cache={}] Place where instances are saved for reuse. Can
|
||||
* also be used to override services speciafied by `providers` (useful in tests).
|
||||
* @returns {function()} Injector function.
|
||||
*
|
||||
* @TODO These docs need a lot of work. Specifically the returned function should be described in
|
||||
* great detail + we need to provide some examples.
|
||||
*/
|
||||
function createInjector(providerScope, providers, cache) {
|
||||
providers = providers || angularService;
|
||||
|
||||
+62
-29
@@ -66,7 +66,7 @@ function getterFn(path){
|
||||
code += 'if(!s) return s;\n' +
|
||||
'l=s;\n' +
|
||||
's=s' + key + ';\n' +
|
||||
'if(typeof s=="function") s = function(){ return l'+key+'.apply(l, arguments); };\n';
|
||||
'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l'+key+'.apply(l, arguments); };\n';
|
||||
if (key.charAt(1) == '$') {
|
||||
// special code for super-imposed functions
|
||||
var name = key.substr(2);
|
||||
@@ -191,7 +191,16 @@ function errorHandlerFor(element, error) {
|
||||
* Note: A widget that creates scopes (i.e. {@link angular.widget.@ng:repeat ng:repeat}) is
|
||||
* responsible for forwarding `$eval()` calls from the parent to those child scopes. That way,
|
||||
* calling $eval() on the root scope will update the whole page.
|
||||
|
||||
*
|
||||
*
|
||||
* @TODO THESE PARAMS AND RETURNS ARE NOT RENDERED IN THE TEMPLATE!! FIX THAT!
|
||||
* @param {Object} parent The scope that should become the parent for the newly created scope.
|
||||
* @param {Object.<string, function()>=} providers Map of service factory which need to be provided
|
||||
* for the current scope. Usually {@link angular.service}.
|
||||
* @param {Object.<string, *>=} instanceCache Provides pre-instantiated services which should
|
||||
* append/override services provided by `providers`.
|
||||
* @returns {Object} Newly created scope.
|
||||
*
|
||||
*
|
||||
* @exampleDescription
|
||||
* This example demonstrates scope inheritance and property overriding.
|
||||
@@ -234,7 +243,6 @@ function createScope(parent, providers, instanceCache) {
|
||||
parent = Parent.prototype = (parent || {});
|
||||
var instance = new Parent();
|
||||
var evalLists = {sorted:[]};
|
||||
var postList = [], postHash = {}, postId = 0;
|
||||
|
||||
extend(instance, {
|
||||
'this': instance,
|
||||
@@ -261,7 +269,58 @@ function createScope(parent, providers, instanceCache) {
|
||||
* @param {function()} fn Function to be bound.
|
||||
*/
|
||||
$bind: bind(instance, bind, instance),
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.scope.$get
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Returns the value for `property_chain` on the current scope. Unlike in JavaScript, if there
|
||||
* are any `undefined` intermediary properties, `undefined` is returned instead of throwing an
|
||||
* exception.
|
||||
*
|
||||
<pre>
|
||||
var scope = angular.scope();
|
||||
expect(scope.$get('person.name')).toEqual(undefined);
|
||||
scope.person = {};
|
||||
expect(scope.$get('person.name')).toEqual(undefined);
|
||||
scope.person.name = 'misko';
|
||||
expect(scope.$get('person.name')).toEqual('misko');
|
||||
</pre>
|
||||
*
|
||||
* @param {string} property_chain String representing name of a scope property. Optionally
|
||||
* properties can be chained with `.` (dot), e.g. `'person.name.first'`
|
||||
* @returns {*} Value for the (nested) property.
|
||||
*/
|
||||
$get: bind(instance, getter, instance),
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.scope.$set
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Assigns a value to a property of the current scope specified via `property_chain`. Unlike in
|
||||
* JavaScript, if there are any `undefined` intermediary properties, empty objects are created
|
||||
* and assigned in to them instead of throwing an exception.
|
||||
*
|
||||
<pre>
|
||||
var scope = angular.scope();
|
||||
expect(scope.person).toEqual(undefined);
|
||||
scope.$set('person.name', 'misko');
|
||||
expect(scope.person).toEqual({name:'misko'});
|
||||
expect(scope.person.name).toEqual('misko');
|
||||
</pre>
|
||||
*
|
||||
* @param {string} property_chain String representing name of a scope property. Optionally
|
||||
* properties can be chained with `.` (dot), e.g. `'person.name.first'`
|
||||
* @param {*} value Value to assign to the scope property.
|
||||
*/
|
||||
$set: bind(instance, setter, instance),
|
||||
|
||||
|
||||
@@ -311,11 +370,6 @@ function createScope(parent, providers, instanceCache) {
|
||||
instance.$tryEval(queue[j].fn, queue[j].handler);
|
||||
}
|
||||
}
|
||||
while(postList.length) {
|
||||
fn = postList.shift();
|
||||
delete postHash[fn.$postEvalId];
|
||||
instance.$tryEval(fn);
|
||||
}
|
||||
} else if (type === $function) {
|
||||
return exp.call(instance);
|
||||
} else if (type === 'string') {
|
||||
@@ -489,27 +543,6 @@ function createScope(parent, providers, instanceCache) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.scope.$postEval
|
||||
* @function
|
||||
*/
|
||||
$postEval: function(expr) {
|
||||
if (expr) {
|
||||
var fn = expressionCompile(expr);
|
||||
var id = fn.$postEvalId;
|
||||
if (!id) {
|
||||
id = '$' + instance.$id + "_" + (postId++);
|
||||
fn.$postEvalId = id;
|
||||
}
|
||||
if (!postHash[id]) {
|
||||
postList.push(postHash[id] = fn);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
|
||||
+496
-1
@@ -11,6 +11,26 @@ var angularGlobal = {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.Object
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Utility functions for manipulation with JavaScript objects.
|
||||
*
|
||||
* These functions are exposed in two ways:
|
||||
*
|
||||
* - **in angular expressions**: the functions are bound to all objects and augment the Object
|
||||
* type. The names of these methods are prefixed with `$` character to minimize naming collisions.
|
||||
* To call a method, invoke the function without the first argument, e.g, `myObject.$foo(param2)`.
|
||||
*
|
||||
* - **in JavaScript code**: the functions don't augment the Object type and must be invoked as
|
||||
* functions of `angular.Object` as `angular.Object.foo(myObject, param2)`.
|
||||
*
|
||||
*/
|
||||
var angularCollection = {
|
||||
'copy': copy,
|
||||
'size': size,
|
||||
@@ -19,8 +39,121 @@ var angularCollection = {
|
||||
var angularObject = {
|
||||
'extend': extend
|
||||
};
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc overview
|
||||
* @name angular.Array
|
||||
*
|
||||
* @description
|
||||
* Utility functions for manipulation with JavaScript Array objects.
|
||||
*
|
||||
* These functions are exposed in two ways:
|
||||
*
|
||||
* - **in angular expressions**: the functions are bound to the Array objects and augment the Array
|
||||
* type as array methods. The names of these methods are prefixed with `$` character to minimize
|
||||
* naming collisions. To call a method, invoke `myArrayObject.$foo(params)`.
|
||||
*
|
||||
* - **in JavaScript code**: the functions don't augment the Array type and must be invoked as
|
||||
* functions of `angular.Array` as `angular.Array.foo(myArrayObject, params)`.
|
||||
*
|
||||
*/
|
||||
var angularArray = {
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.indexOf
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Determines the index of `value` in `array`.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array Array to search.
|
||||
* @param {*} value Value to search for.
|
||||
* @returns {number} The position of the element in `array`. The position is 0-based. `-1` is returned if the value can't be found.
|
||||
*
|
||||
* @example
|
||||
<div ng:init="books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet']"></div>
|
||||
<input name='bookName' value='Romeo and Juliet'> <br>
|
||||
Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
|
||||
|
||||
@scenario
|
||||
it('should correctly calculate the initial index', function() {
|
||||
expect(binding('books.$indexOf(bookName)')).toBe('2');
|
||||
});
|
||||
|
||||
it('should recalculate', function() {
|
||||
input('bookName').enter('foo');
|
||||
expect(binding('books.$indexOf(bookName)')).toBe('-1');
|
||||
|
||||
input('bookName').enter('Moby Dick');
|
||||
expect(binding('books.$indexOf(bookName)')).toBe('0');
|
||||
});
|
||||
*/
|
||||
'indexOf': indexOf,
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.sum
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* This function calculates the sum of all numbers in `array`. If the `expressions` is supplied,
|
||||
* it is evaluated once for each element in `array` and then the sum of these values is returned.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array The source array.
|
||||
* @param {(string|function())=} expression Angular expression or a function to be evaluated for each
|
||||
* element in `array`. The array element becomes the `this` during the evaluation.
|
||||
* @returns {number} Sum of items in the array.
|
||||
*
|
||||
* @example
|
||||
<table ng:init="invoice= {items:[{qty:10, description:'gadget', cost:9.95}]}">
|
||||
<tr><th>Qty</th><th>Description</th><th>Cost</th><th>Total</th><th></th></tr>
|
||||
<tr ng:repeat="item in invoice.items">
|
||||
<td><input name="item.qty" value="1" size="4" ng:required ng:validate="integer"></td>
|
||||
<td><input name="item.description"></td>
|
||||
<td><input name="item.cost" value="0.00" ng:required ng:validate="number" size="6"></td>
|
||||
<td>{{item.qty * item.cost | currency}}</td>
|
||||
<td>[<a href ng:click="invoice.items.$remove(item)">X</a>]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href ng:click="invoice.items.$add()">add item</a></td>
|
||||
<td></td>
|
||||
<td>Total:</td>
|
||||
<td>{{invoice.items.$sum('qty*cost') | currency}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@scenario
|
||||
//TODO: these specs are lame because I had to work around issues #164 and #167
|
||||
it('should initialize and calculate the totals', function() {
|
||||
expect(repeater('.doc-example-live table tr', 'item in invoice.items').count()).toBe(3);
|
||||
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(1)).
|
||||
toEqual(['$99.50']);
|
||||
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
|
||||
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
|
||||
});
|
||||
|
||||
it('should add an entry and recalculate', function() {
|
||||
element('.doc-example a:contains("add item")').click();
|
||||
using('.doc-example-live tr:nth-child(3)').input('item.qty').enter('20');
|
||||
using('.doc-example-live tr:nth-child(3)').input('item.cost').enter('100');
|
||||
|
||||
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(2)).
|
||||
toEqual(['$2,000.00']);
|
||||
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$2,099.50');
|
||||
});
|
||||
*/
|
||||
'sum':function(array, expression) {
|
||||
var fn = angular['Function']['compile'](expression);
|
||||
var sum = 0;
|
||||
@@ -32,12 +165,140 @@ var angularArray = {
|
||||
}
|
||||
return sum;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.remove
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Modifies `array` by removing an element from it. The element will be looked up using the
|
||||
* {@link angular.Array.indexOf indexOf} function on the `array` and only the first instance of
|
||||
* the element will be removed.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array Array from which an element should be removed.
|
||||
* @param {*} value Element to be removed.
|
||||
* @returns {*} The removed element.
|
||||
*
|
||||
* @example
|
||||
<ul ng:init="tasks=['Learn Angular', 'Read Documentation',
|
||||
'Check out demos', 'Build cool applications']">
|
||||
<li ng:repeat="task in tasks">
|
||||
{{task}} [<a href="" ng:click="tasks.$remove(task)">X</a>]
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
tasks = {{tasks}}
|
||||
|
||||
@scenario
|
||||
it('should initialize the task list with for tasks', function() {
|
||||
expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(4);
|
||||
expect(repeater('.doc-example ul li', 'task in tasks').column('task')).
|
||||
toEqual(['Learn Angular', 'Read Documentation', 'Check out demos',
|
||||
'Build cool applications']);
|
||||
});
|
||||
|
||||
it('should initialize the task list with for tasks', function() {
|
||||
element('.doc-example ul li a:contains("X"):first').click();
|
||||
expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(3);
|
||||
|
||||
element('.doc-example ul li a:contains("X"):last').click();
|
||||
expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(2);
|
||||
|
||||
expect(repeater('.doc-example ul li', 'task in tasks').column('task')).
|
||||
toEqual(['Read Documentation', 'Check out demos']);
|
||||
});
|
||||
*/
|
||||
'remove':function(array, value) {
|
||||
var index = indexOf(array, value);
|
||||
if (index >=0)
|
||||
array.splice(index, 1);
|
||||
return value;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.filter
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Selects a subset of items from `array` and returns it as a new array.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array The source array.
|
||||
* @param {string|Object|function()} expression The predicate to be used for selecting items from
|
||||
* `array`.
|
||||
*
|
||||
* Can be one of:
|
||||
*
|
||||
* - `string`: Predicate that results in a substring match using the value of `expression`
|
||||
* string. All strings or objects with string properties in `array` that contain this string
|
||||
* will be returned. The predicate can be negated by prefixing the string with `!`.
|
||||
*
|
||||
* - `Object`: A pattern object can be used to filter specific properties on objects contained
|
||||
* by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
|
||||
* which have property `name` containing "M" and property `phone` containing "1". A special
|
||||
* property name `$` can be used (as in `{$:"text"}`) to accept a match against any
|
||||
* property of the object. That's equivalent to the simple substring match with a `string`
|
||||
* as described above.
|
||||
*
|
||||
* - `function`: A predicate function can be used to write arbitrary filters. The function is
|
||||
* called for each element of `array`. The final result is an array of those elements that
|
||||
* the predicate returned true for.
|
||||
*
|
||||
* @example
|
||||
<div ng:init="friends = [{name:'John', phone:'555-1276'},
|
||||
{name:'Mary', phone:'800-BIG-MARY'},
|
||||
{name:'Mike', phone:'555-4321'},
|
||||
{name:'Adam', phone:'555-5678'},
|
||||
{name:'Julie', phone:'555-8765'}]"></div>
|
||||
|
||||
Search: <input name="searchText"/>
|
||||
<table id="searchTextResults">
|
||||
<tr><th>Name</th><th>Phone</th><tr>
|
||||
<tr ng:repeat="friend in friends.$filter(searchText)">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.phone}}</td>
|
||||
<tr>
|
||||
</table>
|
||||
<hr>
|
||||
Any: <input name="search.$"/> <br>
|
||||
Name only <input name="search.name"/><br>
|
||||
Phone only <input name="search.phone"/><br>
|
||||
<table id="searchObjResults">
|
||||
<tr><th>Name</th><th>Phone</th><tr>
|
||||
<tr ng:repeat="friend in friends.$filter(search)">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.phone}}</td>
|
||||
<tr>
|
||||
</table>
|
||||
|
||||
@scenario
|
||||
it('should search across all fields when filtering with a string', function() {
|
||||
input('searchText').enter('m');
|
||||
expect(repeater('#searchTextResults tr', 'friend in friends').column('name')).
|
||||
toEqual(['Mary', 'Mike', 'Adam']);
|
||||
|
||||
input('searchText').enter('76');
|
||||
expect(repeater('#searchTextResults tr', 'friend in friends').column('name')).
|
||||
toEqual(['John', 'Julie']);
|
||||
});
|
||||
|
||||
it('should search in specific fields when filtering with a predicate object', function() {
|
||||
input('search.$').enter('i');
|
||||
expect(repeater('#searchObjResults tr', 'friend in friends').column('name')).
|
||||
toEqual(['Mary', 'Mike', 'Julie']);
|
||||
});
|
||||
*/
|
||||
'filter':function(array, expression) {
|
||||
var predicates = [];
|
||||
predicates.check = function(value) {
|
||||
@@ -117,10 +378,124 @@ var angularArray = {
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.add
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* `add` is a function similar to JavaScript's `Array#push` method, in that it appends a new
|
||||
* element to an array, but it differs in that the value being added is optional and defaults to
|
||||
* an emty object.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array The array expand.
|
||||
* @param {*=} [value={}] The value to be added.
|
||||
* @returns {Array} The expanded array.
|
||||
*
|
||||
* @exampleDescription
|
||||
* This example shows how an initially empty array can be filled with objects created from user
|
||||
* input via the `$add` method.
|
||||
*
|
||||
* @example
|
||||
[<a href="" ng:click="people.$add()">add empty</a>]
|
||||
[<a href="" ng:click="people.$add({name:'John', sex:'male'})">add 'John'</a>]
|
||||
[<a href="" ng:click="people.$add({name:'Mary', sex:'female'})">add 'Mary'</a>]
|
||||
|
||||
<ul ng:init="people=[]">
|
||||
<li ng:repeat="person in people">
|
||||
<input name="person.name">
|
||||
<select name="person.sex">
|
||||
<option value="">--chose one--</option>
|
||||
<option>male</option>
|
||||
<option>female</option>
|
||||
</select>
|
||||
[<a href="" ng:click="people.$remove(person)">X</a>]
|
||||
</li>
|
||||
</ul>
|
||||
<pre>people = {{people}}</pre>
|
||||
|
||||
@scenario
|
||||
beforeEach(function() {
|
||||
expect(binding('people')).toBe('people = []');
|
||||
});
|
||||
|
||||
it('should create an empty record when "add empty" is clicked', function() {
|
||||
element('.doc-example a:contains("add empty")').click();
|
||||
expect(binding('people')).toBe('people = [{\n "name":"",\n "sex":null}]');
|
||||
});
|
||||
|
||||
it('should create a "John" record when "add \'John\'" is clicked', function() {
|
||||
element('.doc-example a:contains("add \'John\'")').click();
|
||||
expect(binding('people')).toBe('people = [{\n "name":"John",\n "sex":"male"}]');
|
||||
});
|
||||
|
||||
it('should create a "Mary" record when "add \'Mary\'" is clicked', function() {
|
||||
element('.doc-example a:contains("add \'Mary\'")').click();
|
||||
expect(binding('people')).toBe('people = [{\n "name":"Mary",\n "sex":"female"}]');
|
||||
});
|
||||
|
||||
it('should delete a record when "X" is clicked', function() {
|
||||
element('.doc-example a:contains("add empty")').click();
|
||||
element('.doc-example li a:contains("X"):first').click();
|
||||
expect(binding('people')).toBe('people = []');
|
||||
});
|
||||
*/
|
||||
'add':function(array, value) {
|
||||
array.push(isUndefined(value)? {} : value);
|
||||
return array;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.count
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Determines the number of elements in an array. Optionally it will count only those elements
|
||||
* for which the `condition` evaluets to `true`.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array The array to count elements in.
|
||||
* @param {(function()|string)=} condition A function to be evaluated or angular expression to be
|
||||
* compiled and evaluated. The element that is currently being iterated over, is exposed to
|
||||
* the `condition` as `this`.
|
||||
* @returns {number} Number of elements in the array (for which the condition evaluates to true).
|
||||
*
|
||||
* @example
|
||||
<pre ng:init="items = [{name:'knife', points:1},
|
||||
{name:'fork', points:3},
|
||||
{name:'spoon', points:1}]"></pre>
|
||||
<ul>
|
||||
<li ng:repeat="item in items">
|
||||
{{item.name}}: points=
|
||||
<input type="text" name="item.points"/> <!-- id="item{{$index}} -->
|
||||
</li>
|
||||
</ul>
|
||||
<p>Number of items which have one point: <em>{{ items.$count('points==1') }}</em></p>
|
||||
<p>Number of items which have more than one point: <em>{{items.$count('points>1')}}</em></p>
|
||||
|
||||
@scenario
|
||||
it('should calculate counts', function() {
|
||||
expect(binding('items.$count(\'points==1\')')).toEqual(2);
|
||||
expect(binding('items.$count(\'points>1\')')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should recalculate when updated', function() {
|
||||
using('.doc-example li:first-child').input('item.points').enter('23');
|
||||
expect(binding('items.$count(\'points==1\')')).toEqual(1);
|
||||
expect(binding('items.$count(\'points>1\')')).toEqual(2);
|
||||
});
|
||||
*/
|
||||
'count':function(array, condition) {
|
||||
if (!condition) return array.length;
|
||||
var fn = angular['Function']['compile'](condition), count = 0;
|
||||
@@ -131,6 +506,86 @@ var angularArray = {
|
||||
});
|
||||
return count;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.orderBy
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Orders `array` by the `expression` predicate.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array The array to sort.
|
||||
* @param {function()|string|Array.<(function()|string)>} expression A predicate to be used by the
|
||||
* comparator to determine the order of elements.
|
||||
*
|
||||
* Can be one of:
|
||||
*
|
||||
* - `function`: JavaScript's Array#sort comparator function
|
||||
* - `string`: angular expression which evaluates to an object to order by, such as 'name' to
|
||||
* sort by a property called 'name'. Optionally prefixed with `+` or `-` to control ascending
|
||||
* or descending sort order (e.g. +name or -name).
|
||||
* - `Array`: array of function or string predicates, such that a first predicate in the array
|
||||
* is used for sorting, but when the items are equivalent next predicate is used.
|
||||
*
|
||||
* @param {boolean=} reverse Reverse the order the array.
|
||||
* @returns {Array} Sorted copy of the source array.
|
||||
*
|
||||
* @example
|
||||
<div ng:init="friends = [{name:'John', phone:'555-1212', age:10},
|
||||
{name:'Mary', phone:'555-9876', age:19},
|
||||
{name:'Mike', phone:'555-4321', age:21},
|
||||
{name:'Adam', phone:'555-5678', age:35},
|
||||
{name:'Julie', phone:'555-8765', age:29}]"></div>
|
||||
|
||||
<pre>Sorting predicate = {{predicate}}</pre>
|
||||
<hr/>
|
||||
<table ng:init="predicate='-age'">
|
||||
<tr>
|
||||
<th><a href="" ng:click="predicate = 'name'">Name</a>
|
||||
(<a href ng:click="predicate = '-name'">^</a>)</th>
|
||||
<th><a href="" ng:click="predicate = 'phone'">Phone</a>
|
||||
(<a href ng:click="predicate = '-phone'">^</a>)</th>
|
||||
<th><a href="" ng:click="predicate = 'age'">Age</a>
|
||||
(<a href ng:click="predicate = '-age'">^</a>)</th>
|
||||
<tr>
|
||||
<tr ng:repeat="friend in friends.$orderBy(predicate)">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.phone}}</td>
|
||||
<td>{{friend.age}}</td>
|
||||
<tr>
|
||||
</table>
|
||||
|
||||
@scenario
|
||||
it('should be reverse ordered by aged', function() {
|
||||
expect(binding('predicate')).toBe('Sorting predicate = -age');
|
||||
expect(repeater('.doc-example table', 'friend in friends').column('friend.age')).
|
||||
toEqual(['35', '29', '21', '19', '10']);
|
||||
expect(repeater('.doc-example table', 'friend in friends').column('friend.name')).
|
||||
toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']);
|
||||
});
|
||||
|
||||
it('should reorder the table when user selects different predicate', function() {
|
||||
element('.doc-example a:contains("Name")').click();
|
||||
expect(repeater('.doc-example table', 'friend in friends').column('friend.name')).
|
||||
toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']);
|
||||
expect(repeater('.doc-example table', 'friend in friends').column('friend.age')).
|
||||
toEqual(['35', '10', '29', '19', '21']);
|
||||
|
||||
element('.doc-example a:contains("Phone")+a:contains("^")').click();
|
||||
expect(repeater('.doc-example table', 'friend in friends').column('friend.phone')).
|
||||
toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']);
|
||||
expect(repeater('.doc-example table', 'friend in friends').column('friend.name')).
|
||||
toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']);
|
||||
});
|
||||
*/
|
||||
//TODO: WTH is descend param for and how/when it should be used, how is it affected by +/- in
|
||||
// predicate? the code below is impossible to read and specs are not very good.
|
||||
'orderBy':function(array, expression, descend) {
|
||||
expression = isArray(expression) ? expression: [expression];
|
||||
expression = map(expression, function($){
|
||||
@@ -173,11 +628,51 @@ var angularArray = {
|
||||
return t1 < t2 ? -1 : 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc function
|
||||
* @name angular.Array.limitTo
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Creates a new array containing only the first, or last `limit` number of elements of the
|
||||
* source `array`.
|
||||
*
|
||||
* Note: this function is used to augment the Array type in angular expressions. See
|
||||
* {@link angular.Array} for more info.
|
||||
*
|
||||
* @param {Array} array Source array to be limited.
|
||||
* @param {string|Number} limit The length of the returned array. If the number is positive, the
|
||||
* first `limit` items from the source array will be copied, if the number is negative, the
|
||||
* last `limit` items will be copied.
|
||||
* @returns {Array} New array of length `limit`.
|
||||
*
|
||||
*/
|
||||
limitTo: function(array, limit) {
|
||||
limit = parseInt(limit, 10);
|
||||
var out = [],
|
||||
i, n;
|
||||
|
||||
if (limit > 0) {
|
||||
i = 0;
|
||||
n = limit;
|
||||
} else {
|
||||
i = array.length + limit;
|
||||
n = array.length;
|
||||
}
|
||||
|
||||
for (; i<n; i++) {
|
||||
out.push(array[i]);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
var R_ISO8061_STR = /^(\d{4})-(\d\d)-(\d\d)(?:T(\d\d)(?:\:(\d\d)(?:\:(\d\d)(?:\.(\d{3}))?)?)?Z)?$/
|
||||
var R_ISO8061_STR = /^(\d{4})-(\d\d)-(\d\d)(?:T(\d\d)(?:\:(\d\d)(?:\:(\d\d)(?:\.(\d{3}))?)?)?Z)?$/;
|
||||
|
||||
var angularString = {
|
||||
'quote':function(string) {
|
||||
|
||||
+13
-8
@@ -304,7 +304,8 @@ angularDirective("ng:bind-template", function(expression, element){
|
||||
var REMOVE_ATTRIBUTES = {
|
||||
'disabled':'disabled',
|
||||
'readonly':'readOnly',
|
||||
'checked':'checked'
|
||||
'checked':'checked',
|
||||
'selected':'selected'
|
||||
};
|
||||
/**
|
||||
* @workInProgress
|
||||
@@ -359,27 +360,31 @@ var REMOVE_ATTRIBUTES = {
|
||||
angularDirective("ng:bind-attr", function(expression){
|
||||
return function(element){
|
||||
var lastValue = {};
|
||||
var updateFn = element.parent().data('$update');
|
||||
var updateFn = element.data($$update) || noop;
|
||||
this.$onEval(function(){
|
||||
var values = this.$eval(expression);
|
||||
var values = this.$eval(expression),
|
||||
dirty = noop;
|
||||
for(var key in values) {
|
||||
var value = compileBindTemplate(values[key]).call(this, element),
|
||||
specialName = REMOVE_ATTRIBUTES[lowercase(key)];
|
||||
if (lastValue[key] !== value) {
|
||||
lastValue[key] = value;
|
||||
if (specialName) {
|
||||
if (element[specialName] = toBoolean(value)) {
|
||||
element.attr(specialName, value);
|
||||
if (toBoolean(value)) {
|
||||
element.attr(specialName, specialName);
|
||||
element.attr('ng-' + specialName, value);
|
||||
} else {
|
||||
element.removeAttr(key);
|
||||
element.removeAttr(specialName);
|
||||
element.removeAttr('ng-' + specialName);
|
||||
}
|
||||
(element.data('$validate')||noop)();
|
||||
(element.data($$validate)||noop)();
|
||||
} else {
|
||||
element.attr(key, value);
|
||||
}
|
||||
this.$postEval(updateFn);
|
||||
dirty = updateFn;
|
||||
}
|
||||
}
|
||||
dirty();
|
||||
}, element);
|
||||
};
|
||||
});
|
||||
|
||||
+71
-138
@@ -9,7 +9,7 @@ var OPERATORS = {
|
||||
'/':function(self, a,b){return a/b;},
|
||||
'%':function(self, a,b){return a%b;},
|
||||
'^':function(self, a,b){return a^b;},
|
||||
'=':function(self, a,b){return setter(self, a, b);},
|
||||
'=':noop,
|
||||
'==':function(self, a,b){return a==b;},
|
||||
'!=':function(self, a,b){return a!=b;},
|
||||
'<':function(self, a,b){return a<b;},
|
||||
@@ -32,7 +32,7 @@ function lex(text, parseStringsForObjects){
|
||||
index = 0,
|
||||
json = [],
|
||||
ch,
|
||||
lastCh = ':'; // can start regexp
|
||||
lastCh = ':';
|
||||
|
||||
while (index < text.length) {
|
||||
ch = text.charAt(index);
|
||||
@@ -40,8 +40,6 @@ function lex(text, parseStringsForObjects){
|
||||
readString(ch);
|
||||
} else if (isNumber(ch) || is('.') && isNumber(peek())) {
|
||||
readNumber();
|
||||
} else if ( was('({[:,;') && is('/') ) {
|
||||
readRegexp();
|
||||
} else if (isIdent(ch)) {
|
||||
readIdent();
|
||||
if (was('{,') && json[0]=='{' &&
|
||||
@@ -73,6 +71,9 @@ function lex(text, parseStringsForObjects){
|
||||
lastCh = ch;
|
||||
}
|
||||
return tokens;
|
||||
|
||||
|
||||
//////////////////////////////////////////////
|
||||
|
||||
function is(chars) {
|
||||
return chars.indexOf(ch) != -1;
|
||||
@@ -97,10 +98,6 @@ function lex(text, parseStringsForObjects){
|
||||
'A' <= ch && ch <= 'Z' ||
|
||||
'_' == ch || ch == '$';
|
||||
}
|
||||
function isExpOperator(ch) {
|
||||
return ch == '-' || ch == '+' || isNumber(ch);
|
||||
}
|
||||
|
||||
function throwError(error, start, end) {
|
||||
end = end || index;
|
||||
throw Error("Lexer Error: " + error + " at column" +
|
||||
@@ -109,128 +106,61 @@ function lex(text, parseStringsForObjects){
|
||||
" " + end) +
|
||||
" in expression [" + text + "].");
|
||||
}
|
||||
|
||||
function consume(regexp, processToken, errorMsg) {
|
||||
var match = text.substr(index).match(regexp);
|
||||
var token = {index: index};
|
||||
var start = index;
|
||||
if (!match) throwError(errorMsg);
|
||||
index += match[0].length;
|
||||
processToken(token, token.text = match[0], start);
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
function readNumber() {
|
||||
var number = "";
|
||||
var start = index;
|
||||
while (index < text.length) {
|
||||
var ch = lowercase(text.charAt(index));
|
||||
if (ch == '.' || isNumber(ch)) {
|
||||
number += ch;
|
||||
} else {
|
||||
var peekCh = peek();
|
||||
if (ch == 'e' && isExpOperator(peekCh)) {
|
||||
number += ch;
|
||||
} else if (isExpOperator(ch) &&
|
||||
peekCh && isNumber(peekCh) &&
|
||||
number.charAt(number.length - 1) == 'e') {
|
||||
number += ch;
|
||||
} else if (isExpOperator(ch) &&
|
||||
(!peekCh || !isNumber(peekCh)) &&
|
||||
number.charAt(number.length - 1) == 'e') {
|
||||
throwError('Invalid exponent');
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
number = 1 * number;
|
||||
tokens.push({index:start, text:number, json:true,
|
||||
fn:function(){return number;}});
|
||||
consume(/^(\d+)?(\.\d+)?([eE][+-]?\d+)?/, function(token, number){
|
||||
token.text = number = 1 * number;
|
||||
token.json = true;
|
||||
token.fn = valueFn(number);
|
||||
}, "Not a valid number");
|
||||
}
|
||||
|
||||
function readIdent() {
|
||||
var ident = "";
|
||||
var start = index;
|
||||
while (index < text.length) {
|
||||
var ch = text.charAt(index);
|
||||
if (ch == '.' || isIdent(ch) || isNumber(ch)) {
|
||||
ident += ch;
|
||||
} else {
|
||||
break;
|
||||
consume(/^[\w_\$][\w_\$\d]*(\.[\w_\$][\w_\$\d]*)*/, function(token, ident){
|
||||
fn = OPERATORS[ident];
|
||||
if (!fn) {
|
||||
fn = getterFn(ident);
|
||||
fn.isAssignable = ident;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
var fn = OPERATORS[ident];
|
||||
if (!fn) {
|
||||
fn = getterFn(ident);
|
||||
fn.isAssignable = ident;
|
||||
}
|
||||
tokens.push({index:start, text:ident, fn:fn, json: OPERATORS[ident]});
|
||||
token.fn = OPERATORS[ident]||extend(getterFn(ident), {
|
||||
assign:function(self, value){
|
||||
return setter(self, ident, value);
|
||||
}
|
||||
});
|
||||
token.json = OPERATORS[ident];
|
||||
});
|
||||
}
|
||||
|
||||
function readString(quote) {
|
||||
var start = index;
|
||||
index++;
|
||||
var string = "";
|
||||
var rawString = quote;
|
||||
var escape = false;
|
||||
while (index < text.length) {
|
||||
var ch = text.charAt(index);
|
||||
rawString += ch;
|
||||
if (escape) {
|
||||
if (ch == 'u') {
|
||||
var hex = text.substring(index + 1, index + 5);
|
||||
if (!hex.match(/[\da-f]{4}/i))
|
||||
throwError( "Invalid unicode escape [\\u" + hex + "]");
|
||||
index += 4;
|
||||
string += String.fromCharCode(parseInt(hex, 16));
|
||||
} else {
|
||||
var rep = ESCAPE[ch];
|
||||
if (rep) {
|
||||
string += rep;
|
||||
} else {
|
||||
string += ch;
|
||||
}
|
||||
}
|
||||
escape = false;
|
||||
} else if (ch == '\\') {
|
||||
escape = true;
|
||||
} else if (ch == quote) {
|
||||
index++;
|
||||
tokens.push({index:start, text:rawString, string:string, json:true,
|
||||
fn:function(){
|
||||
return (string.length == dateParseLength) ?
|
||||
angular['String']['toDate'](string) : string;
|
||||
}});
|
||||
return;
|
||||
} else {
|
||||
string += ch;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
throwError("Unterminated quote", start);
|
||||
}
|
||||
function readRegexp(quote) {
|
||||
var start = index;
|
||||
index++;
|
||||
var regexp = "";
|
||||
var escape = false;
|
||||
while (index < text.length) {
|
||||
var ch = text.charAt(index);
|
||||
if (escape) {
|
||||
regexp += ch;
|
||||
escape = false;
|
||||
} else if (ch === '\\') {
|
||||
regexp += ch;
|
||||
escape = true;
|
||||
} else if (ch === '/') {
|
||||
index++;
|
||||
var flags = "";
|
||||
if (isIdent(text.charAt(index))) {
|
||||
readIdent();
|
||||
flags = tokens.pop().text;
|
||||
}
|
||||
var compiledRegexp = new RegExp(regexp, flags);
|
||||
tokens.push({index:start, text:regexp, flags:flags,
|
||||
fn:function(){return compiledRegexp;}});
|
||||
return;
|
||||
} else {
|
||||
regexp += ch;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
throwError("Unterminated RegExp", start);
|
||||
consume(/^(('(\\'|[^'])*')|("(\\"|[^"])*"))/, function(token, rawString, start){
|
||||
var hasError;
|
||||
var string = token.string = rawString.substr(1, rawString.length - 2).
|
||||
replace(/(\\u(.?.?.?.?))|(\\(.))/g,
|
||||
function(match, wholeUnicode, unicode, wholeEscape, escape){
|
||||
if (unicode && !unicode.match(/[\da-fA-F]{4}/))
|
||||
hasError = hasError || bind(null, throwError, "Invalid unicode escape [\\u" + unicode + "]", start);
|
||||
return unicode ?
|
||||
String.fromCharCode(parseInt(unicode, 16)) :
|
||||
ESCAPE[escape] || escape;
|
||||
});
|
||||
(hasError||noop)();
|
||||
token.json = true;
|
||||
token.fn = function(){
|
||||
return (string.length == dateParseLength) ?
|
||||
angular['String']['toDate'](string) :
|
||||
string;
|
||||
};
|
||||
}, "Unterminated string");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,14 +314,17 @@ function parser(text, json){
|
||||
|
||||
function assignment(){
|
||||
var left = logicalOR();
|
||||
var right;
|
||||
var token;
|
||||
if (token = expect('=')) {
|
||||
if (!left.isAssignable) {
|
||||
if (!left.assign) {
|
||||
throwError("implies assignment but [" +
|
||||
text.substring(0, token.index) + "] can not be assigned to", token);
|
||||
}
|
||||
var ident = function(){return left.isAssignable;};
|
||||
return binaryFn(ident, token.fn, logicalOR());
|
||||
right = logicalOR();
|
||||
return function(self){
|
||||
return left.assign(self, right(self));
|
||||
};
|
||||
} else {
|
||||
return left;
|
||||
}
|
||||
@@ -518,28 +451,28 @@ function parser(text, json){
|
||||
function fieldAccess(object) {
|
||||
var field = expect().text;
|
||||
var getter = getterFn(field);
|
||||
var fn = function (self){
|
||||
return extend(function (self){
|
||||
return getter(object(self));
|
||||
};
|
||||
fn.isAssignable = field;
|
||||
return fn;
|
||||
}, {
|
||||
assign:function(self, value){
|
||||
return setter(object(self), field, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function objectIndex(obj) {
|
||||
var indexFn = expression();
|
||||
consume(']');
|
||||
if (expect('=')) {
|
||||
var rhs = expression();
|
||||
return function (self){
|
||||
return obj(self)[indexFn(self)] = rhs(self);
|
||||
};
|
||||
} else {
|
||||
return function (self){
|
||||
return extend(
|
||||
function (self){
|
||||
var o = obj(self);
|
||||
var i = indexFn(self);
|
||||
return (o) ? o[i] : _undefined;
|
||||
};
|
||||
}
|
||||
}, {
|
||||
assign:function(self, value){
|
||||
return obj(self)[indexFn(self)] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function functionCall(fn) {
|
||||
|
||||
+66
-71
@@ -17,40 +17,42 @@
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
var START_TAG_REGEXP = /^<\s*([\w:]+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
|
||||
END_TAG_REGEXP = /^<\s*\/\s*([\w:]+)[^>]*>/,
|
||||
ATTR_REGEXP = /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
|
||||
ATTR_REGEXP = /(\w+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
|
||||
BEGIN_TAG_REGEXP = /^</,
|
||||
BEGING_END_TAGE_REGEXP = /^<\s*\//,
|
||||
COMMENT_REGEXP = /<!--(.*?)-->/g,
|
||||
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g;
|
||||
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
|
||||
URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/,
|
||||
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character)
|
||||
|
||||
// Empty Elements - HTML 4.01
|
||||
var emptyElements = makeMap("area,base,basefont,br,col,hr,img,input,isindex,link,param");
|
||||
var emptyElements = makeMap("area,br,col,hr,img");
|
||||
|
||||
// Block Elements - HTML 4.01
|
||||
var blockElements = makeMap("address,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,"+
|
||||
"form,hr,ins,isindex,li,map,menu,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
|
||||
var blockElements = makeMap("address,blockquote,center,dd,del,dir,div,dl,dt,"+
|
||||
"hr,ins,li,map,menu,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
|
||||
|
||||
// Inline Elements - HTML 4.01
|
||||
var inlineElements = makeMap("a,abbr,acronym,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,img,"+
|
||||
"input,ins,kbd,label,map,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");
|
||||
|
||||
var inlineElements = makeMap("a,abbr,acronym,b,bdo,big,br,cite,code,del,dfn,em,font,i,img,"+
|
||||
"ins,kbd,label,map,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var");
|
||||
// Elements that you can, intentionally, leave open
|
||||
// (and which close themselves)
|
||||
var closeSelfElements = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
|
||||
|
||||
// Attributes that have their values filled in disabled="disabled"
|
||||
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
|
||||
|
||||
var closeSelfElements = makeMap("colgroup,dd,dt,li,p,td,tfoot,th,thead,tr");
|
||||
// Special Elements (can contain anything)
|
||||
var specialElements = makeMap("script,style");
|
||||
|
||||
var validElements = extend({}, emptyElements, blockElements, inlineElements, closeSelfElements);
|
||||
var validAttrs = extend({}, fillAttrs, makeMap(
|
||||
'abbr,align,alink,alt,archive,axis,background,bgcolor,border,cellpadding,cellspacing,cite,class,classid,clear,code,codebase,'+
|
||||
'codetype,color,cols,colspan,content,coords,data,dir,face,for,headers,height,href,hreflang,hspace,id,label,lang,language,'+
|
||||
'link,longdesc,marginheight,marginwidth,maxlength,media,method,name,nowrap,profile,prompt,rel,rev,rows,rowspan,rules,scheme,'+
|
||||
'scope,scrolling,shape,size,span,src,standby,start,summary,tabindex,target,text,title,type,usemap,valign,value,valuetype,'+
|
||||
'vlink,vspace,width'));
|
||||
|
||||
//see: http://www.w3.org/TR/html4/index/attributes.html
|
||||
//Attributes that have their values filled in disabled="disabled"
|
||||
var fillAttrs = makeMap("compact,ismap,nohref,nowrap");
|
||||
//Attributes that have href and hence need to be sanitized
|
||||
var uriAttrs = makeMap("background,href,longdesc,src,usemap");
|
||||
var validAttrs = extend({}, fillAttrs, uriAttrs, makeMap(
|
||||
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
|
||||
'color,cols,colspan,coords,dir,face,headers,height,hreflang,hspace,'+
|
||||
'lang,language,rel,rev,rows,rowspan,rules,'+
|
||||
'scope,scrolling,shape,span,start,summary,target,title,type,'+
|
||||
'valign,value,vspace,width'));
|
||||
|
||||
/**
|
||||
* @example
|
||||
@@ -64,7 +66,7 @@ var validAttrs = extend({}, fillAttrs, makeMap(
|
||||
* @param {string} html string
|
||||
* @param {object} handler
|
||||
*/
|
||||
var htmlParser = function( html, handler ) {
|
||||
function htmlParser( html, handler ) {
|
||||
var index, chars, match, stack = [], last = html;
|
||||
stack.last = function(){ return stack[ stack.length - 1 ]; };
|
||||
|
||||
@@ -112,8 +114,7 @@ var htmlParser = function( html, handler ) {
|
||||
var text = index < 0 ? html : html.substring( 0, index );
|
||||
html = index < 0 ? "" : html.substring( index );
|
||||
|
||||
if ( handler.chars )
|
||||
handler.chars( text );
|
||||
handler.chars( decodeEntities(text) );
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -122,8 +123,7 @@ var htmlParser = function( html, handler ) {
|
||||
replace(COMMENT_REGEXP, "$1").
|
||||
replace(CDATA_REGEXP, "$1");
|
||||
|
||||
if ( handler.chars )
|
||||
handler.chars( text );
|
||||
handler.chars( decodeEntities(text) );
|
||||
|
||||
return "";
|
||||
});
|
||||
@@ -157,21 +157,18 @@ var htmlParser = function( html, handler ) {
|
||||
if ( !unary )
|
||||
stack.push( tagName );
|
||||
|
||||
if ( handler.start ) {
|
||||
var attrs = {};
|
||||
var attrs = {};
|
||||
|
||||
rest.replace(ATTR_REGEXP, function(match, name) {
|
||||
var value = arguments[2] ? arguments[2] :
|
||||
arguments[3] ? arguments[3] :
|
||||
arguments[4] ? arguments[4] :
|
||||
fillAttrs[name] ? name : "";
|
||||
rest.replace(ATTR_REGEXP, function(match, name) {
|
||||
var value = arguments[2] ? arguments[2] :
|
||||
arguments[3] ? arguments[3] :
|
||||
arguments[4] ? arguments[4] :
|
||||
fillAttrs[name] ? name : "";
|
||||
|
||||
attrs[name] = value; //value.replace(/(^|[^\\])"/g, '$1\\\"') //"
|
||||
});
|
||||
attrs[name] = decodeEntities(value); //value.replace(/(^|[^\\])"/g, '$1\\\"') //"
|
||||
});
|
||||
|
||||
if ( handler.start )
|
||||
handler.start( tagName, attrs, unary );
|
||||
}
|
||||
handler.start( tagName, attrs, unary );
|
||||
}
|
||||
|
||||
function parseEndTag( tag, tagName ) {
|
||||
@@ -186,14 +183,13 @@ var htmlParser = function( html, handler ) {
|
||||
if ( pos >= 0 ) {
|
||||
// Close all the open elements, up the stack
|
||||
for ( i = stack.length - 1; i >= pos; i-- )
|
||||
if ( handler.end )
|
||||
handler.end( stack[ i ] );
|
||||
handler.end( stack[ i ] );
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param str 'key1,key2,...'
|
||||
@@ -206,28 +202,32 @@ function makeMap(str){
|
||||
return obj;
|
||||
}
|
||||
|
||||
/*
|
||||
* For attack vectors see: http://ha.ckers.org/xss.html
|
||||
*/
|
||||
var JAVASCRIPT_URL = /^javascript:/i,
|
||||
NBSP_REGEXP = / /gim,
|
||||
HEX_ENTITY_REGEXP = /&#x([\da-f]*);?/igm,
|
||||
DEC_ENTITY_REGEXP = /&#(\d+);?/igm,
|
||||
CHAR_REGEXP = /[\w:]/gm,
|
||||
HEX_DECODE = function(match, code){return fromCharCode(parseInt(code,16));},
|
||||
DEC_DECODE = function(match, code){return fromCharCode(code);};
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns true if url decodes to something which starts with 'javascript:' hence unsafe
|
||||
* decodes all entities into regular string
|
||||
* @param value
|
||||
* @returns {string} A string with decoded entities.
|
||||
*/
|
||||
function isJavaScriptUrl(url) {
|
||||
var chars = [];
|
||||
url.replace(NBSP_REGEXP, '').
|
||||
replace(HEX_ENTITY_REGEXP, HEX_DECODE).
|
||||
replace(DEC_ENTITY_REGEXP, DEC_DECODE).
|
||||
// Remove all non \w: characters, unfurtunetly value.replace(/[\w:]/,'') can be defeated using \u0000
|
||||
replace(CHAR_REGEXP, function(ch){chars.push(ch);});
|
||||
return JAVASCRIPT_URL.test(lowercase(chars.join('')));
|
||||
var hiddenPre=document.createElement("pre");
|
||||
function decodeEntities(value) {
|
||||
hiddenPre.innerHTML=value.replace(/</g,"<");
|
||||
return hiddenPre.innerText || hiddenPre.textContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all potentially dangerous characters, so that the
|
||||
* resulting string can be safely inserted into attribute or
|
||||
* element text.
|
||||
* @param value
|
||||
* @returns escaped text
|
||||
*/
|
||||
function encodeEntities(value) {
|
||||
return value.
|
||||
replace(/&/g, '&').
|
||||
replace(NON_ALPHANUMERIC_REGEXP, function(value){
|
||||
return '&#' + value.charCodeAt(0) + ';';
|
||||
}).
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,18 +249,16 @@ function htmlSanitizeWriter(buf){
|
||||
if (!ignore && specialElements[tag]) {
|
||||
ignore = tag;
|
||||
}
|
||||
if (!ignore && validElements[tag]) {
|
||||
if (!ignore && validElements[tag] == true) {
|
||||
out('<');
|
||||
out(tag);
|
||||
foreach(attrs, function(value, key){
|
||||
if (validAttrs[lowercase(key)] && !isJavaScriptUrl(value)) {
|
||||
var lkey=lowercase(key);
|
||||
if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) {
|
||||
out(' ');
|
||||
out(key);
|
||||
out('="');
|
||||
out(value.
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>').
|
||||
replace(/\"/g,'"'));
|
||||
out(encodeEntities(value));
|
||||
out('"');
|
||||
}
|
||||
});
|
||||
@@ -269,7 +267,7 @@ function htmlSanitizeWriter(buf){
|
||||
},
|
||||
end: function(tag){
|
||||
tag = lowercase(tag);
|
||||
if (!ignore && validElements[tag]) {
|
||||
if (!ignore && validElements[tag] == true) {
|
||||
out('</');
|
||||
out(tag);
|
||||
out('>');
|
||||
@@ -280,10 +278,7 @@ function htmlSanitizeWriter(buf){
|
||||
},
|
||||
chars: function(chars){
|
||||
if (!ignore) {
|
||||
out(chars.
|
||||
replace(/&(\w+[&;\W])?/g, function(match, entity){return entity?match:'&';}).
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>'));
|
||||
out(encodeEntities(chars));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+140
-11
@@ -685,7 +685,7 @@ angularServiceInject('$route', function(location) {
|
||||
* @ngdoc service
|
||||
* @name angular.service.$xhr
|
||||
* @requires $browser
|
||||
* @requires $error
|
||||
* @requires $xhr.error
|
||||
* @requires $log
|
||||
*
|
||||
* @description
|
||||
@@ -801,6 +801,40 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){
|
||||
return bulkXHR;
|
||||
}, ['$xhr', '$xhr.error', '$log']);
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc service
|
||||
* @name angular.service.$defer
|
||||
* @requires $browser
|
||||
* @requires $log
|
||||
*
|
||||
* @description
|
||||
* Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function
|
||||
* into a try/catch block and delegates any exceptions to
|
||||
* {@link angular.services.$exceptionHandler $exceptionHandler} service.
|
||||
*
|
||||
* In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions.
|
||||
*
|
||||
* @param {function()} fn A function, who's execution should be deferred.
|
||||
*/
|
||||
angularServiceInject('$defer', function($browser, $exceptionHandler) {
|
||||
var scope = this;
|
||||
|
||||
return function(fn) {
|
||||
$browser.defer(function() {
|
||||
try {
|
||||
fn();
|
||||
} catch(e) {
|
||||
$exceptionHandler(e);
|
||||
} finally {
|
||||
scope.$eval();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, ['$browser', '$exceptionHandler']);
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc service
|
||||
@@ -811,7 +845,7 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){
|
||||
*
|
||||
* @example
|
||||
*/
|
||||
angularServiceInject('$xhr.cache', function($xhr){
|
||||
angularServiceInject('$xhr.cache', function($xhr, $defer){
|
||||
var inflight = {}, self = this;
|
||||
function cache(method, url, post, callback, verifyCache){
|
||||
if (isFunction(post)) {
|
||||
@@ -819,9 +853,9 @@ angularServiceInject('$xhr.cache', function($xhr){
|
||||
post = _null;
|
||||
}
|
||||
if (method == 'GET') {
|
||||
var data;
|
||||
if (data = cache.data[url]) {
|
||||
callback(200, copy(data.value));
|
||||
var data, dataCached;
|
||||
if (dataCached = cache.data[url]) {
|
||||
$defer(function() { callback(200, copy(dataCached.value)); });
|
||||
if (!verifyCache)
|
||||
return;
|
||||
}
|
||||
@@ -853,21 +887,116 @@ angularServiceInject('$xhr.cache', function($xhr){
|
||||
cache.data = {};
|
||||
cache.delegate = $xhr;
|
||||
return cache;
|
||||
}, ['$xhr.bulk']);
|
||||
}, ['$xhr.bulk', '$defer']);
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc service
|
||||
* @ngdoc function
|
||||
* @name angular.service.$resource
|
||||
* @requires $xhr
|
||||
*
|
||||
*
|
||||
* @description
|
||||
* Is a factory which creates a resource object which lets you interact with
|
||||
* <a href="http://en.wikipedia.org/wiki/Representational_State_Transfer" target="_blank">RESTful</a>
|
||||
* server-side data sources.
|
||||
* Resource object has action methods which provide high-level behaviors without
|
||||
* the need to interact with the low level $xhr or XMLHttpRequest().
|
||||
*
|
||||
* the need to interact with the low level $xhr or XMLHttpRequest().
|
||||
*
|
||||
* <pre>
|
||||
// Define CreditCard class
|
||||
var CreditCard = $resource('/user/:userId/card/:cardId',
|
||||
{userId:123, cardId:'@id'}, {
|
||||
charge: {method:'POST', params:{charge:true}}
|
||||
});
|
||||
|
||||
// We can retrieve a collection from the server
|
||||
var cards = CreditCard.query();
|
||||
// GET: /user/123/card
|
||||
// server returns: [ {id:456, number:'1234', name:'Smith'} ];
|
||||
|
||||
var card = cards[0];
|
||||
// each item is an instance of CreditCard
|
||||
expect(card instanceof CreditCard).toEqual(true);
|
||||
card.name = "J. Smith";
|
||||
// non GET methods are mapped onto the instances
|
||||
card.$save();
|
||||
// POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
|
||||
// server returns: {id:456, number:'1234', name: 'J. Smith'};
|
||||
|
||||
// our custom method is mapped as well.
|
||||
card.$charge({amount:9.99});
|
||||
// POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
|
||||
// server returns: {id:456, number:'1234', name: 'J. Smith'};
|
||||
|
||||
// we can create an instance as well
|
||||
var newCard = new CreditCard({number:'0123'});
|
||||
newCard.name = "Mike Smith";
|
||||
newCard.$save();
|
||||
// POST: /user/123/card {number:'0123', name:'Mike Smith'}
|
||||
// server returns: {id:789, number:'01234', name: 'Mike Smith'};
|
||||
expect(newCard.id).toEqual(789);
|
||||
* </pre>
|
||||
*
|
||||
* The object returned from this function execution is a resource "class" which has "static" method
|
||||
* for each action in the definition.
|
||||
*
|
||||
* Calling these methods invoke `$xhr` on the `url` template with the given `method` and `params`.
|
||||
* When the data is returned from the server then the object is an instance of the resource type and
|
||||
* all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
|
||||
* operations (create, read, update, delete) on server-side data.
|
||||
|
||||
<pre>
|
||||
var User = $resource('/user/:userId', {userId:'@id'});
|
||||
var user = User.get({userId:123}, function(){
|
||||
user.abc = true;
|
||||
user.$save();
|
||||
});
|
||||
</pre>
|
||||
*
|
||||
* It's worth noting that the callback for `get`, `query` and other method gets passed in the
|
||||
* response that came from the server, so one could rewrite the above example as:
|
||||
*
|
||||
<pre>
|
||||
var User = $resource('/user/:userId', {userId:'@id'});
|
||||
User.get({userId:123}, function(u){
|
||||
u.abc = true;
|
||||
u.$save();
|
||||
});
|
||||
</pre>
|
||||
*
|
||||
*
|
||||
* @param {string} url A parameterized URL template with parameters prefixed by `:` as in
|
||||
* `/user/:username`.
|
||||
* @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
|
||||
* `actions` methods.
|
||||
* @param {Object.<Object>=} actions Map of actions available for the resource.
|
||||
*
|
||||
* Each resource comes preconfigured with `get`, `save`, `query`, `remove`, and `delete` to
|
||||
* mimic the RESTful philosophy.
|
||||
*
|
||||
* To create your own actions, pass in a map keyed on action names (e.g. `'charge'`) with
|
||||
* elements consisting of these properties:
|
||||
*
|
||||
* - `{string} method`: Request method type. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,
|
||||
* and [`JSON`](http://en.wikipedia.org/wiki/JSON#JSONP) (also known as JSONP).
|
||||
* - `{Object=} params`: Set of pre-bound parameters for the action.
|
||||
* - `{boolean=} isArray`: If true then the returned object for this action is an array, see the
|
||||
* pre-binding section.
|
||||
* - `{boolean=} verifyCache`: If true then items returned from cache, are double checked by
|
||||
* running the query again and updating the resource asynchroniously.
|
||||
*
|
||||
* Each service comes preconfigured with the following overridable actions:
|
||||
* <pre>
|
||||
* { 'get': {method:'GET'},
|
||||
'save': {method:'POST'},
|
||||
'query': {method:'GET', isArray:true},
|
||||
'remove': {method:'DELETE'},
|
||||
'delete': {method:'DELETE'} };
|
||||
* </pre>
|
||||
*
|
||||
* @returns {Object} A resource "class".
|
||||
*
|
||||
* @example
|
||||
<script>
|
||||
function BuzzController($resource) {
|
||||
@@ -897,7 +1026,7 @@ angularServiceInject('$xhr.cache', function($xhr){
|
||||
<h1 style="font-size: 15px;">
|
||||
<img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
|
||||
<a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
|
||||
<a href="#" ng:click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
|
||||
<a href ng:click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
|
||||
</h1>
|
||||
{{item.object.content | html}}
|
||||
<div ng:repeat="reply in item.replies.data.items" style="margin-left: 20px;">
|
||||
|
||||
+3
-2
@@ -13,8 +13,9 @@ extend(angularValidator, {
|
||||
* @css ng-validation-error
|
||||
*
|
||||
* @example
|
||||
* <script> var ssn = /^\d\d\d-\d\d-\d\d\d\d$/; </script>
|
||||
* Enter valid SSN:
|
||||
* <input name="ssn" value="123-45-6789" ng:validate="regexp:/^\d\d\d-\d\d-\d\d\d\d$/" >
|
||||
* <input name="ssn" value="123-45-6789" ng:validate="regexp:$window.ssn" >
|
||||
*
|
||||
* @scenario
|
||||
* it('should invalidate non ssn', function(){
|
||||
@@ -394,7 +395,7 @@ extend(angularValidator, {
|
||||
element.removeClass('ng-input-indicator-wait');
|
||||
scope.$invalidWidgets.markValid(element);
|
||||
}
|
||||
element.data('$validate')();
|
||||
element.data($$validate)();
|
||||
scope.$root.$eval();
|
||||
});
|
||||
} else if (inputState.inFlight) {
|
||||
|
||||
+40
-9
@@ -282,7 +282,7 @@ function valueAccessor(scope, element) {
|
||||
required = requiredExpr === '';
|
||||
}
|
||||
|
||||
element.data('$validate', validate);
|
||||
element.data($$validate, validate);
|
||||
return {
|
||||
get: function(){
|
||||
if (lastError)
|
||||
@@ -391,6 +391,7 @@ var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initW
|
||||
// 'file': fileWidget???
|
||||
};
|
||||
|
||||
|
||||
function initWidgetValue(initValue) {
|
||||
return function (model, view) {
|
||||
var value = view.get();
|
||||
@@ -461,18 +462,13 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn) {
|
||||
this.$eval(element.attr('ng:init')||'');
|
||||
// Don't register a handler if we are a button (noopAccessor) and there is no action
|
||||
if (action || modelAccessor !== noopAccessor) {
|
||||
element.bind(events, function(event){
|
||||
element.bind(events, function (){
|
||||
model.set(view.get());
|
||||
lastValue = model.get();
|
||||
scope.$tryEval(action, element);
|
||||
scope.$root.$eval();
|
||||
});
|
||||
}
|
||||
function updateView(){
|
||||
view.set(lastValue = model.get());
|
||||
}
|
||||
updateView();
|
||||
element.data('$update', updateView);
|
||||
scope.$watch(model.get, function(value){
|
||||
if (lastValue !== value) {
|
||||
view.set(lastValue = value);
|
||||
@@ -494,15 +490,50 @@ angularWidget('select', function(element){
|
||||
return inputWidgetSelector.call(this, element);
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* Consider this:
|
||||
* <select name="selection">
|
||||
* <option ng:repeat="x in [1,2]">{{x}}</option>
|
||||
* </select>
|
||||
*
|
||||
* The issue is that the select gets evaluated before option is unrolled.
|
||||
* This means that the selection is undefined, but the browser
|
||||
* default behavior is to show the top selection in the list.
|
||||
* To fix that we register a $update function on the select element
|
||||
* and the option creation then calls the $update function when it is
|
||||
* unrolled. The $update function then calls this update function, which
|
||||
* then tries to determine if the model is unassigned, and if so it tries to
|
||||
* chose one of the options from the list.
|
||||
*/
|
||||
angularWidget('option', function(){
|
||||
this.descend(true);
|
||||
this.directives(true);
|
||||
return function(element) {
|
||||
this.$postEval(element.parent().data('$update'));
|
||||
var select = element.parent();
|
||||
var scope = retrieveScope(select);
|
||||
var model = modelFormattedAccessor(scope, select);
|
||||
var view = valueAccessor(scope, select);
|
||||
var option = element;
|
||||
var lastValue = option.attr($value);
|
||||
var lastSelected = option.attr('ng-' + $selected);
|
||||
element.data($$update, function(){
|
||||
var value = option.attr($value);
|
||||
var selected = option.attr('ng-' + $selected);
|
||||
var modelValue = model.get();
|
||||
if (lastSelected != selected || lastValue != value) {
|
||||
lastSelected = selected;
|
||||
lastValue = value;
|
||||
if (selected || modelValue == _null || modelValue == _undefined)
|
||||
model.set(value);
|
||||
if (value == modelValue) {
|
||||
view.set(lastValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc widget
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('Angular', function(){
|
||||
scope.$init();
|
||||
scope.$eval();
|
||||
expect(onUpdateView).wasCalled();
|
||||
dealoc(scope);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -115,6 +115,36 @@ describe('api', function(){
|
||||
});
|
||||
|
||||
|
||||
describe('limit', function() {
|
||||
var items;
|
||||
|
||||
beforeEach(function() {
|
||||
items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
||||
});
|
||||
|
||||
|
||||
it('should return the first X items when X is positive', function() {
|
||||
expect(angular.Array.limitTo(items, 3)).toEqual(['a', 'b', 'c']);
|
||||
expect(angular.Array.limitTo(items, '3')).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
|
||||
it('should return the last X items when X is negative', function() {
|
||||
expect(angular.Array.limitTo(items, -3)).toEqual(['f', 'g', 'h']);
|
||||
expect(angular.Array.limitTo(items, '-3')).toEqual(['f', 'g', 'h']);
|
||||
});
|
||||
|
||||
|
||||
it('should return an empty array when X cannot be parsed', function() {
|
||||
expect(angular.Array.limitTo(items, 'bogus')).toEqual([]);
|
||||
expect(angular.Array.limitTo(items, 'null')).toEqual([]);
|
||||
expect(angular.Array.limitTo(items, 'undefined')).toEqual([]);
|
||||
expect(angular.Array.limitTo(items, null)).toEqual([]);
|
||||
expect(angular.Array.limitTo(items, undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Add', function(){
|
||||
var add = angular.Array.add;
|
||||
assertJsonEquals([{}, "a"], add(add([]),"a"));
|
||||
|
||||
@@ -5,6 +5,7 @@ BinderTest.prototype.setUp = function(){
|
||||
|
||||
this.compile = function(html, initialScope, parent) {
|
||||
var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget);
|
||||
if (self.element) dealoc(self.element);
|
||||
var element = self.element = jqLite(html);
|
||||
var scope = compiler.compile(element)(element);
|
||||
|
||||
|
||||
+37
-2
@@ -1,8 +1,21 @@
|
||||
describe('browser', function(){
|
||||
|
||||
var browser, location, head, xhr;
|
||||
var browser, location, head, xhr, setTimeoutQueue;
|
||||
|
||||
function fakeSetTimeout(fn) {
|
||||
setTimeoutQueue.push(fn);
|
||||
}
|
||||
|
||||
fakeSetTimeout.flush = function() {
|
||||
foreach(setTimeoutQueue, function(fn) {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
beforeEach(function(){
|
||||
setTimeoutQueue = [];
|
||||
|
||||
location = {href:"http://server", hash:""};
|
||||
head = {
|
||||
scripts: [],
|
||||
@@ -14,7 +27,7 @@ describe('browser', function(){
|
||||
this.open = noop;
|
||||
this.setRequestHeader = noop;
|
||||
this.send = noop;
|
||||
});
|
||||
}, undefined, fakeSetTimeout);
|
||||
});
|
||||
|
||||
it('should contain cookie cruncher', function() {
|
||||
@@ -59,6 +72,28 @@ describe('browser', function(){
|
||||
});
|
||||
|
||||
|
||||
describe('defer', function() {
|
||||
it('should execute fn asynchroniously via setTimeout', function() {
|
||||
var counter = 0;
|
||||
browser.defer(function() {counter++;});
|
||||
expect(counter).toBe(0);
|
||||
|
||||
fakeSetTimeout.flush();
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('should update outstandingRequests counter', function() {
|
||||
var callback = jasmine.createSpy('callback');
|
||||
browser.defer(callback);
|
||||
expect(callback).not.wasCalled();
|
||||
|
||||
fakeSetTimeout.flush();
|
||||
expect(callback).wasCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('cookies', function() {
|
||||
|
||||
function deleteAllCookies() {
|
||||
|
||||
+15
-10
@@ -1,5 +1,5 @@
|
||||
describe('compiler', function(){
|
||||
var compiler, markup, directives, widgets, compile, log;
|
||||
var compiler, markup, directives, widgets, compile, log, scope;
|
||||
|
||||
beforeEach(function(){
|
||||
log = "";
|
||||
@@ -32,6 +32,10 @@ describe('compiler', function(){
|
||||
return scope;
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
it('should recognize a directive', function(){
|
||||
var e = jqLite('<div directive="expr" ignore="me"></div>');
|
||||
@@ -44,7 +48,8 @@ describe('compiler', function(){
|
||||
};
|
||||
};
|
||||
var template = compiler.compile(e);
|
||||
var init = template(e).$init;
|
||||
scope = template(e);
|
||||
var init = scope.$init;
|
||||
expect(log).toEqual("found");
|
||||
init();
|
||||
expect(e.hasClass('ng-directive')).toEqual(true);
|
||||
@@ -52,12 +57,12 @@ describe('compiler', function(){
|
||||
});
|
||||
|
||||
it('should recurse to children', function(){
|
||||
var scope = compile('<div><span hello="misko"/></div>');
|
||||
scope = compile('<div><span hello="misko"/></div>');
|
||||
expect(log).toEqual("hello misko");
|
||||
});
|
||||
|
||||
it('should watch scope', function(){
|
||||
var scope = compile('<span watch="name"/>');
|
||||
scope = compile('<span watch="name"/>');
|
||||
expect(log).toEqual("");
|
||||
scope.$eval();
|
||||
scope.$set('name', 'misko');
|
||||
@@ -71,7 +76,7 @@ describe('compiler', function(){
|
||||
|
||||
it('should prevent descend', function(){
|
||||
directives.stop = function(){ this.descend(false); };
|
||||
var scope = compile('<span hello="misko" stop="true"><span hello="adam"/></span>');
|
||||
scope = compile('<span hello="misko" stop="true"><span hello="adam"/></span>');
|
||||
expect(log).toEqual("hello misko");
|
||||
});
|
||||
|
||||
@@ -87,7 +92,7 @@ describe('compiler', function(){
|
||||
});
|
||||
};
|
||||
};
|
||||
var scope = compile('before<span duplicate="expr">x</span>after');
|
||||
scope = compile('before<span duplicate="expr">x</span>after');
|
||||
expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span>after</div>');
|
||||
scope.$eval();
|
||||
expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span><span>x</span>after</div>');
|
||||
@@ -103,7 +108,7 @@ describe('compiler', function(){
|
||||
textNode[0].nodeValue = 'replaced';
|
||||
}
|
||||
});
|
||||
var scope = compile('before<span>middle</span>after');
|
||||
scope = compile('before<span>middle</span>after');
|
||||
expect(sortedHtml(scope.$element[0], true)).toEqual('<div>before<span class="ng-directive" hello="middle">replaced</span>after</div>');
|
||||
expect(log).toEqual("hello middle");
|
||||
});
|
||||
@@ -116,7 +121,7 @@ describe('compiler', function(){
|
||||
log += 'init';
|
||||
};
|
||||
};
|
||||
var scope = compile('<ng:button>push me</ng:button>');
|
||||
scope = compile('<ng:button>push me</ng:button>');
|
||||
expect(lowercase(scope.$element[0].innerHTML)).toEqual('<div>button</div>');
|
||||
expect(log).toEqual('init');
|
||||
});
|
||||
@@ -135,7 +140,7 @@ describe('compiler', function(){
|
||||
if (text == '{{1+2}}')
|
||||
parent.text('3');
|
||||
});
|
||||
var scope = compile('<div><h1>ignore me</h1></div>');
|
||||
scope = compile('<div><h1>ignore me</h1></div>');
|
||||
expect(scope.$element.text()).toEqual('3');
|
||||
});
|
||||
|
||||
@@ -158,7 +163,7 @@ describe('compiler', function(){
|
||||
textNode.remove();
|
||||
}
|
||||
});
|
||||
var scope = compile('A---B---C===D');
|
||||
scope = compile('A---B---C===D');
|
||||
expect(sortedHtml(scope.$element)).toEqual('<div>A<hr></hr>B<hr></hr>C<p></p>D</div>');
|
||||
});
|
||||
|
||||
|
||||
+16
-37
@@ -59,14 +59,6 @@ describe('parser', function() {
|
||||
expect(undefined).toEqual(tokens[i].fn());
|
||||
});
|
||||
|
||||
it('should tokenize RegExp', function() {
|
||||
var tokens = lex("/r 1/");
|
||||
var i = 0;
|
||||
expect(tokens[i].index).toEqual(0);
|
||||
expect(tokens[i].text).toEqual('r 1');
|
||||
expect("r 1".match(tokens[i].fn())[0]).toEqual('r 1');
|
||||
});
|
||||
|
||||
it('should tokenize quoted string', function() {
|
||||
var str = "['\\'', \"\\\"\"]";
|
||||
var tokens = lex(str);
|
||||
@@ -90,26 +82,15 @@ describe('parser', function() {
|
||||
expect(tokens.length).toEqual(1);
|
||||
expect(tokens[0].string).toEqual('\u00a0');
|
||||
});
|
||||
|
||||
it('should tokenize RegExp with options', function() {
|
||||
var tokens = lex("/r/g");
|
||||
var i = 0;
|
||||
expect(tokens[i].index).toEqual(0);
|
||||
expect(tokens[i].text).toEqual('r');
|
||||
expect(tokens[i].flags).toEqual('g');
|
||||
expect("rr".match(tokens[i].fn()).length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should tokenize RegExp with escaping', function() {
|
||||
var tokens = lex("/\\/\\d/");
|
||||
var i = 0;
|
||||
expect(tokens[i].index).toEqual(0);
|
||||
expect(tokens[i].text).toEqual('\\/\\d');
|
||||
expect("/1".match(tokens[i].fn())[0]).toEqual('/1');
|
||||
|
||||
it('should error when non terminated string', function(){
|
||||
expect(function(){
|
||||
lex('ignore "text');
|
||||
}).toThrow(new Error('Lexer Error: Unterminated string at column 7 in expression [ignore "text].'));
|
||||
});
|
||||
|
||||
it('should ignore whitespace', function() {
|
||||
var tokens = lex("a \t \n \r b");
|
||||
var tokens = lex("a \t \n \r \u00A0 b");
|
||||
expect(tokens[0].text).toEqual('a');
|
||||
expect(tokens[1].text).toEqual('b');
|
||||
});
|
||||
@@ -155,16 +136,6 @@ describe('parser', function() {
|
||||
expect(tokens[0].text).toEqual(0.5E+10);
|
||||
});
|
||||
|
||||
it('should throws exception for invalid exponent', function() {
|
||||
expect(function() {
|
||||
lex("0.5E-");
|
||||
}).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-].'));
|
||||
|
||||
expect(function() {
|
||||
lex("0.5E-A");
|
||||
}).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].'));
|
||||
});
|
||||
|
||||
it('should tokenize number starting with a dot', function() {
|
||||
var tokens = lex(".5");
|
||||
expect(tokens[0].text).toEqual(0.5);
|
||||
@@ -172,8 +143,8 @@ describe('parser', function() {
|
||||
|
||||
it('should throw error on invalid unicode', function() {
|
||||
expect(function() {
|
||||
lex("'\\u1''bla'");
|
||||
}).toThrow(new Error("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']."));
|
||||
lex("'\\u1xbla'");
|
||||
}).toThrow(new Error("Lexer Error: Invalid unicode escape [\\u1xbl] at columns 0-9 ['\\u1xbla'] in expression ['\\u1xbla']."));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,4 +384,12 @@ describe('parser', function() {
|
||||
expect(scope.$eval("a=undefined")).not.toBeDefined();
|
||||
expect(scope.$get("a")).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow assignment after array dereference', function(){
|
||||
scope = angular.scope();
|
||||
scope.obj = [{}];
|
||||
scope.$eval('obj[0].name=1');
|
||||
expect(scope.obj.name).toBeUndefined();
|
||||
expect(scope.obj[0].name).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,6 +169,7 @@ describe("resource", function() {
|
||||
var person = Person.get({id:123});
|
||||
$browser.xhr.flush();
|
||||
expect(person.name).toEqual('misko');
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
it('should return the same object when verifying the cache', function(){
|
||||
@@ -183,11 +184,14 @@ describe("resource", function() {
|
||||
|
||||
$browser.xhr.expectGET('/Person/123').respond('[\n{\nname:\n"rob"\n}\n]');
|
||||
var person2 = Person.query({id:123});
|
||||
$browser.defer.flush();
|
||||
|
||||
expect(person2[0].name).toEqual('misko');
|
||||
var person2Cache = person2;
|
||||
$browser.xhr.flush();
|
||||
expect(person2Cache).toEqual(person2);
|
||||
expect(person2[0].name).toEqual('rob');
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
describe('failure mode', function(){
|
||||
|
||||
+58
-46
@@ -1,52 +1,64 @@
|
||||
describe("ScenarioSpec: Compilation", function(){
|
||||
it("should compile dom node and return scope", function(){
|
||||
var node = jqLite('<div ng:init="a=1">{{b=a+1}}</div>')[0];
|
||||
var scope = compile(node);
|
||||
scope.$init();
|
||||
expect(scope.a).toEqual(1);
|
||||
expect(scope.b).toEqual(2);
|
||||
var scope;
|
||||
|
||||
beforeEach(function(){
|
||||
scope = null;
|
||||
});
|
||||
|
||||
it("should compile jQuery node and return scope", function(){
|
||||
var scope = compile(jqLite('<div>{{a=123}}</div>')).$init();
|
||||
expect(jqLite(scope.$element).text()).toEqual('123');
|
||||
|
||||
afterEach(function(){
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
it("should compile text node and return scope", function(){
|
||||
var scope = compile('<div>{{a=123}}</div>').$init();
|
||||
expect(jqLite(scope.$element).text()).toEqual('123');
|
||||
|
||||
describe('compilation', function(){
|
||||
it("should compile dom node and return scope", function(){
|
||||
var node = jqLite('<div ng:init="a=1">{{b=a+1}}</div>')[0];
|
||||
scope = compile(node);
|
||||
scope.$init();
|
||||
expect(scope.a).toEqual(1);
|
||||
expect(scope.b).toEqual(2);
|
||||
});
|
||||
|
||||
it("should compile jQuery node and return scope", function(){
|
||||
scope = compile(jqLite('<div>{{a=123}}</div>')).$init();
|
||||
expect(jqLite(scope.$element).text()).toEqual('123');
|
||||
});
|
||||
|
||||
it("should compile text node and return scope", function(){
|
||||
scope = compile('<div>{{a=123}}</div>').$init();
|
||||
expect(jqLite(scope.$element).text()).toEqual('123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ScenarioSpec: Scope", function(){
|
||||
it("should have set, get, eval, $init, updateView methods", function(){
|
||||
var scope = compile('<div>{{a}}</div>').$init();
|
||||
scope.$eval("$invalidWidgets.push({})");
|
||||
expect(scope.$set("a", 2)).toEqual(2);
|
||||
expect(scope.$get("a")).toEqual(2);
|
||||
expect(scope.$eval("a=3")).toEqual(3);
|
||||
scope.$eval();
|
||||
expect(jqLite(scope.$element).text()).toEqual('3');
|
||||
|
||||
describe('scope', function(){
|
||||
it("should have set, get, eval, $init, updateView methods", function(){
|
||||
scope = compile('<div>{{a}}</div>').$init();
|
||||
scope.$eval("$invalidWidgets.push({})");
|
||||
expect(scope.$set("a", 2)).toEqual(2);
|
||||
expect(scope.$get("a")).toEqual(2);
|
||||
expect(scope.$eval("a=3")).toEqual(3);
|
||||
scope.$eval();
|
||||
expect(jqLite(scope.$element).text()).toEqual('3');
|
||||
});
|
||||
|
||||
it("should have $ objects", function(){
|
||||
scope = compile('<div></div>', {$config: {a:"b"}});
|
||||
expect(scope.$get('$location')).toBeDefined();
|
||||
expect(scope.$get('$eval')).toBeDefined();
|
||||
expect(scope.$get('$config')).toBeDefined();
|
||||
expect(scope.$get('$config.a')).toEqual("b");
|
||||
});
|
||||
});
|
||||
|
||||
it("should have $ objects", function(){
|
||||
var scope = compile('<div></div>', {$config: {a:"b"}});
|
||||
expect(scope.$get('$location')).toBeDefined();
|
||||
expect(scope.$get('$eval')).toBeDefined();
|
||||
expect(scope.$get('$config')).toBeDefined();
|
||||
expect(scope.$get('$config.a')).toEqual("b");
|
||||
|
||||
describe("configuration", function(){
|
||||
it("should take location object", function(){
|
||||
var url = "http://server/#?book=moby";
|
||||
scope = compile("<div>{{$location}}</div>");
|
||||
var $location = scope.$location;
|
||||
var $browser = scope.$inject('$browser');
|
||||
expect($location.hashSearch.book).toBeUndefined();
|
||||
$browser.setUrl(url);
|
||||
$browser.poll();
|
||||
expect($location.hashSearch.book).toEqual('moby');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ScenarioSpec: configuration", function(){
|
||||
it("should take location object", function(){
|
||||
var url = "http://server/#?book=moby";
|
||||
var scope = compile("<div>{{$location}}</div>");
|
||||
var $location = scope.$location;
|
||||
var $browser = scope.$inject('$browser');
|
||||
expect($location.hashSearch.book).toBeUndefined();
|
||||
$browser.setUrl(url);
|
||||
$browser.poll();
|
||||
expect($location.hashSearch.book).toEqual('moby');
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-22
@@ -52,6 +52,11 @@ describe('scope/model', function(){
|
||||
model.$eval('name="works"');
|
||||
expect(model.name).toEqual('works');
|
||||
});
|
||||
|
||||
it('should not bind regexps', function(){
|
||||
model.exp = /abc/;
|
||||
expect(model.$eval('exp')).toEqual(model.exp);
|
||||
});
|
||||
|
||||
it('should do nothing on empty string and not update view', function(){
|
||||
var onEval = jasmine.createSpy('onEval');
|
||||
@@ -209,28 +214,6 @@ describe('scope/model', function(){
|
||||
});
|
||||
});
|
||||
|
||||
describe('$postEval', function(){
|
||||
it('should eval function once and last', function(){
|
||||
var log = '';
|
||||
var scope = createScope();
|
||||
function onceOnly(){log+= '@';}
|
||||
scope.$onEval(function(){log+= '.';});
|
||||
scope.$postEval(function(){log+= '!';});
|
||||
scope.$postEval(onceOnly);
|
||||
scope.$postEval(onceOnly);
|
||||
scope.$postEval(); // ignore
|
||||
scope.$eval();
|
||||
expect(log).toEqual('.!@');
|
||||
scope.$eval();
|
||||
expect(log).toEqual('.!@.');
|
||||
|
||||
scope.$postEval(onceOnly);
|
||||
scope.$postEval(onceOnly);
|
||||
scope.$eval();
|
||||
expect(log).toEqual('.!@..@');
|
||||
});
|
||||
});
|
||||
|
||||
describe('$new', function(){
|
||||
it('should $new should create new child scope and $become controller', function(){
|
||||
var parent = createScope(null, {exampleService: function(){return 'Example Service';}});
|
||||
|
||||
@@ -104,12 +104,6 @@ describe('Validator:asynchronous', function(){
|
||||
|
||||
afterEach(function(){
|
||||
if (self.$element) self.$element.remove();
|
||||
var oldCache = jqCache;
|
||||
jqCache = {};
|
||||
if (size(oldCache)) {
|
||||
dump(oldCache);
|
||||
}
|
||||
expect(size(oldCache)).toEqual(0);
|
||||
});
|
||||
|
||||
it('should make a request and show spinner', function(){
|
||||
|
||||
Vendored
+9
-1
@@ -113,6 +113,15 @@ function MockBrowser() {
|
||||
|
||||
self.cookieHash = {};
|
||||
self.lastCookieHash = {};
|
||||
self.deferredFns = [];
|
||||
|
||||
self.defer = function(fn) {
|
||||
self.deferredFns.push(fn);
|
||||
};
|
||||
|
||||
self.defer.flush = function() {
|
||||
while (self.deferredFns.length) self.deferredFns.shift()();
|
||||
};
|
||||
}
|
||||
MockBrowser.prototype = {
|
||||
|
||||
@@ -156,7 +165,6 @@ MockBrowser.prototype = {
|
||||
return this.cookieHash;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
angular.service('$browser', function(){
|
||||
|
||||
@@ -13,8 +13,7 @@ describe("directive", function(){
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
if (model && model.$element) model.$element.remove();
|
||||
expect(size(jqCache)).toEqual(0);
|
||||
dealoc(model);
|
||||
});
|
||||
|
||||
it("should ng:init", function() {
|
||||
|
||||
+87
-89
@@ -14,8 +14,7 @@ describe("markups", function(){
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
if (element) element.remove();
|
||||
expect(size(jqCache)).toEqual(0);
|
||||
dealoc(element);
|
||||
});
|
||||
|
||||
it('should translate {{}} in text', function(){
|
||||
@@ -63,92 +62,91 @@ describe("markups", function(){
|
||||
compile('<a ng:href="{{url}}" rel="{{rel}}"></a>');
|
||||
expect(sortedHtml(element)).toEqual('<a ng:bind-attr="{"href":"{{url}}","rel":"{{rel}}"}"></a>');
|
||||
});
|
||||
|
||||
it('should Parse Text With No Bindings', function(){
|
||||
var parts = parseBindings("a");
|
||||
assertEquals(parts.length, 1);
|
||||
assertEquals(parts[0], "a");
|
||||
assertTrue(!binding(parts[0]));
|
||||
});
|
||||
|
||||
it('should Parse Empty Text', function(){
|
||||
var parts = parseBindings("");
|
||||
assertEquals(parts.length, 1);
|
||||
assertEquals(parts[0], "");
|
||||
assertTrue(!binding(parts[0]));
|
||||
});
|
||||
|
||||
it('should Parse Inner Binding', function(){
|
||||
var parts = parseBindings("a{{b}}C");
|
||||
assertEquals(parts.length, 3);
|
||||
assertEquals(parts[0], "a");
|
||||
assertTrue(!binding(parts[0]));
|
||||
assertEquals(parts[1], "{{b}}");
|
||||
assertEquals(binding(parts[1]), "b");
|
||||
assertEquals(parts[2], "C");
|
||||
assertTrue(!binding(parts[2]));
|
||||
});
|
||||
|
||||
it('should Parse Ending Binding', function(){
|
||||
var parts = parseBindings("a{{b}}");
|
||||
assertEquals(parts.length, 2);
|
||||
assertEquals(parts[0], "a");
|
||||
assertTrue(!binding(parts[0]));
|
||||
assertEquals(parts[1], "{{b}}");
|
||||
assertEquals(binding(parts[1]), "b");
|
||||
});
|
||||
|
||||
it('should Parse Begging Binding', function(){
|
||||
var parts = parseBindings("{{b}}c");
|
||||
assertEquals(parts.length, 2);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
assertEquals(parts[1], "c");
|
||||
assertTrue(!binding(parts[1]));
|
||||
});
|
||||
|
||||
it('should Parse Loan Binding', function(){
|
||||
var parts = parseBindings("{{b}}");
|
||||
assertEquals(parts.length, 1);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
});
|
||||
|
||||
it('should Parse Two Bindings', function(){
|
||||
var parts = parseBindings("{{b}}{{c}}");
|
||||
assertEquals(parts.length, 2);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
assertEquals(parts[1], "{{c}}");
|
||||
assertEquals(binding(parts[1]), "c");
|
||||
});
|
||||
|
||||
it('should Parse Two Bindings With Text In Middle', function(){
|
||||
var parts = parseBindings("{{b}}x{{c}}");
|
||||
assertEquals(parts.length, 3);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
assertEquals(parts[1], "x");
|
||||
assertTrue(!binding(parts[1]));
|
||||
assertEquals(parts[2], "{{c}}");
|
||||
assertEquals(binding(parts[2]), "c");
|
||||
});
|
||||
|
||||
it('should Parse Multiline', function(){
|
||||
var parts = parseBindings('"X\nY{{A\nB}}C\nD"');
|
||||
assertTrue(!!binding('{{A\nB}}'));
|
||||
assertEquals(parts.length, 3);
|
||||
assertEquals(parts[0], '"X\nY');
|
||||
assertEquals(parts[1], '{{A\nB}}');
|
||||
assertEquals(parts[2], 'C\nD"');
|
||||
});
|
||||
|
||||
it('should Has Binding', function(){
|
||||
assertTrue(hasBindings(parseBindings("{{a}}")));
|
||||
assertTrue(!hasBindings(parseBindings("a")));
|
||||
assertTrue(hasBindings(parseBindings("{{b}}x{{c}}")));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
var BindingMarkupTest = TestCase("BindingMarkupTest");
|
||||
|
||||
BindingMarkupTest.prototype.testParseTextWithNoBindings = function(){
|
||||
var parts = parseBindings("a");
|
||||
assertEquals(parts.length, 1);
|
||||
assertEquals(parts[0], "a");
|
||||
assertTrue(!binding(parts[0]));
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseEmptyText = function(){
|
||||
var parts = parseBindings("");
|
||||
assertEquals(parts.length, 1);
|
||||
assertEquals(parts[0], "");
|
||||
assertTrue(!binding(parts[0]));
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseInnerBinding = function(){
|
||||
var parts = parseBindings("a{{b}}c");
|
||||
assertEquals(parts.length, 3);
|
||||
assertEquals(parts[0], "a");
|
||||
assertTrue(!binding(parts[0]));
|
||||
assertEquals(parts[1], "{{b}}");
|
||||
assertEquals(binding(parts[1]), "b");
|
||||
assertEquals(parts[2], "c");
|
||||
assertTrue(!binding(parts[2]));
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseEndingBinding = function(){
|
||||
var parts = parseBindings("a{{b}}");
|
||||
assertEquals(parts.length, 2);
|
||||
assertEquals(parts[0], "a");
|
||||
assertTrue(!binding(parts[0]));
|
||||
assertEquals(parts[1], "{{b}}");
|
||||
assertEquals(binding(parts[1]), "b");
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseBeggingBinding = function(){
|
||||
var parts = parseBindings("{{b}}c");
|
||||
assertEquals(parts.length, 2);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
assertEquals(parts[1], "c");
|
||||
assertTrue(!binding(parts[1]));
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseLoanBinding = function(){
|
||||
var parts = parseBindings("{{b}}");
|
||||
assertEquals(parts.length, 1);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseTwoBindings = function(){
|
||||
var parts = parseBindings("{{b}}{{c}}");
|
||||
assertEquals(parts.length, 2);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
assertEquals(parts[1], "{{c}}");
|
||||
assertEquals(binding(parts[1]), "c");
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseTwoBindingsWithTextInMiddle = function(){
|
||||
var parts = parseBindings("{{b}}x{{c}}");
|
||||
assertEquals(parts.length, 3);
|
||||
assertEquals(parts[0], "{{b}}");
|
||||
assertEquals(binding(parts[0]), "b");
|
||||
assertEquals(parts[1], "x");
|
||||
assertTrue(!binding(parts[1]));
|
||||
assertEquals(parts[2], "{{c}}");
|
||||
assertEquals(binding(parts[2]), "c");
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testParseMultiline = function(){
|
||||
var parts = parseBindings('"X\nY{{A\nB}}C\nD"');
|
||||
assertTrue(!!binding('{{A\nB}}'));
|
||||
assertEquals(parts.length, 3);
|
||||
assertEquals(parts[0], '"X\nY');
|
||||
assertEquals(parts[1], '{{A\nB}}');
|
||||
assertEquals(parts[2], 'C\nD"');
|
||||
};
|
||||
|
||||
BindingMarkupTest.prototype.testHasBinding = function(){
|
||||
assertTrue(hasBindings(parseBindings("{{a}}")));
|
||||
assertTrue(!hasBindings(parseBindings("a")));
|
||||
assertTrue(hasBindings(parseBindings("{{b}}x{{c}}")));
|
||||
};
|
||||
|
||||
+84
-20
@@ -6,7 +6,7 @@ describe('HTML', function(){
|
||||
|
||||
it('should echo html', function(){
|
||||
expectHTML('hello<b class="1\'23" align=\'""\'>world</b>.').
|
||||
toEqual('hello<b class="1\'23" align="""">world</b>.');
|
||||
toEqual('hello<b class="1\'23" align="""">world</b>.');
|
||||
});
|
||||
|
||||
it('should remove script', function(){
|
||||
@@ -33,7 +33,7 @@ describe('HTML', function(){
|
||||
expectHTML('a<SCRIPT>ev<script>evil</sCript>il</scrIpt>c.').toEqual('ac.');
|
||||
});
|
||||
|
||||
it('should remove unknown tag names', function(){
|
||||
it('should remove unknown names', function(){
|
||||
expectHTML('a<xxx><B>b</B></xxx>c').toEqual('a<b>b</b>c');
|
||||
});
|
||||
|
||||
@@ -49,16 +49,34 @@ describe('HTML', function(){
|
||||
expectHTML('a<my:hr/><my:div>b</my:div>c').toEqual('abc');
|
||||
});
|
||||
|
||||
it('should handle entities', function(){
|
||||
var everything = '<div rel="!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ">' +
|
||||
'!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ</div>';
|
||||
expectHTML(everything).toEqual(everything);
|
||||
});
|
||||
|
||||
it('should handle improper html', function(){
|
||||
expectHTML('< div id="</div>" alt=abc href=\'"\' >text< /div>').
|
||||
toEqual('<div id="</div>" alt="abc" href=""">text</div>');
|
||||
expectHTML('< div rel="</div>" alt=abc dir=\'"\' >text< /div>').
|
||||
toEqual('<div rel="</div>" alt="abc" dir=""">text</div>');
|
||||
});
|
||||
|
||||
it('should handle improper html2', function(){
|
||||
expectHTML('< div id="</div>" / >').
|
||||
toEqual('<div id="</div>"/>');
|
||||
expectHTML('< div rel="</div>" / >').
|
||||
toEqual('<div rel="</div>"/>');
|
||||
});
|
||||
|
||||
|
||||
it('should ignore back slash as escape', function(){
|
||||
expectHTML('<img alt="xxx\\" title="><script>....">').
|
||||
toEqual('<img alt="xxx\\" title="><script>...."/>');
|
||||
});
|
||||
|
||||
it('should ignore object attributes', function(){
|
||||
expectHTML('<a constructor="hola">:)</a>').
|
||||
toEqual('<a>:)</a>');
|
||||
expectHTML('<constructor constructor="hola">:)</constructor>').
|
||||
toEqual('');
|
||||
});
|
||||
|
||||
describe('htmlSanitizerWriter', function(){
|
||||
var writer, html;
|
||||
beforeEach(function(){
|
||||
@@ -68,12 +86,12 @@ describe('HTML', function(){
|
||||
|
||||
it('should write basic HTML', function(){
|
||||
writer.chars('before');
|
||||
writer.start('div', {id:'123'}, false);
|
||||
writer.start('div', {rel:'123'}, false);
|
||||
writer.chars('in');
|
||||
writer.end('div');
|
||||
writer.chars('after');
|
||||
|
||||
expect(html).toEqual('before<div id="123">in</div>after');
|
||||
expect(html).toEqual('before<div rel="123">in</div>after');
|
||||
});
|
||||
|
||||
it('should escape text nodes', function(){
|
||||
@@ -81,19 +99,14 @@ describe('HTML', function(){
|
||||
expect(html).toEqual('a<div>&</div>c');
|
||||
});
|
||||
|
||||
it('should not double escape entities', function(){
|
||||
writer.chars(' ><');
|
||||
expect(html).toEqual(' ><');
|
||||
});
|
||||
|
||||
it('should escape IE script', function(){
|
||||
writer.chars('&{}');
|
||||
expect(html).toEqual('&{}');
|
||||
writer.chars('&<>{}');
|
||||
expect(html).toEqual('&<>{}');
|
||||
});
|
||||
|
||||
it('should escape attributes', function(){
|
||||
writer.start('div', {id:'\"\'<>'});
|
||||
expect(html).toEqual('<div id=""\'<>">');
|
||||
writer.start('div', {rel:'!@#$%^&*()_+-={}[]:";\'<>?,./`~ \n\0\r\u0127'});
|
||||
expect(html).toEqual('<div rel="!@#$%^&*()_+-={}[]:";\'<>?,./`~ � ħ">');
|
||||
});
|
||||
|
||||
it('should ignore missformed elements', function(){
|
||||
@@ -105,12 +118,63 @@ describe('HTML', function(){
|
||||
writer.start('div', {unknown:""});
|
||||
expect(html).toEqual('<div>');
|
||||
});
|
||||
|
||||
describe('explicitly dissallow', function(){
|
||||
it('should not allow attributes', function(){
|
||||
writer.start('div', {id:'a', name:'a', style:'a'});
|
||||
expect(html).toEqual('<div>');
|
||||
});
|
||||
|
||||
it('should not allow tags', function(){
|
||||
function tag(name) {
|
||||
writer.start(name, {});
|
||||
writer.end(name);
|
||||
};
|
||||
tag('frameset');
|
||||
tag('frame');
|
||||
tag('form');
|
||||
tag('param');
|
||||
tag('object');
|
||||
tag('embed');
|
||||
tag('textarea');
|
||||
tag('input');
|
||||
tag('button');
|
||||
tag('option');
|
||||
tag('select');
|
||||
tag('script');
|
||||
tag('style');
|
||||
tag('link');
|
||||
tag('base');
|
||||
tag('basefont');
|
||||
expect(html).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUri', function(){
|
||||
|
||||
function isUri(value) {
|
||||
return value.match(URI_REGEXP);
|
||||
}
|
||||
|
||||
it('should be URI', function(){
|
||||
expect(isUri('http://abc')).toBeTruthy();
|
||||
expect(isUri('https://abc')).toBeTruthy();
|
||||
expect(isUri('ftp://abc')).toBeTruthy();
|
||||
expect(isUri('mailto:me@example.com')).toBeTruthy();
|
||||
expect(isUri('#anchor')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not be UIR', function(){
|
||||
expect(isUri('')).toBeFalsy();
|
||||
expect(isUri('javascript:alert')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('javascript URL attribute', function(){
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toBeValidUrl: function(){
|
||||
return !isJavaScriptUrl(this.actual);
|
||||
return URI_REGEXP.exec(this.actual);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -118,7 +182,7 @@ describe('HTML', function(){
|
||||
it('should ignore javascript:', function(){
|
||||
expect('JavaScript:abc').not.toBeValidUrl();
|
||||
expect(' \n Java\n Script:abc').not.toBeValidUrl();
|
||||
expect('JavaScript/my.js').toBeValidUrl();
|
||||
expect('http://JavaScript/my.js').toBeValidUrl();
|
||||
});
|
||||
|
||||
it('should ignore dec encoded javascript:', function(){
|
||||
|
||||
+119
-7
@@ -17,8 +17,7 @@ describe("service", function(){
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
if (scope && scope.$element)
|
||||
scope.$element.remove();
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
|
||||
@@ -202,7 +201,7 @@ describe("service", function(){
|
||||
});
|
||||
|
||||
it('should update hash before any processing', function(){
|
||||
var scope = compile('<div>');
|
||||
scope = compile('<div>');
|
||||
var log = '';
|
||||
scope.$watch('$location.hash', function(){
|
||||
log += this.$location.hashPath + ';';
|
||||
@@ -259,7 +258,7 @@ describe("service", function(){
|
||||
|
||||
describe("$invalidWidgets", function(){
|
||||
it("should count number of invalid widgets", function(){
|
||||
var scope = compile('<input name="price" ng:required ng:validate="number"></input>');
|
||||
scope = compile('<input name="price" ng:required ng:validate="number"></input>');
|
||||
jqLite(document.body).append(scope.$element);
|
||||
scope.$init();
|
||||
expect(scope.$invalidWidgets.length).toEqual(1);
|
||||
@@ -291,8 +290,8 @@ describe("service", function(){
|
||||
function BookChapter() {
|
||||
this.log = '<init>';
|
||||
}
|
||||
var scope = compile('<div></div>').$init();
|
||||
var $route = scope.$inject('$route');
|
||||
scope = compile('<div></div>').$init();
|
||||
$route = scope.$inject('$route');
|
||||
$route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'});
|
||||
$route.when('/Blank');
|
||||
$route.onChange(function(){
|
||||
@@ -330,6 +329,76 @@ describe("service", function(){
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('$defer', function() {
|
||||
var $defer, $exceptionHandler;
|
||||
|
||||
beforeEach(function(){
|
||||
scope = createScope({}, angularService, {
|
||||
'$exceptionHandler': jasmine.createSpy('$exceptionHandler')
|
||||
});
|
||||
|
||||
$browser = scope.$inject('$browser');
|
||||
$defer = scope.$inject('$defer');
|
||||
$exceptionHandler = scope.$inject('$exceptionHandler');
|
||||
});
|
||||
|
||||
|
||||
it('should delegate functions to $browser.defer', function() {
|
||||
var counter = 0;
|
||||
$defer(function() { counter++; });
|
||||
|
||||
expect(counter).toBe(0);
|
||||
|
||||
$browser.defer.flush();
|
||||
expect(counter).toBe(1);
|
||||
|
||||
$browser.defer.flush(); //does nothing
|
||||
expect(counter).toBe(1);
|
||||
|
||||
expect($exceptionHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should delegate exception to the $exceptionHandler service', function() {
|
||||
$defer(function() {throw "Test Error";});
|
||||
expect($exceptionHandler).not.toHaveBeenCalled();
|
||||
|
||||
$browser.defer.flush();
|
||||
expect($exceptionHandler).toHaveBeenCalledWith("Test Error");
|
||||
});
|
||||
|
||||
|
||||
it('should call eval after each callback is executed', function() {
|
||||
var eval = this.spyOn(scope, '$eval').andCallThrough();
|
||||
|
||||
$defer(function() {});
|
||||
expect(eval).wasNotCalled();
|
||||
|
||||
$browser.defer.flush();
|
||||
expect(eval).wasCalled();
|
||||
|
||||
eval.reset(); //reset the spy;
|
||||
|
||||
$defer(function() {});
|
||||
$defer(function() {});
|
||||
$browser.defer.flush();
|
||||
expect(eval.callCount).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
it('should call eval even if an exception is thrown in callback', function() {
|
||||
var eval = this.spyOn(scope, '$eval').andCallThrough();
|
||||
|
||||
$defer(function() {throw "Test Error"});
|
||||
expect(eval).wasNotCalled();
|
||||
|
||||
$browser.defer.flush();
|
||||
expect(eval).wasCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('$xhr', function(){
|
||||
var log;
|
||||
function callback(code, response) {
|
||||
@@ -427,12 +496,15 @@ describe("service", function(){
|
||||
$browserXhr.expectGET('/url').respond('first');
|
||||
cache('GET', '/url', null, callback);
|
||||
$browserXhr.flush();
|
||||
|
||||
$browserXhr.expectGET('/url').respond('ERROR');
|
||||
cache('GET', '/url', null, callback);
|
||||
$browser.defer.flush();
|
||||
$browserXhr.flush();
|
||||
expect(log).toEqual('"first";"first";');
|
||||
|
||||
cache('GET', '/url', null, callback, false);
|
||||
$browserXhr.flush();
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual('"first";"first";"first";');
|
||||
});
|
||||
|
||||
@@ -440,9 +512,12 @@ describe("service", function(){
|
||||
$browserXhr.expectGET('/url').respond('first');
|
||||
cache('GET', '/url', null, callback, true);
|
||||
$browserXhr.flush();
|
||||
|
||||
$browserXhr.expectGET('/url').respond('ERROR');
|
||||
cache('GET', '/url', null, callback, true);
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual('"first";"first";');
|
||||
|
||||
$browserXhr.flush();
|
||||
expect(log).toEqual('"first";"first";"ERROR";');
|
||||
});
|
||||
@@ -450,8 +525,11 @@ describe("service", function(){
|
||||
it('should serve requests from cache', function(){
|
||||
cache.data.url = {value:'123'};
|
||||
cache('GET', 'url', null, callback);
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual('"123";');
|
||||
|
||||
cache('GET', 'url', null, callback, false);
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual('"123";"123";');
|
||||
});
|
||||
|
||||
@@ -479,6 +557,40 @@ describe("service", function(){
|
||||
cache('POST', 'abc', {});
|
||||
expect(cache.data.url).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should call callback asynchronously for both cache hit and cache miss', function() {
|
||||
$browserXhr.expectGET('/url').respond('+');
|
||||
cache('GET', '/url', null, callback);
|
||||
expect(log).toEqual(''); //callback hasn't executed
|
||||
|
||||
$browserXhr.flush();
|
||||
expect(log).toEqual('"+";'); //callback has executed
|
||||
|
||||
cache('GET', '/url', null, callback);
|
||||
expect(log).toEqual('"+";'); //callback hasn't executed
|
||||
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual('"+";"+";'); //callback has executed
|
||||
});
|
||||
|
||||
it('should call eval after callbacks for both cache hit and cache miss execute', function() {
|
||||
var eval = this.spyOn(scope, '$eval').andCallThrough();
|
||||
|
||||
$browserXhr.expectGET('/url').respond('+');
|
||||
cache('GET', '/url', null, callback);
|
||||
expect(eval).wasNotCalled();
|
||||
|
||||
$browserXhr.flush();
|
||||
expect(eval).wasCalled();
|
||||
|
||||
eval.reset(); //reset the spy
|
||||
|
||||
cache('GET', '/url', null, callback);
|
||||
expect(eval).wasNotCalled();
|
||||
|
||||
$browser.defer.flush();
|
||||
expect(eval).wasCalled();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ if (window.jstestdriver) {
|
||||
}
|
||||
|
||||
beforeEach(function(){
|
||||
// This is to reset parsers global cache of expressions.
|
||||
compileCache = {};
|
||||
this.addMatchers({
|
||||
toBeInvalid: function(){
|
||||
var element = jqLite(this.actual);
|
||||
@@ -50,6 +52,25 @@ beforeEach(function(){
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(clearJqCache);
|
||||
|
||||
function clearJqCache(){
|
||||
var count = 0;
|
||||
foreachSorted(jqCache, function(value, key){
|
||||
count ++;
|
||||
delete jqCache[key];
|
||||
foreach(value, function(value, key){
|
||||
if (value.$element)
|
||||
dump(key, sortedHtml(value.$element));
|
||||
else
|
||||
dump(key, toJson(value));
|
||||
});
|
||||
});
|
||||
if (count) {
|
||||
fail('Found jqCache references that were not deallocated!');
|
||||
}
|
||||
}
|
||||
|
||||
function nakedExpect(obj) {
|
||||
return expect(angular.fromJson(angular.toJson(obj)));
|
||||
}
|
||||
@@ -58,6 +79,11 @@ function childNode(element, index) {
|
||||
return jqLite(element[0].childNodes[index]);
|
||||
}
|
||||
|
||||
function dealoc(obj) {
|
||||
var element = (obj||{}).$element || obj;
|
||||
if (element && element.dealoc) element.dealoc();
|
||||
}
|
||||
|
||||
extend(angular, {
|
||||
'element': jqLite,
|
||||
'compile': compile,
|
||||
|
||||
+48
-5
@@ -16,8 +16,7 @@ describe("widget", function(){
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
if (element && element.dealoc) element.dealoc();
|
||||
expect(size(jqCache)).toEqual(0);
|
||||
dealoc(element);
|
||||
});
|
||||
|
||||
describe("input", function(){
|
||||
@@ -42,6 +41,13 @@ describe("widget", function(){
|
||||
expect(scope.$get('name')).toEqual('Kai');
|
||||
expect(scope.$get('count')).toEqual(2);
|
||||
});
|
||||
|
||||
it('should allow complex refernce binding', function(){
|
||||
compile('<div ng:init="obj={abc:{}}">'+
|
||||
'<input type="Text" name="obj[\'abc\'].name" value="Misko""/>'+
|
||||
'</div>');
|
||||
expect(scope.obj['abc'].name).toEqual('Misko');
|
||||
});
|
||||
|
||||
describe("ng:format", function(){
|
||||
|
||||
@@ -362,7 +368,7 @@ describe("widget", function(){
|
||||
'<option value="{{$index}}" ng:repeat="name in [\'A\', \'B\', \'C\']">{{name}}</option>' +
|
||||
'</select>');
|
||||
// childNodes[0] is repeater comment
|
||||
expect(scope.selection).toEqual(undefined);
|
||||
expect(scope.selection).toEqual(0);
|
||||
|
||||
browserTrigger(element[0].childNodes[2], 'change');
|
||||
expect(scope.selection).toEqual(1);
|
||||
@@ -398,6 +404,32 @@ describe("widget", function(){
|
||||
scope.$eval();
|
||||
expect(element[0].childNodes[1].selected).toEqual(true);
|
||||
});
|
||||
|
||||
it('should select default option on repeater', function(){
|
||||
compile(
|
||||
'<select name="selection">' +
|
||||
'<option ng:repeat="no in [1,2]">{{no}}</option>' +
|
||||
'</select>');
|
||||
expect(scope.selection).toEqual('1');
|
||||
});
|
||||
|
||||
it('should select selected option on repeater', function(){
|
||||
compile(
|
||||
'<select name="selection">' +
|
||||
'<option ng:repeat="no in [1,2]">{{no}}</option>' +
|
||||
'<option selected>ABC</option>' +
|
||||
'</select>');
|
||||
expect(scope.selection).toEqual('ABC');
|
||||
});
|
||||
|
||||
it('should select dynamically selected option on repeater', function(){
|
||||
compile(
|
||||
'<select name="selection">' +
|
||||
'<option ng:repeat="no in [1,2]" ng:bind-attr="{selected:\'{{no==2}}\'}">{{no}}</option>' +
|
||||
'</select>');
|
||||
expect(scope.selection).toEqual('2');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should support type="select-multiple"', function(){
|
||||
@@ -476,6 +508,7 @@ describe("widget", function(){
|
||||
scope.url = '/Book/Moby';
|
||||
scope.$init();
|
||||
expect(scope.$element.text()).toEqual('Moby');
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
it("should match sandwich ids", function(){
|
||||
@@ -491,6 +524,7 @@ describe("widget", function(){
|
||||
scope.$init();
|
||||
expect(scope.name).toEqual(undefined);
|
||||
expect(scope.$element.text()).toEqual('works');
|
||||
dealoc(scope);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -503,7 +537,9 @@ describe("widget", function(){
|
||||
scope.url = 'myUrl';
|
||||
scope.$inject('$xhr.cache').data.myUrl = {value:'{{name}}'};
|
||||
scope.$init();
|
||||
scope.$inject('$browser').defer.flush();
|
||||
expect(element.text()).toEqual('misko');
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
it('should remove previously included text if a falsy value is bound to src', function() {
|
||||
@@ -514,6 +550,7 @@ describe("widget", function(){
|
||||
scope.url = 'myUrl';
|
||||
scope.$inject('$xhr.cache').data.myUrl = {value:'{{name}}'};
|
||||
scope.$init();
|
||||
scope.$inject('$browser').defer.flush();
|
||||
|
||||
expect(element.text()).toEqual('igor');
|
||||
|
||||
@@ -521,6 +558,7 @@ describe("widget", function(){
|
||||
scope.$eval();
|
||||
|
||||
expect(element.text()).toEqual('');
|
||||
dealoc(scope);
|
||||
});
|
||||
|
||||
it('should allow this for scope', function(){
|
||||
@@ -529,9 +567,12 @@ describe("widget", function(){
|
||||
scope.url = 'myUrl';
|
||||
scope.$inject('$xhr.cache').data.myUrl = {value:'{{c=c+1}}'};
|
||||
scope.$init();
|
||||
// This should not be 4, but to fix this properly
|
||||
// we need to have real events on the scopes.
|
||||
scope.$inject('$browser').defer.flush();
|
||||
|
||||
// this one should really be just '1', but due to lack of real events things are not working
|
||||
// properly. see discussion at: http://is.gd/ighKk
|
||||
expect(element.text()).toEqual('4');
|
||||
dealoc(element);
|
||||
});
|
||||
|
||||
it('should evaluate onload expression when a partial is loaded', function() {
|
||||
@@ -543,8 +584,10 @@ describe("widget", function(){
|
||||
scope.url = 'myUrl';
|
||||
scope.$inject('$xhr.cache').data.myUrl = {value:'my partial'};
|
||||
scope.$init();
|
||||
scope.$inject('$browser').defer.flush();
|
||||
expect(element.text()).toEqual('my partial');
|
||||
expect(scope.loaded).toBe(true);
|
||||
dealoc(element);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# <angular/> build config file
|
||||
---
|
||||
version: 0.9.4
|
||||
codename: total-recall
|
||||
version: 0.9.7
|
||||
codename: sonic-scream
|
||||
|
||||
Reference in New Issue
Block a user