diff --git a/src/app/services/annotationsSrv.js b/src/app/services/annotationsSrv.js
index fdc5e9ca745..68b5c85b5dd 100644
--- a/src/app/services/annotationsSrv.js
+++ b/src/app/services/annotationsSrv.js
@@ -7,7 +7,7 @@ define([
var module = angular.module('grafana.services');
- module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) {
+ module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope, $sanitize) {
var promiseCached;
var list = [];
var timezone;
@@ -63,9 +63,11 @@ define([
}
function addAnnotation(options) {
- var tooltip = "" + options.title + " ";
+ var title = $sanitize(options.title);
+ var tooltip = "" + title + " ";
if (options.tags) {
- tooltip += '' + (options.tags || '') + ' ';
+ var tags = $sanitize(options.tags);
+ tooltip += '' + (tags || '') + ' ';
}
if (timezone === 'browser') {
@@ -76,7 +78,8 @@ define([
}
if (options.text) {
- tooltip += options.text.replace(/\n/g, ' ');
+ var text = $sanitize(options.text);
+ tooltip += text.replace(/\n/g, ' ');
}
tooltip += "";
diff --git a/src/test/test-main.js b/src/test/test-main.js
index f0fc9bb4d51..21cbd5ff5fb 100644
--- a/src/test/test-main.js
+++ b/src/test/test-main.js
@@ -18,6 +18,7 @@ require.config({
angular: '../vendor/angular/angular',
'angular-route': '../vendor/angular/angular-route',
+ 'angular-sanitize': '../vendor/angular/angular-sanitize',
angularMocks: '../vendor/angular/angular-mocks',
'angular-dragdrop': '../vendor/angular/angular-dragdrop',
'angular-strap': '../vendor/angular/angular-strap',
@@ -80,14 +81,11 @@ require.config({
'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
'angular-route': ['angular'],
- 'angular-cookies': ['angular'],
+ 'angular-sanitize': ['angular'],
'angular-dragdrop': ['jquery', 'angular'],
- 'angular-loader': ['angular'],
'angular-mocks': ['angular'],
- 'angular-resource': ['angular'],
- 'angular-touch': ['angular'],
- 'bindonce': ['angular'],
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
+ 'bindonce': ['angular'],
'bootstrap-tagsinput': ['jquery'],
diff --git a/src/vendor/angular/angular-dragdrop.js b/src/vendor/angular/angular-dragdrop.js
index 40fb1c9e814..5eb196409e7 100644
--- a/src/vendor/angular/angular-dragdrop.js
+++ b/src/vendor/angular/angular-dragdrop.js
@@ -39,6 +39,7 @@ angular.module("ang-drag-drop",[])
element.attr("draggable", false);
attrs.$observe("uiDraggable", function (newValue) {
+ console.log(newValue);
if(newValue){
element.attr("draggable", newValue);
}
diff --git a/src/vendor/angular/angular-mocks.js b/src/vendor/angular/angular-mocks.js
index fa3fdba92b6..80219656eb6 100644
--- a/src/vendor/angular/angular-mocks.js
+++ b/src/vendor/angular/angular-mocks.js
@@ -1,5 +1,5 @@
/**
- * @license AngularJS v1.3.0-rc.3
+ * @license AngularJS v1.3.4
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
@@ -53,9 +53,10 @@ angular.mock.$Browser = function() {
self.onUrlChange = function(listener) {
self.pollFns.push(
function() {
- if (self.$$lastUrl != self.$$url) {
+ if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) {
self.$$lastUrl = self.$$url;
- listener(self.$$url);
+ self.$$lastState = self.$$state;
+ listener(self.$$url, self.$$state);
}
}
);
@@ -73,7 +74,7 @@ angular.mock.$Browser = function() {
self.defer = function(fn, delay) {
delay = delay || 0;
self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
- self.deferredFns.sort(function(a,b){ return a.time - b.time;});
+ self.deferredFns.sort(function(a, b) { return a.time - b.time;});
return self.deferredNextId++;
};
@@ -116,7 +117,7 @@ angular.mock.$Browser = function() {
self.defer.now += delay;
} else {
if (self.deferredFns.length) {
- self.defer.now = self.deferredFns[self.deferredFns.length-1].time;
+ self.defer.now = self.deferredFns[self.deferredFns.length - 1].time;
} else {
throw new Error('No deferred tasks to be flushed');
}
@@ -141,7 +142,7 @@ angular.mock.$Browser.prototype = {
* run all fns in pollFns
*/
poll: function poll() {
- angular.forEach(this.pollFns, function(pollFn){
+ angular.forEach(this.pollFns, function(pollFn) {
pollFn();
});
},
@@ -151,15 +152,24 @@ angular.mock.$Browser.prototype = {
return pollFn;
},
- url: function(url, replace) {
+ url: function(url, replace, state) {
+ if (angular.isUndefined(state)) {
+ state = null;
+ }
if (url) {
this.$$url = url;
+ // Native pushState serializes & copies the object; simulate it.
+ this.$$state = angular.copy(state);
return this;
}
return this.$$url;
},
+ state: function() {
+ return this.$$state;
+ },
+
cookies: function(name, value) {
if (name) {
if (angular.isUndefined(value)) {
@@ -191,7 +201,7 @@ angular.mock.$Browser.prototype = {
*
* @description
* Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors
- * passed into the `$exceptionHandler`.
+ * passed to the `$exceptionHandler`.
*/
/**
@@ -200,7 +210,7 @@ angular.mock.$Browser.prototype = {
*
* @description
* Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed
- * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
+ * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
* information.
*
*
@@ -240,16 +250,15 @@ angular.mock.$ExceptionHandlerProvider = function() {
*
* @param {string} mode Mode of operation, defaults to `rethrow`.
*
- * - `rethrow`: If any errors are passed into the handler in tests, it typically
- * means that there is a bug in the application or test, so this mock will
- * make these tests fail.
+ * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
+ * is a bug in the application or test, so this mock will make these tests fail.
* - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
* mode stores an array of errors in `$exceptionHandler.errors`, to allow later
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
* {@link ngMock.$log#reset reset()}
*/
this.mode = function(mode) {
- switch(mode) {
+ switch (mode) {
case 'rethrow':
handler = function(e) {
throw e;
@@ -307,7 +316,7 @@ angular.mock.$LogProvider = function() {
}
};
- this.$get = function () {
+ this.$get = function() {
var $log = {
log: function() { $log.log.logs.push(concat([], arguments, 0)); },
warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
@@ -327,13 +336,13 @@ angular.mock.$LogProvider = function() {
* @description
* Reset all of the logging arrays to empty.
*/
- $log.reset = function () {
+ $log.reset = function() {
/**
* @ngdoc property
* @name $log#log.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#log}.
+ * Array of messages logged using {@link ng.$log#log `log()`}.
*
* @example
* ```js
@@ -347,7 +356,7 @@ angular.mock.$LogProvider = function() {
* @name $log#info.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#info}.
+ * Array of messages logged using {@link ng.$log#info `info()`}.
*
* @example
* ```js
@@ -361,7 +370,7 @@ angular.mock.$LogProvider = function() {
* @name $log#warn.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#warn}.
+ * Array of messages logged using {@link ng.$log#warn `warn()`}.
*
* @example
* ```js
@@ -375,7 +384,7 @@ angular.mock.$LogProvider = function() {
* @name $log#error.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#error}.
+ * Array of messages logged using {@link ng.$log#error `error()`}.
*
* @example
* ```js
@@ -389,7 +398,7 @@ angular.mock.$LogProvider = function() {
* @name $log#debug.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#debug}.
+ * Array of messages logged using {@link ng.$log#debug `debug()`}.
*
* @example
* ```js
@@ -405,21 +414,21 @@ angular.mock.$LogProvider = function() {
* @name $log#assertEmpty
*
* @description
- * Assert that the all of the logging methods have no logged messages. If messages present, an
- * exception is thrown.
+ * Assert that all of the logging methods have no logged messages. If any messages are present,
+ * an exception is thrown.
*/
$log.assertEmpty = function() {
var errors = [];
angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) {
angular.forEach($log[logLevel].logs, function(log) {
- angular.forEach(log, function (logItem) {
+ angular.forEach(log, function(logItem) {
errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' +
(logItem.stack || ''));
});
});
});
if (errors.length) {
- errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+
+ errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " +
"an expected log message was not checked and removed:");
errors.push('');
throw new Error(errors.join('\n---------\n'));
@@ -452,17 +461,17 @@ angular.mock.$LogProvider = function() {
* @returns {promise} A promise which will be notified on each iteration.
*/
angular.mock.$IntervalProvider = function() {
- this.$get = ['$rootScope', '$q',
- function($rootScope, $q) {
+ this.$get = ['$browser', '$rootScope', '$q', '$$q',
+ function($browser, $rootScope, $q, $$q) {
var repeatFns = [],
nextRepeatId = 0,
now = 0;
var $interval = function(fn, delay, count, invokeApply) {
- var deferred = $q.defer(),
- promise = deferred.promise,
- iteration = 0,
- skipApply = (angular.isDefined(invokeApply) && !invokeApply);
+ var iteration = 0,
+ skipApply = (angular.isDefined(invokeApply) && !invokeApply),
+ deferred = (skipApply ? $$q : $q).defer(),
+ promise = deferred.promise;
count = (angular.isDefined(count)) ? count : 0;
promise.then(null, null, fn);
@@ -485,7 +494,11 @@ angular.mock.$IntervalProvider = function() {
}
}
- if (!skipApply) $rootScope.$apply();
+ if (skipApply) {
+ $browser.defer.flush();
+ } else {
+ $rootScope.$apply();
+ }
}
repeatFns.push({
@@ -495,7 +508,7 @@ angular.mock.$IntervalProvider = function() {
id: nextRepeatId,
deferred: deferred
});
- repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
+ repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
nextRepeatId++;
return promise;
@@ -511,7 +524,7 @@ angular.mock.$IntervalProvider = function() {
* @returns {boolean} Returns `true` if the task was successfully cancelled.
*/
$interval.cancel = function(promise) {
- if(!promise) return false;
+ if (!promise) return false;
var fnIndex;
angular.forEach(repeatFns, function(fn, index) {
@@ -544,7 +557,7 @@ angular.mock.$IntervalProvider = function() {
var task = repeatFns[0];
task.fn();
task.nextTime += task.delay;
- repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
+ repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
}
return millis;
};
@@ -572,10 +585,10 @@ function jsonStringToDate(string) {
tzMin = int(match[9] + match[11]);
}
date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
- date.setUTCHours(int(match[4]||0) - tzHour,
- int(match[5]||0) - tzMin,
- int(match[6]||0),
- int(match[7]||0));
+ date.setUTCHours(int(match[4] || 0) - tzHour,
+ int(match[5] || 0) - tzMin,
+ int(match[6] || 0),
+ int(match[7] || 0));
return date;
}
return string;
@@ -592,7 +605,7 @@ function padNumber(num, digits, trim) {
num = -num;
}
num = '' + num;
- while(num.length < digits) num = '0' + num;
+ while (num.length < digits) num = '0' + num;
if (trim)
num = num.substr(num.length - digits);
return neg + num;
@@ -636,7 +649,7 @@ function padNumber(num, digits, trim) {
* ```
*
*/
-angular.mock.TzDate = function (offset, timestamp) {
+angular.mock.TzDate = function(offset, timestamp) {
var self = new Date(0);
if (angular.isString(timestamp)) {
var tsStr = timestamp;
@@ -654,7 +667,7 @@ angular.mock.TzDate = function (offset, timestamp) {
}
var localOffset = new Date(timestamp).getTimezoneOffset();
- self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
+ self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60;
self.date = new Date(timestamp + self.offsetDiff);
self.getTime = function() {
@@ -779,20 +792,20 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
$provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser',
function($delegate, $$asyncCallback, $timeout, $browser) {
var animate = {
- queue : [],
- cancel : $delegate.cancel,
- enabled : $delegate.enabled,
- triggerCallbackEvents : function() {
+ queue: [],
+ cancel: $delegate.cancel,
+ enabled: $delegate.enabled,
+ triggerCallbackEvents: function() {
$$asyncCallback.flush();
},
- triggerCallbackPromise : function() {
+ triggerCallbackPromise: function() {
$timeout.flush(0);
},
- triggerCallbacks : function() {
+ triggerCallbacks: function() {
this.triggerCallbackEvents();
this.triggerCallbackPromise();
},
- triggerReflow : function() {
+ triggerReflow: function() {
angular.forEach(reflowQueue, function(fn) {
fn();
});
@@ -801,12 +814,13 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
};
angular.forEach(
- ['enter','leave','move','addClass','removeClass','setClass'], function(method) {
+ ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) {
animate[method] = function() {
animate.queue.push({
- event : method,
- element : arguments[0],
- args : arguments
+ event: method,
+ element: arguments[0],
+ options: arguments[arguments.length - 1],
+ args: arguments
});
return $delegate[method].apply($delegate, arguments);
};
@@ -873,13 +887,13 @@ angular.mock.dump = function(object) {
function serializeScope(scope, offset) {
offset = offset || ' ';
var log = [offset + 'Scope(' + scope.$id + '): {'];
- for ( var key in scope ) {
+ for (var key in scope) {
if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
log.push(' ' + key + ': ' + angular.toJson(scope[key]));
}
}
var child = scope.$$childHead;
- while(child) {
+ while (child) {
log.push(serializeScope(child, offset + ' '));
child = child.$$nextSibling;
}
@@ -991,6 +1005,11 @@ angular.mock.dump = function(object) {
* First we create the controller under test:
*
```js
+ // The module code
+ angular
+ .module('MyApp', [])
+ .controller('MyController', MyController);
+
// The controller code
function MyController($scope, $http) {
var authToken;
@@ -1020,6 +1039,9 @@ angular.mock.dump = function(object) {
describe('MyController', function() {
var $httpBackend, $rootScope, createController, authRequestHandler;
+ // Set up the module
+ beforeEach(module('MyApp'));
+
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
@@ -1368,7 +1390,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
$httpBackend.expect = function(method, url, data, headers) {
var expectation = new MockHttpExpectation(method, url, data, headers),
chain = {
- respond: function (status, data, headers, statusText) {
+ respond: function(status, data, headers, statusText) {
expectation.response = createResponse(status, data, headers, statusText);
return chain;
}
@@ -1618,7 +1640,9 @@ function MockHttpExpectation(method, url, data, headers) {
if (angular.isUndefined(data)) return true;
if (data && angular.isFunction(data.test)) return data.test(d);
if (data && angular.isFunction(data)) return data(d);
- if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d));
+ if (data && !angular.isString(data)) {
+ return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d));
+ }
return data == d;
};
@@ -1691,7 +1715,7 @@ function MockXhr() {
* that adds a "flush" and "verifyNoPendingTasks" methods.
*/
-angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function ($delegate, $browser) {
+angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) {
/**
* @ngdoc method
@@ -1745,12 +1769,12 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
rafFn.supported = $delegate.supported;
rafFn.flush = function() {
- if(queue.length === 0) {
+ if (queue.length === 0) {
throw new Error('No rAF callbacks present');
}
var length = queue.length;
- for(var i=0;i
+ *
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
+ */
+
+/*
+ * HTML Parser By Misko Hevery (misko@hevery.com)
+ * based on: HTML Parser By John Resig (ejohn.org)
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ *
+ * // Use like so:
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ */
+
+
+/**
+ * @ngdoc service
+ * @name $sanitize
+ * @kind function
+ *
+ * @description
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
+ * it into the returned string, however, since our parser is more strict than a typical browser
+ * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
+ * browser, won't make it through the sanitizer. The input may also contain SVG markup.
+ * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
+ * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
+ *
+ * @param {string} html HTML input.
+ * @returns {string} Sanitized HTML.
+ *
+ * @example
+
+
+
+
+ Snippet:
+
+
+
Directive
+
How
+
Source
+
Rendered
+
+
+
ng-bind-html
+
Automatically uses $sanitize
+
<div ng-bind-html="snippet"> </div>
+
+
+
+
ng-bind-html
+
Bypass $sanitize by explicitly trusting the dangerous value
-
-
- angular.module('anchorScrollExample', [])
- .controller('ScrollController', ['$scope', '$location', '$anchorScroll',
- function ($scope, $location, $anchorScroll) {
- $scope.gotoBottom = function() {
- // set the location.hash to the id of
- // the element you wish to scroll to.
- $location.hash('bottom');
-
- // call $anchorScroll()
- $anchorScroll();
- };
- }]);
-
-
- #scrollArea {
- height: 350px;
- overflow: auto;
- }
-
- #bottom {
- display: block;
- margin-top: 2000px;
- }
-
-
+ * Use `$anchorScrollProvider` to disable automatic scrolling whenever
+ * {@link ng.$location#hash $location.hash()} changes.
*/
function $AnchorScrollProvider() {
var autoScrollingEnabled = true;
+ /**
+ * @ngdoc method
+ * @name $anchorScrollProvider#disableAutoScrolling
+ *
+ * @description
+ * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to
+ * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.
+ * Use this method to disable automatic scrolling.
+ *
+ * If automatic scrolling is disabled, one must explicitly call
+ * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the
+ * current hash.
+ */
this.disableAutoScrolling = function() {
autoScrollingEnabled = false;
};
+ /**
+ * @ngdoc service
+ * @name $anchorScroll
+ * @kind function
+ * @requires $window
+ * @requires $location
+ * @requires $rootScope
+ *
+ * @description
+ * When called, it checks the current value of {@link ng.$location#hash $location.hash()} and
+ * scrolls to the related element, according to the rules specified in the
+ * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
+ *
+ * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
+ * match any anchor whenever it changes. This can be disabled by calling
+ * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}.
+ *
+ * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a
+ * vertical scroll-offset (either fixed or dynamic).
+ *
+ * @property {(number|function|jqLite)} yOffset
+ * If set, specifies a vertical scroll-offset. This is often useful when there are fixed
+ * positioned elements at the top of the page, such as navbars, headers etc.
+ *
+ * `yOffset` can be specified in various ways:
+ * - **number**: A fixed number of pixels to be used as offset.
+ * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return
+ * a number representing the offset (in pixels).
+ * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from
+ * the top of the page to the element's bottom will be used as offset.
+ * **Note**: The element will be taken into account only as long as its `position` is set to
+ * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust
+ * their height and/or positioning according to the viewport's size.
+ *
+ *
+ *
+ * In order for `yOffset` to work properly, scrolling should take place on the document's root and
+ * not some child element.
+ *
+
+
+ angular.module('anchorScrollOffsetExample', [])
+ .run(['$anchorScroll', function($anchorScroll) {
+ $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels
+ }])
+ .controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
+ function ($anchorScroll, $location, $scope) {
+ $scope.gotoAnchor = function(x) {
+ var newHash = 'anchor' + x;
+ if ($location.hash() !== newHash) {
+ // set the $location.hash to `newHash` and
+ // $anchorScroll will automatically scroll to it
+ $location.hash('anchor' + x);
+ } else {
+ // call $anchorScroll() explicitly,
+ // since $location.hash hasn't changed
+ $anchorScroll();
+ }
+ };
+ }
+ ]);
+
+
+ body {
+ padding-top: 50px;
+ }
+
+ .anchor {
+ border: 2px dashed DarkOrchid;
+ padding: 10px 10px 200px 10px;
+ }
+
+ .fixed-header {
+ background-color: rgba(0, 0, 0, 0.2);
+ height: 50px;
+ position: fixed;
+ top: 0; left: 0; right: 0;
+ }
+
+ .fixed-header > a {
+ display: inline-block;
+ margin: 5px 15px;
+ }
+
+
+ */
this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) {
var document = $window.document;
- // helper function to get first anchor from a NodeList
- // can't use filter.filter, as it accepts only instances of Array
- // and IE can't convert NodeList to an array using [].slice
- // TODO(vojta): use filter if we change it to accept lists as well
+ // Helper function to get first anchor from a NodeList
+ // (using `Array#some()` instead of `angular#forEach()` since it's more performant
+ // and working in all supported browsers.)
function getFirstAnchor(list) {
var result = null;
- forEach(list, function(element) {
- if (!result && nodeName_(element) === 'a') result = element;
+ Array.prototype.some.call(list, function(element) {
+ if (nodeName_(element) === 'a') {
+ result = element;
+ return true;
+ }
});
return result;
}
+ function getYOffset() {
+
+ var offset = scroll.yOffset;
+
+ if (isFunction(offset)) {
+ offset = offset();
+ } else if (isElement(offset)) {
+ var elem = offset[0];
+ var style = $window.getComputedStyle(elem);
+ if (style.position !== 'fixed') {
+ offset = 0;
+ } else {
+ offset = elem.getBoundingClientRect().bottom;
+ }
+ } else if (!isNumber(offset)) {
+ offset = 0;
+ }
+
+ return offset;
+ }
+
+ function scrollTo(elem) {
+ if (elem) {
+ elem.scrollIntoView();
+
+ var offset = getYOffset();
+
+ if (offset) {
+ // `offset` is the number of pixels we should scroll UP in order to align `elem` properly.
+ // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the
+ // top of the viewport.
+ //
+ // IF the number of pixels from the top of `elem` to the end of the page's content is less
+ // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some
+ // way down the page.
+ //
+ // This is often the case for elements near the bottom of the page.
+ //
+ // In such cases we do not need to scroll the whole `offset` up, just the difference between
+ // the top of the element and the offset, which is enough to align the top of `elem` at the
+ // desired position.
+ var elemTop = elem.getBoundingClientRect().top;
+ $window.scrollBy(0, elemTop - offset);
+ }
+ } else {
+ $window.scrollTo(0, 0);
+ }
+ }
+
function scroll() {
var hash = $location.hash(), elm;
// empty hash, scroll to the top of the page
- if (!hash) $window.scrollTo(0, 0);
+ if (!hash) scrollTo(null);
// element with given id
- else if ((elm = document.getElementById(hash))) elm.scrollIntoView();
+ else if ((elm = document.getElementById(hash))) scrollTo(elm);
// first anchor with given name :-D
- else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView();
+ else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm);
// no element and hash == 'top', scroll to the top of the page
- else if (hash === 'top') $window.scrollTo(0, 0);
+ else if (hash === 'top') scrollTo(null);
}
// does not scroll when user clicks on anchor link that is currently on
// (no url change, no $location.hash() change), browser native does scroll
if (autoScrollingEnabled) {
$rootScope.$watch(function autoScrollWatch() {return $location.hash();},
- function autoScrollWatchAction() {
- $rootScope.$evalAsync(scroll);
+ function autoScrollWatchAction(newVal, oldVal) {
+ // skip the initial scroll if $location.hash is empty
+ if (newVal === oldVal && newVal === '') return;
+
+ jqLiteDocumentLoaded(function() {
+ $rootScope.$evalAsync(scroll);
+ });
});
}
@@ -4317,15 +4503,64 @@ var $AnimateProvider = ['$provide', function($provide) {
* @return {RegExp} The current CSS className expression value. If null then there is no expression value
*/
this.classNameFilter = function(expression) {
- if(arguments.length === 1) {
+ if (arguments.length === 1) {
this.$$classNameFilter = (expression instanceof RegExp) ? expression : null;
}
return this.$$classNameFilter;
};
- this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) {
+ this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) {
var currentDefer;
+
+ function runAnimationPostDigest(fn) {
+ var cancelFn, defer = $$q.defer();
+ defer.promise.$$cancelFn = function ngAnimateMaybeCancel() {
+ cancelFn && cancelFn();
+ };
+
+ $rootScope.$$postDigest(function ngAnimatePostDigest() {
+ cancelFn = fn(function ngAnimateNotifyComplete() {
+ defer.resolve();
+ });
+ });
+
+ return defer.promise;
+ }
+
+ function resolveElementClasses(element, classes) {
+ var toAdd = [], toRemove = [];
+
+ var hasClasses = createMap();
+ forEach((element.attr('class') || '').split(/\s+/), function(className) {
+ hasClasses[className] = true;
+ });
+
+ forEach(classes, function(status, className) {
+ var hasClass = hasClasses[className];
+
+ // If the most recent class manipulation (via $animate) was to remove the class, and the
+ // element currently has the class, the class is scheduled for removal. Otherwise, if
+ // the most recent class manipulation (via $animate) was to add the class, and the
+ // element does not currently have the class, the class is scheduled to be added.
+ if (status === false && hasClass) {
+ toRemove.push(className);
+ } else if (status === true && !hasClass) {
+ toAdd.push(className);
+ }
+ });
+
+ return (toAdd.length + toRemove.length) > 0 &&
+ [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null];
+ }
+
+ function cachedClassManipulation(cache, classes, op) {
+ for (var i=0, ii = classes.length; i < ii; ++i) {
+ var className = classes[i];
+ cache[className] = op;
+ }
+ }
+
function asyncPromise() {
// only serve one instance of a promise in order to save CPU cycles
if (!currentDefer) {
@@ -4338,6 +4573,13 @@ var $AnimateProvider = ['$provide', function($provide) {
return currentDefer.promise;
}
+ function applyStyles(element, options) {
+ if (angular.isObject(options)) {
+ var styles = extend(options.from || {}, options.to || {});
+ element.css(styles);
+ }
+ }
+
/**
*
* @ngdoc service
@@ -4356,6 +4598,10 @@ var $AnimateProvider = ['$provide', function($provide) {
* page}.
*/
return {
+ animate: function(element, from, to) {
+ applyStyles(element, { from: from, to: to });
+ return asyncPromise();
+ },
/**
*
@@ -4370,9 +4616,11 @@ var $AnimateProvider = ['$provide', function($provide) {
* a child (if the after element is not present)
* @param {DOMElement} after the sibling element which will append the element
* after itself
+ * @param {object=} options an optional collection of styles that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- enter : function(element, parent, after) {
+ enter: function(element, parent, after, options) {
+ applyStyles(element, options);
after ? after.after(element)
: parent.prepend(element);
return asyncPromise();
@@ -4386,9 +4634,10 @@ var $AnimateProvider = ['$provide', function($provide) {
* @description Removes the element from the DOM. When the function is called a promise
* is returned that will be resolved at a later time.
* @param {DOMElement} element the element which will be removed from the DOM
+ * @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- leave : function(element) {
+ leave: function(element, options) {
element.remove();
return asyncPromise();
},
@@ -4408,12 +4657,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* inserted into (if the after element is not present)
* @param {DOMElement} after the sibling element where the element will be
* positioned next to
+ * @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- move : function(element, parent, after) {
+ move: function(element, parent, after, options) {
// Do not remove element before insert. Removing will cause data associated with the
// element to be dropped. Insert will implicitly do the remove.
- return this.enter(element, parent, after);
+ return this.enter(element, parent, after, options);
},
/**
@@ -4426,15 +4676,22 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} element the element which will have the className value
* added to it
* @param {string} className the CSS class which will be added to the element
+ * @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- addClass : function(element, className) {
+ addClass: function(element, className, options) {
+ return this.setClass(element, className, [], options);
+ },
+
+ $$addClassImmediately: function(element, className, options) {
+ element = jqLite(element);
className = !isString(className)
? (isArray(className) ? className.join(' ') : '')
: className;
- forEach(element, function (element) {
+ forEach(element, function(element) {
jqLiteAddClass(element, className);
});
+ applyStyles(element, options);
return asyncPromise();
},
@@ -4448,15 +4705,22 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} element the element which will have the className value
* removed from it
* @param {string} className the CSS class which will be removed from the element
+ * @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- removeClass : function(element, className) {
+ removeClass: function(element, className, options) {
+ return this.setClass(element, [], className, options);
+ },
+
+ $$removeClassImmediately: function(element, className, options) {
+ element = jqLite(element);
className = !isString(className)
? (isArray(className) ? className.join(' ') : '')
: className;
- forEach(element, function (element) {
+ forEach(element, function(element) {
jqLiteRemoveClass(element, className);
});
+ applyStyles(element, options);
return asyncPromise();
},
@@ -4471,21 +4735,70 @@ var $AnimateProvider = ['$provide', function($provide) {
* removed from it
* @param {string} add the CSS classes which will be added to the element
* @param {string} remove the CSS class which will be removed from the element
+ * @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- setClass : function(element, add, remove) {
- this.addClass(element, add);
- this.removeClass(element, remove);
+ setClass: function(element, add, remove, options) {
+ var self = this;
+ var STORAGE_KEY = '$$animateClasses';
+ var createdCache = false;
+ element = jqLite(element);
+
+ var cache = element.data(STORAGE_KEY);
+ if (!cache) {
+ cache = {
+ classes: {},
+ options: options
+ };
+ createdCache = true;
+ } else if (options && cache.options) {
+ cache.options = angular.extend(cache.options || {}, options);
+ }
+
+ var classes = cache.classes;
+
+ add = isArray(add) ? add : add.split(' ');
+ remove = isArray(remove) ? remove : remove.split(' ');
+ cachedClassManipulation(classes, add, true);
+ cachedClassManipulation(classes, remove, false);
+
+ if (createdCache) {
+ cache.promise = runAnimationPostDigest(function(done) {
+ var cache = element.data(STORAGE_KEY);
+ element.removeData(STORAGE_KEY);
+
+ // in the event that the element is removed before postDigest
+ // is run then the cache will be undefined and there will be
+ // no need anymore to add or remove and of the element classes
+ if (cache) {
+ var classes = resolveElementClasses(element, cache.classes);
+ if (classes) {
+ self.$$setClassImmediately(element, classes[0], classes[1], cache.options);
+ }
+ }
+
+ done();
+ });
+ element.data(STORAGE_KEY, cache);
+ }
+
+ return cache.promise;
+ },
+
+ $$setClassImmediately: function(element, add, remove, options) {
+ add && this.$$addClassImmediately(element, add);
+ remove && this.$$removeClassImmediately(element, remove);
+ applyStyles(element, options);
return asyncPromise();
},
- enabled : noop,
- cancel : noop
+ enabled: noop,
+ cancel: noop
};
}];
}];
-function $$AsyncCallbackProvider(){
+function $$AsyncCallbackProvider() {
this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) {
return $$rAF.supported
? function(fn) { return $$rAF(fn); }
@@ -4495,6 +4808,8 @@ function $$AsyncCallbackProvider(){
}];
}
+/* global stripHash: true */
+
/**
* ! This is a private undocumented service !
*
@@ -4513,8 +4828,7 @@ function $$AsyncCallbackProvider(){
/**
* @param {object} window The global window object.
* @param {object} document jQuery wrapped document.
- * @param {function()} XHR XMLHttpRequest constructor.
- * @param {object} $log console.log or an object with the same interface.
+ * @param {object} $log window.console or an object with the same interface.
* @param {object} $sniffer $sniffer service
*/
function Browser(window, document, $log, $sniffer) {
@@ -4545,7 +4859,7 @@ function Browser(window, document, $log, $sniffer) {
} finally {
outstandingRequestCount--;
if (outstandingRequestCount === 0) {
- while(outstandingRequestCallbacks.length) {
+ while (outstandingRequestCallbacks.length) {
try {
outstandingRequestCallbacks.pop()();
} catch (e) {
@@ -4566,7 +4880,7 @@ function Browser(window, document, $log, $sniffer) {
// force browser to execute all pollFns - this is needed so that cookies and other pollers fire
// at some deterministic time in respect to the test runner's actions. Leaving things up to the
// regular poller would result in flaky tests.
- forEach(pollFns, function(pollFn){ pollFn(); });
+ forEach(pollFns, function(pollFn) { pollFn(); });
if (outstandingRequestCount === 0) {
callback();
@@ -4608,7 +4922,7 @@ function Browser(window, document, $log, $sniffer) {
*/
function startPoller(interval, setTimeout) {
(function check() {
- forEach(pollFns, function(pollFn){ pollFn(); });
+ forEach(pollFns, function(pollFn) { pollFn(); });
pollTimeout = setTimeout(check, interval);
})();
}
@@ -4617,9 +4931,13 @@ function Browser(window, document, $log, $sniffer) {
// URL API
//////////////////////////////////////////////////////////////
- var lastBrowserUrl = location.href,
+ var cachedState, lastHistoryState,
+ lastBrowserUrl = location.href,
baseElement = document.find('base'),
- newLocation = null;
+ reloadLocation = null;
+
+ cacheState();
+ lastHistoryState = cachedState;
/**
* @name $browser#url
@@ -4638,26 +4956,47 @@ function Browser(window, document, $log, $sniffer) {
* {@link ng.$location $location service} to change url.
*
* @param {string} url New url (when used as setter)
- * @param {boolean=} replace Should new url replace current history record ?
+ * @param {boolean=} replace Should new url replace current history record?
+ * @param {object=} state object to use with pushState/replaceState
*/
- self.url = function(url, replace) {
+ self.url = function(url, replace, state) {
+ // In modern browsers `history.state` is `null` by default; treating it separately
+ // from `undefined` would cause `$browser.url('/foo')` to change `history.state`
+ // to undefined via `pushState`. Instead, let's change `undefined` to `null` here.
+ if (isUndefined(state)) {
+ state = null;
+ }
+
// Android Browser BFCache causes location, history reference to become stale.
if (location !== window.location) location = window.location;
if (history !== window.history) history = window.history;
// setter
if (url) {
- if (lastBrowserUrl == url) return;
+ var sameState = lastHistoryState === state;
+
+ // Don't change anything if previous and current URLs and states match. This also prevents
+ // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
+ // See https://github.com/angular/angular.js/commit/ffb2701
+ if (lastBrowserUrl === url && (!$sniffer.history || sameState)) {
+ return self;
+ }
+ var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
lastBrowserUrl = url;
- if ($sniffer.history) {
- if (replace) history.replaceState(null, '', url);
- else {
- history.pushState(null, '', url);
- // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462
- baseElement.attr('href', baseElement.attr('href'));
- }
+ lastHistoryState = state;
+ // Don't use history API if only the hash changed
+ // due to a bug in IE10/IE11 which leads
+ // to not firing a `hashchange` nor `popstate` event
+ // in some cases (see #9143).
+ if ($sniffer.history && (!sameBase || !sameState)) {
+ history[replace ? 'replaceState' : 'pushState'](state, '', url);
+ cacheState();
+ // Do the assignment again so that those two variables are referentially identical.
+ lastHistoryState = cachedState;
} else {
- newLocation = url;
+ if (!sameBase) {
+ reloadLocation = url;
+ }
if (replace) {
location.replace(url);
} else {
@@ -4667,23 +5006,58 @@ function Browser(window, document, $log, $sniffer) {
return self;
// getter
} else {
- // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href
- // methods not updating location.href synchronously.
+ // - reloadLocation is needed as browsers don't allow to read out
+ // the new location.href if a reload happened.
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
- return newLocation || location.href.replace(/%27/g,"'");
+ return reloadLocation || location.href.replace(/%27/g,"'");
}
};
+ /**
+ * @name $browser#state
+ *
+ * @description
+ * This method is a getter.
+ *
+ * Return history.state or null if history.state is undefined.
+ *
+ * @returns {object} state
+ */
+ self.state = function() {
+ return cachedState;
+ };
+
var urlChangeListeners = [],
urlChangeInit = false;
+ function cacheStateAndFireUrlChange() {
+ cacheState();
+ fireUrlChange();
+ }
+
+ // This variable should be used *only* inside the cacheState function.
+ var lastCachedState = null;
+ function cacheState() {
+ // This should be the only place in $browser where `history.state` is read.
+ cachedState = window.history.state;
+ cachedState = isUndefined(cachedState) ? null : cachedState;
+
+ // Prevent callbacks fo fire twice if both hashchange & popstate were fired.
+ if (equals(cachedState, lastCachedState)) {
+ cachedState = lastCachedState;
+ }
+ lastCachedState = cachedState;
+ }
+
function fireUrlChange() {
- newLocation = null;
- if (lastBrowserUrl == self.url()) return;
+ if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
+ return;
+ }
lastBrowserUrl = self.url();
+ lastHistoryState = cachedState;
forEach(urlChangeListeners, function(listener) {
- listener(self.url());
+ listener(self.url(), cachedState);
});
}
@@ -4716,11 +5090,9 @@ function Browser(window, document, $log, $sniffer) {
// changed by push/replaceState
// html5 history api - popstate event
- if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange);
+ if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange);
// hashchange event
- if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange);
- // polling
- else self.addPollFn(fireUrlChange);
+ jqLite(window).on('hashchange', cacheStateAndFireUrlChange);
urlChangeInit = true;
}
@@ -4761,6 +5133,14 @@ function Browser(window, document, $log, $sniffer) {
var lastCookieString = '';
var cookiePath = self.baseHref();
+ function safeDecodeURIComponent(str) {
+ try {
+ return decodeURIComponent(str);
+ } catch (e) {
+ return str;
+ }
+ }
+
/**
* @name $browser#cookies
*
@@ -4798,8 +5178,8 @@ function Browser(window, document, $log, $sniffer) {
// - 20 cookies per unique domain
// - 4096 bytes per cookie
if (cookieLength > 4096) {
- $log.warn("Cookie '"+ name +
- "' possibly not set or overflowed because it was too large ("+
+ $log.warn("Cookie '" + name +
+ "' possibly not set or overflowed because it was too large (" +
cookieLength + " > 4096 bytes)!");
}
}
@@ -4814,12 +5194,12 @@ function Browser(window, document, $log, $sniffer) {
cookie = cookieArray[i];
index = cookie.indexOf('=');
if (index > 0) { //ignore nameless cookies
- name = decodeURIComponent(cookie.substring(0, index));
+ name = safeDecodeURIComponent(cookie.substring(0, index));
// the first value that is seen for a cookie is the most
// specific one. values for the same cookie name that
// follow are for less specific paths.
if (lastCookies[name] === undefined) {
- lastCookies[name] = decodeURIComponent(cookie.substring(index + 1));
+ lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1));
}
}
}
@@ -4877,9 +5257,9 @@ function Browser(window, document, $log, $sniffer) {
}
-function $BrowserProvider(){
+function $BrowserProvider() {
this.$get = ['$window', '$log', '$sniffer', '$document',
- function( $window, $log, $sniffer, $document){
+ function($window, $log, $sniffer, $document) {
return new Browser($window, $document, $log, $sniffer);
}];
}
@@ -5253,7 +5633,8 @@ function $CacheFactoryProvider() {
* ```
*
* **Note:** the `script` tag containing the template does not need to be included in the `head` of
- * the document, but it must be below the `ng-app` definition.
+ * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE,
+ * element with ng-app attribute), otherwise the template will be ignored.
*
* Adding via the $templateCache service:
*
@@ -5344,6 +5725,7 @@ function $TemplateCacheProvider() {
* // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
* transclude: false,
* restrict: 'A',
+ * templateNamespace: 'html',
* scope: false,
* controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
* controllerAs: 'stringAlias',
@@ -5398,8 +5780,8 @@ function $TemplateCacheProvider() {
* When this property is set to true, the HTML compiler will collect DOM nodes between
* nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
* together as the directive elements. It is recomended that this feature be used on directives
- * which are not strictly behavioural (such as {@link api/ng.directive:ngClick ngClick}), and which
- * do not manipulate or replace child nodes (such as {@link api/ng.directive:ngInclude ngInclude}).
+ * which are not strictly behavioural (such as {@link ngClick}), and which
+ * do not manipulate or replace child nodes (such as {@link ngInclude}).
*
* #### `priority`
* When there are multiple directives defined on a single DOM element, sometimes it
@@ -5412,7 +5794,8 @@ function $TemplateCacheProvider() {
* #### `terminal`
* If set to true then the current `priority` will be the last set of directives
* which will execute (any directives at the current priority will still execute
- * as the order of execution on same `priority` is undefined).
+ * as the order of execution on same `priority` is undefined). Note that expressions
+ * and other directives used in the directive's template will also be excluded from execution.
*
* #### `scope`
* **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the
@@ -5445,7 +5828,9 @@ function $TemplateCacheProvider() {
* value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
* in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent
* scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You
- * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional.
+ * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If
+ * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use
+ * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional).
*
* * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
* If no `attr` name is specified then the attribute name is assumed to be the same as the
@@ -5459,7 +5844,7 @@ function $TemplateCacheProvider() {
*
*
* #### `bindToController`
- * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController` will
+ * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will
* allow a component to have its properties bound to the controller, rather than to scope. When the controller
* is instantiated, the initial values of the isolate scope bindings are already available.
*
@@ -5495,8 +5880,11 @@ function $TemplateCacheProvider() {
* * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
* * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
* * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found.
+ * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found.
* * `?^` - Attempt to locate the required controller by searching the element and its parents or pass
* `null` to the `link` fn if not found.
+ * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass
+ * `null` to the `link` fn if not found.
*
*
* #### `controllerAs`
@@ -5556,7 +5944,7 @@ function $TemplateCacheProvider() {
* You can specify `templateUrl` as a string representing the URL or as a function which takes two
* arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns
* a string value representing the url. In either case, the template URL is passed through {@link
- * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}.
+ * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}.
*
*
* #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0)
@@ -5566,7 +5954,7 @@ function $TemplateCacheProvider() {
* * `false` - the template will replace the contents of the directive's element.
*
* The replacement process migrates all of the attributes / classes from the old element to the new
- * one. See the {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive
+ * one. See the {@link guide/directive#template-expanding-directive
* Directives Guide} for an example.
*
* There are very few scenarios where element replacement is required for the application function,
@@ -5574,22 +5962,18 @@ function $TemplateCacheProvider() {
* (because SVG doesn't work with custom elements in the DOM tree).
*
* #### `transclude`
- * compile the content of the element and make it available to the directive.
- * Typically used with {@link ng.directive:ngTransclude
- * ngTransclude}. The advantage of transclusion is that the linking function receives a
- * transclusion function which is pre-bound to the correct scope. In a typical setup the widget
- * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate`
- * scope. This makes it possible for the widget to have private state, and the transclusion to
- * be bound to the parent (pre-`isolate`) scope.
+ * Extract the contents of the element where the directive appears and make it available to the directive.
+ * The contents are compiled and provided to the directive as a **transclusion function**. See the
+ * {@link $compile#transclusion Transclusion} section below.
*
- * * `true` - transclude the content of the directive.
- * * `'element'` - transclude the whole element including any directives defined at lower priority.
+ * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
+ * directive's element or the entire element:
+ *
+ * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
+ * * `'element'` - transclude the whole of the directive's element including any directives on this
+ * element that defined at a lower priority than this directive. When used, the `template`
+ * property is ignored.
*
- *
- * **Note:** When testing an element transclude directive you must not place the directive at the root of the
- * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives
- * Testing Transclusion Directives}.
- *
*
* #### `compile`
*
@@ -5687,7 +6071,121 @@ function $TemplateCacheProvider() {
* It is safe to do DOM transformation in the post-linking function on elements that are not waiting
* for their async templates to be resolved.
*
- *
+ *
+ * ### Transclusion
+ *
+ * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and
+ * copying them to another part of the DOM, while maintaining their connection to the original AngularJS
+ * scope from where they were taken.
+ *
+ * Transclusion is used (often with {@link ngTransclude}) to insert the
+ * original contents of a directive's element into a specified place in the template of the directive.
+ * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded
+ * content has access to the properties on the scope from which it was taken, even if the directive
+ * has isolated scope.
+ * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}.
+ *
+ * This makes it possible for the widget to have private state for its template, while the transcluded
+ * content has access to its originating scope.
+ *
+ *
+ * **Note:** When testing an element transclude directive you must not place the directive at the root of the
+ * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives
+ * Testing Transclusion Directives}.
+ *
+ *
+ * #### Transclusion Functions
+ *
+ * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion
+ * function** to the directive's `link` function and `controller`. This transclusion function is a special
+ * **linking function** that will return the compiled contents linked to a new transclusion scope.
+ *
+ *
+ * If you are just using {@link ngTransclude} then you don't need to worry about this function, since
+ * ngTransclude will deal with it for us.
+ *
+ *
+ * If you want to manually control the insertion and removal of the transcluded content in your directive
+ * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery
+ * object that contains the compiled DOM, which is linked to the correct transclusion scope.
+ *
+ * When you call a transclusion function you can pass in a **clone attach function**. This function accepts
+ * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded
+ * content and the `scope` is the newly created transclusion scope, to which the clone is bound.
+ *
+ *
+ * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function
+ * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope.
+ *
+ *
+ * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone
+ * attach function**:
+ *
+ * ```js
+ * var transcludedContent, transclusionScope;
+ *
+ * $transclude(function(clone, scope) {
+ * element.append(clone);
+ * transcludedContent = clone;
+ * transclusionScope = scope;
+ * });
+ * ```
+ *
+ * Later, if you want to remove the transcluded content from your DOM then you should also destroy the
+ * associated transclusion scope:
+ *
+ * ```js
+ * transcludedContent.remove();
+ * transclusionScope.$destroy();
+ * ```
+ *
+ *
+ * **Best Practice**: if you intend to add and remove transcluded content manually in your directive
+ * (by calling the transclude function to get the DOM and and calling `element.remove()` to remove it),
+ * then you are also responsible for calling `$destroy` on the transclusion scope.
+ *
+ *
+ * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat}
+ * automatically destroy their transluded clones as necessary so you do not need to worry about this if
+ * you are simply using {@link ngTransclude} to inject the transclusion into your directive.
+ *
+ *
+ * #### Transclusion Scopes
+ *
+ * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion
+ * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed
+ * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it
+ * was taken.
+ *
+ * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look
+ * like this:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * The `$parent` scope hierarchy will look like this:
+ *
+ * ```
+ * - $rootScope
+ * - isolate
+ * - transclusion
+ * ```
+ *
+ * but the scopes will inherit prototypically from different scopes to their `$parent`.
+ *
+ * ```
+ * - $rootScope
+ * - transclusion
+ * - isolate
+ * ```
+ *
+ *
* ### Attributes
*
* The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the
@@ -5725,7 +6223,7 @@ function $TemplateCacheProvider() {
* }
* ```
*
- * Below is an example using `$compileProvider`.
+ * ## Example
*
*
* **Note**: Typically directives are registered with `module.directive`. The example below is
@@ -5788,10 +6286,17 @@ function $TemplateCacheProvider() {
*
*
* @param {string|DOMElement} element Element or HTML string to compile into a template function.
- * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives.
+ * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED.
+ *
+ *
+ * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it
+ * e.g. will not use the right outer scope. Please pass the transclude function as a
+ * `parentBoundTranscludeFn` to the link function instead.
+ *
+ *
* @param {number} maxPriority only apply directives lower than given priority (Only effects the
* root element(s), not their children)
- * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template
+ * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template
* (a DOM element/tree) to a scope. Where:
*
* * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
@@ -5803,6 +6308,19 @@ function $TemplateCacheProvider() {
* * `clonedElement` - is a clone of the original `element` passed into the compiler.
* * `scope` - is the current scope with which the linking function is working with.
*
+ * * `options` - An optional object hash with linking options. If `options` is provided, then the following
+ * keys may be used to control linking behavior:
+ *
+ * * `parentBoundTranscludeFn` - the transclude function made available to
+ * directives; if given, it will be passed through to the link functions of
+ * directives found in `element` during compilation.
+ * * `transcludeControllers` - an object hash with keys that map controller names
+ * to controller instances; if given, it will make the controllers
+ * available to directives.
+ * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add
+ * the cloned elements; only needed for transcludes that are allowed to contain non html
+ * elements (e.g. SVG elements). See also the directive.controller property.
+ *
* Calling the linking function returns the element of the template. It is either the original
* element passed in, or the clone of the element if the `cloneAttachFn` is provided.
*
@@ -5848,9 +6366,10 @@ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider'];
function $CompileProvider($provide, $$sanitizeUriProvider) {
var hasDirectives = {},
Suffix = 'Directive',
- COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/,
- CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/,
- ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset');
+ COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/,
+ CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/,
+ ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'),
+ REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/;
// Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes
// The assumption is that future DOM event attribute names will begin with
@@ -5858,7 +6377,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/;
function parseIsolateBindings(scope, directiveName) {
- var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
+ var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/;
var bindings = {};
@@ -5873,9 +6392,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
bindings[scopeName] = {
- attrName: match[3] || scopeName,
- mode: match[1],
- optional: match[2] === '?'
+ mode: match[1][0],
+ collection: match[2] === '*',
+ optional: match[3] === '?',
+ attrName: match[4] || scopeName
};
});
@@ -6021,7 +6541,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*/
var debugInfoEnabled = true;
this.debugInfoEnabled = function(enabled) {
- if(isDefined(enabled)) {
+ if (isDefined(enabled)) {
debugInfoEnabled = enabled;
return this;
}
@@ -6065,8 +6585,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*
* @param {string} classVal The className value that will be added to the element
*/
- $addClass : function(classVal) {
- if(classVal && classVal.length > 0) {
+ $addClass: function(classVal) {
+ if (classVal && classVal.length > 0) {
$animate.addClass(this.$$element, classVal);
}
},
@@ -6082,8 +6602,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*
* @param {string} classVal The className value that will be removed from the element
*/
- $removeClass : function(classVal) {
- if(classVal && classVal.length > 0) {
+ $removeClass: function(classVal) {
+ if (classVal && classVal.length > 0) {
$animate.removeClass(this.$$element, classVal);
}
},
@@ -6100,7 +6620,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string} newClasses The current CSS className value
* @param {string} oldClasses The former CSS className value
*/
- $updateClass : function(newClasses, oldClasses) {
+ $updateClass: function(newClasses, oldClasses) {
var toAdd = tokenDifference(newClasses, oldClasses);
if (toAdd && toAdd.length) {
$animate.addClass(this.$$element, toAdd);
@@ -6130,13 +6650,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
booleanKey = getBooleanAttrName(node, key),
aliasedKey = getAliasedAttrName(node, key),
observer = key,
- normalizedVal,
nodeName;
if (booleanKey) {
this.$$element.prop(key, value);
attrName = booleanKey;
- } else if(aliasedKey) {
+ } else if (aliasedKey) {
this[aliasedKey] = value;
observer = aliasedKey;
}
@@ -6155,10 +6674,44 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
- // sanitize a[href] and img[src] values
if ((nodeName === 'a' && key === 'href') ||
(nodeName === 'img' && key === 'src')) {
+ // sanitize a[href] and img[src] values
this[key] = value = $$sanitizeUri(value, key === 'src');
+ } else if (nodeName === 'img' && key === 'srcset') {
+ // sanitize img[srcset] values
+ var result = "";
+
+ // first check if there are spaces because it's not the same pattern
+ var trimmedSrcset = trim(value);
+ // ( 999x ,| 999w ,| ,|, )
+ var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
+ var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
+
+ // split srcset into tuple of uri and descriptor except for the last item
+ var rawUris = trimmedSrcset.split(pattern);
+
+ // for each tuples
+ var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
+ for (var i = 0; i < nbrUrisWith2parts; i++) {
+ var innerIdx = i * 2;
+ // sanitize the uri
+ result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
+ // add the descriptor
+ result += (" " + trim(rawUris[innerIdx + 1]));
+ }
+
+ // split the last item into uri and descriptor
+ var lastTuple = trim(rawUris[i * 2]).split(/\s/);
+
+ // sanitize the last uri
+ result += $$sanitizeUri(trim(lastTuple[0]), true);
+
+ // and add the last descriptor if any
+ if (lastTuple.length === 2) {
+ result += (" " + trim(lastTuple[1]));
+ }
+ this[key] = value = result;
}
if (writeAttr !== false) {
@@ -6196,17 +6749,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string} key Normalized key. (ie ngAttribute) .
* @param {function(interpolatedValue)} fn Function that will be called whenever
the interpolated value of the attribute changes.
- * See the {@link guide/directive#Attributes Directives} guide for more info.
+ * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info.
* @returns {function()} Returns a deregistration function for this observer.
*/
$observe: function(key, fn) {
var attrs = this,
- $$observers = (attrs.$$observers || (attrs.$$observers = {})),
+ $$observers = (attrs.$$observers || (attrs.$$observers = createMap())),
listeners = ($$observers[key] || ($$observers[key] = []));
listeners.push(fn);
$rootScope.$evalAsync(function() {
- if (!listeners.$$inter) {
+ if (!listeners.$$inter && attrs.hasOwnProperty(key)) {
// no one registered attribute interpolation function, so lets call it manually
fn(attrs[key]);
}
@@ -6222,7 +6775,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function safeAddClass($element, className) {
try {
$element.addClass(className);
- } catch(e) {
+ } catch (e) {
// ignore, since it means that we are trying to set class on
// SVG element, where class name is read-only.
}
@@ -6276,8 +6829,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
// We can not compile top level text elements since text nodes can be merged and we will
// not be able to attach scope data to them, so we will wrap them in
- forEach($compileNodes, function(node, index){
- if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
+ forEach($compileNodes, function(node, index) {
+ if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) {
$compileNodes[index] = jqLite(node).wrap('').parent()[0];
}
});
@@ -6286,27 +6839,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
maxPriority, ignoreDirective, previousCompileContext);
compile.$$addScopeClass($compileNodes);
var namespace = null;
- var namespaceAdaptedCompileNodes = $compileNodes;
- var lastCompileNode;
- return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){
+ return function publicLinkFn(scope, cloneConnectFn, options) {
assertArg(scope, 'scope');
+
+ options = options || {};
+ var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
+ transcludeControllers = options.transcludeControllers,
+ futureParentElement = options.futureParentElement;
+
+ // When `parentBoundTranscludeFn` is passed, it is a
+ // `controllersBoundTransclude` function (it was previously passed
+ // as `transclude` to directive.link) so we must unwrap it to get
+ // its `boundTranscludeFn`
+ if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) {
+ parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude;
+ }
+
if (!namespace) {
namespace = detectNamespaceForChildElements(futureParentElement);
}
- if (namespace !== 'html' && $compileNodes[0] !== lastCompileNode) {
- namespaceAdaptedCompileNodes = jqLite(
+ var $linkNode;
+ if (namespace !== 'html') {
+ // When using a directive with replace:true and templateUrl the $compileNodes
+ // (or a child element inside of them)
+ // might change, so we need to recreate the namespace adapted compileNodes
+ // for call to the link function.
+ // Note: This will already clone the nodes...
+ $linkNode = jqLite(
wrapTemplate(namespace, jqLite('
').append($compileNodes).html())
);
+ } else if (cloneConnectFn) {
+ // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
+ // and sometimes changes the structure of the DOM.
+ $linkNode = JQLitePrototype.clone.call($compileNodes);
+ } else {
+ $linkNode = $compileNodes;
}
- // When using a directive with replace:true and templateUrl the $compileNodes
- // might change, so we need to recreate the namespace adapted compileNodes.
- lastCompileNode = $compileNodes[0];
-
- // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
- // and sometimes changes the structure of the DOM.
- var $linkNode = cloneConnectFn
- ? JQLitePrototype.clone.call(namespaceAdaptedCompileNodes) // IMPORTANT!!!
- : namespaceAdaptedCompileNodes;
if (transcludeControllers) {
for (var controllerName in transcludeControllers) {
@@ -6328,7 +6896,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!node) {
return 'html';
} else {
- return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg': 'html';
+ return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html';
}
}
@@ -6410,7 +6978,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
stableNodeList = nodeList;
}
- for(i = 0, ii = linkFns.length; i < ii;) {
+ for (i = 0, ii = linkFns.length; i < ii;) {
node = stableNodeList[linkFns[i++]];
nodeLinkFn = linkFns[i++];
childLinkFn = linkFns[i++];
@@ -6423,7 +6991,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childScope = scope;
}
- if ( nodeLinkFn.transcludeOnThisElement ) {
+ if (nodeLinkFn.transcludeOnThisElement) {
childBoundTranscludeFn = createBoundTranscludeFn(
scope, nodeLinkFn.transclude, parentBoundTranscludeFn,
nodeLinkFn.elementTranscludeOnThisElement);
@@ -6449,20 +7017,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) {
- var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement) {
- var scopeCreated = false;
+ var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
if (!transcludedScope) {
- transcludedScope = scope.$new();
+ transcludedScope = scope.$new(false, containingScope);
transcludedScope.$$transcluded = true;
- scopeCreated = true;
}
- var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement);
- if (scopeCreated && !elementTransclusion) {
- clone.on('$destroy', function() { transcludedScope.$destroy(); });
- }
- return clone;
+ return transcludeFn(transcludedScope, cloneFn, {
+ parentBoundTranscludeFn: previousBoundTranscludeFn,
+ transcludeControllers: controllers,
+ futureParentElement: futureParentElement
+ });
};
return boundTranscludeFn;
@@ -6484,8 +7050,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
match,
className;
- switch(nodeType) {
- case 1: /* Element */
+ switch (nodeType) {
+ case NODE_TYPE_ELEMENT: /* Element */
// use the node name:
addDirective(directives,
directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective);
@@ -6497,37 +7063,35 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var attrEndName = false;
attr = nAttrs[j];
- if (!msie || msie >= 8 || attr.specified) {
- name = attr.name;
- value = trim(attr.value);
+ name = attr.name;
+ value = trim(attr.value);
- // support ngAttr attribute binding
- ngAttrName = directiveNormalize(name);
- if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
- name = snake_case(ngAttrName.substr(6), '-');
- }
-
- var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
- if (directiveIsMultiElement(directiveNName)) {
- if (ngAttrName === directiveNName + 'Start') {
- attrStartName = name;
- attrEndName = name.substr(0, name.length - 5) + 'end';
- name = name.substr(0, name.length - 6);
- }
- }
-
- nName = directiveNormalize(name.toLowerCase());
- attrsMap[nName] = name;
- if (isNgAttr || !attrs.hasOwnProperty(nName)) {
- attrs[nName] = value;
- if (getBooleanAttrName(node, nName)) {
- attrs[nName] = true; // presence means true
- }
- }
- addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
- addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
- attrEndName);
+ // support ngAttr attribute binding
+ ngAttrName = directiveNormalize(name);
+ if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
+ name = snake_case(ngAttrName.substr(6), '-');
}
+
+ var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
+ if (directiveIsMultiElement(directiveNName)) {
+ if (ngAttrName === directiveNName + 'Start') {
+ attrStartName = name;
+ attrEndName = name.substr(0, name.length - 5) + 'end';
+ name = name.substr(0, name.length - 6);
+ }
+ }
+
+ nName = directiveNormalize(name.toLowerCase());
+ attrsMap[nName] = name;
+ if (isNgAttr || !attrs.hasOwnProperty(nName)) {
+ attrs[nName] = value;
+ if (getBooleanAttrName(node, nName)) {
+ attrs[nName] = true; // presence means true
+ }
+ }
+ addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
+ addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
+ attrEndName);
}
// use class as directive
@@ -6542,10 +7106,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
break;
- case 3: /* Text Node */
+ case NODE_TYPE_TEXT: /* Text Node */
addTextInterpolateDirective(directives, node.nodeValue);
break;
- case 8: /* Comment */
+ case NODE_TYPE_COMMENT: /* Comment */
try {
match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
if (match) {
@@ -6578,14 +7142,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var nodes = [];
var depth = 0;
if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
- var startNode = node;
do {
if (!node) {
throw $compileMinErr('uterdir',
"Unterminated attribute, found '{0}' but no matching '{1}' found.",
attrStart, attrEnd);
}
- if (node.nodeType == 1 /** Element **/) {
+ if (node.nodeType == NODE_TYPE_ELEMENT) {
if (node.hasAttribute(attrStart)) depth++;
if (node.hasAttribute(attrEnd)) depth--;
}
@@ -6662,7 +7225,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directiveValue;
// executes all directives on the current element
- for(var i = 0, ii = directives.length; i < ii; i++) {
+ for (var i = 0, ii = directives.length; i < ii; i++) {
directive = directives[i];
var attrStart = directive.$$start;
var attrEnd = directive.$$end;
@@ -6764,11 +7327,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (jqLiteIsTextNode(directiveValue)) {
$template = [];
} else {
- $template = jqLite(wrapTemplate(directive.templateNamespace, trim(directiveValue)));
+ $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue)));
}
compileNode = $template[0];
- if ($template.length != 1 || compileNode.nodeType !== 1) {
+ if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
throw $compileMinErr('tplrt',
"Template for directive '{0}' must have exactly one root element. {1}",
directiveName, '');
@@ -6872,14 +7435,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function getControllers(directiveName, require, $element, elementControllers) {
var value, retrievalMethod = 'data', optional = false;
+ var $searchElement = $element;
+ var match;
if (isString(require)) {
- while((value = require.charAt(0)) == '^' || value == '?') {
- require = require.substr(1);
- if (value == '^') {
- retrievalMethod = 'inheritedData';
- }
- optional = optional || value == '?';
+ match = require.match(REQUIRE_PREFIX_REGEXP);
+ require = require.substring(match[0].length);
+
+ if (match[3]) {
+ if (match[1]) match[3] = null;
+ else match[1] = match[3];
}
+ if (match[1] === '^') {
+ retrievalMethod = 'inheritedData';
+ } else if (match[1] === '^^') {
+ retrievalMethod = 'inheritedData';
+ $searchElement = $element.parent();
+ }
+ if (match[2] === '?') {
+ optional = true;
+ }
+
value = null;
if (elementControllers && retrievalMethod === 'data') {
@@ -6887,14 +7462,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
value = value.instance;
}
}
- value = value || $element[retrievalMethod]('$' + require + 'Controller');
+ value = value || $searchElement[retrievalMethod]('$' + require + 'Controller');
if (!value && !optional) {
throw $compileMinErr('ctreq',
"Controller '{0}', required by directive '{1}', can't be found!",
require, directiveName);
}
- return value;
+ return value || null;
} else if (isArray(require)) {
value = [];
forEach(require, function(require) {
@@ -6921,7 +7496,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
isolateScope = scope.$new(true);
}
- transcludeFn = boundTranscludeFn && controllersBoundTransclude;
+ if (boundTranscludeFn) {
+ // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn`
+ // is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
+ transcludeFn = controllersBoundTransclude;
+ transcludeFn.$$boundTransclude = boundTranscludeFn;
+ }
+
if (controllerDirectives) {
// TODO: merge `controllers` and `elementControllers` into single object.
controllers = {};
@@ -6956,8 +7537,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
if (newIsolateScopeDirective) {
- var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
-
compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective ||
templateDirective === newIsolateScopeDirective.$$originalDirective)));
compile.$$addScopeClass($element, true);
@@ -6983,7 +7562,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
isolateBindingContext[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = scope;
- if( attrs[attrName] ) {
+ if (attrs[attrName]) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope);
@@ -6998,7 +7577,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (parentGet.literal) {
compare = equals;
} else {
- compare = function(a,b) { return a === b || (a !== a && b !== b); };
+ compare = function(a, b) { return a === b || (a !== a && b !== b); };
}
parentSet = parentGet.assign || function() {
// reset the change, or we will throw this exception on every $digest
@@ -7022,7 +7601,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
- var unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
+ var unwatch;
+ if (definition.collection) {
+ unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
+ } else {
+ unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
+ }
isolateScope.$on('$destroy', unwatch);
break;
@@ -7043,7 +7627,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
// PRELINKING
- for(i = 0, ii = preLinkFns.length; i < ii; i++) {
+ for (i = 0, ii = preLinkFns.length; i < ii; i++) {
linkFn = preLinkFns[i];
invokeLinkFn(linkFn,
linkFn.isolateScope ? isolateScope : scope,
@@ -7064,7 +7648,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
// POSTLINKING
- for(i = postLinkFns.length - 1; i >= 0; i--) {
+ for (i = postLinkFns.length - 1; i >= 0; i--) {
linkFn = postLinkFns[i];
invokeLinkFn(linkFn,
linkFn.isolateScope ? isolateScope : scope,
@@ -7093,7 +7677,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!futureParentElement) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
- return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement);
+ return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
}
}
@@ -7124,11 +7708,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (name === ignoreDirective) return null;
var match = null;
if (hasDirectives.hasOwnProperty(name)) {
- for(var directive, directives = $injector.get(name + Suffix),
- i = 0, ii = directives.length; i directive.priority) &&
+ if ((maxPriority === undefined || maxPriority > directive.priority) &&
directive.restrict.indexOf(location) != -1) {
if (startAttrName) {
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
@@ -7136,7 +7720,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
tDirectives.push(directive);
match = directive;
}
- } catch(e) { $exceptionHandler(e); }
+ } catch (e) { $exceptionHandler(e); }
}
}
return match;
@@ -7153,8 +7737,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*/
function directiveIsMultiElement(name) {
if (hasDirectives.hasOwnProperty(name)) {
- for(var directive, directives = $injector.get(name + Suffix),
- i = 0, ii = directives.length; i'+template+''+type+'>';
+ wrapper.innerHTML = '<' + type + '>' + template + '' + type + '>';
return wrapper.childNodes[0].childNodes;
default:
return template;
@@ -7419,6 +8006,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
"ng- versions (such as ng-click instead of onclick) instead.");
}
+ // If the attribute was removed, then we are done
+ if (!attr[name]) {
+ return;
+ }
+
// we need to interpolate again, in case the attribute value has been updated
// (e.g. by another directive's compile function)
interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name),
@@ -7442,7 +8034,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
//skip animations when the first digest occurs (when
//both the new and the old values are the same) since
//the CSS classes are the non-interpolated values
- if(name === 'class' && newValue != oldValue) {
+ if (name === 'class' && newValue != oldValue) {
attr.$updateClass(newValue, oldValue);
} else {
attr.$set(name, newValue);
@@ -7472,7 +8064,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
i, ii;
if ($rootElement) {
- for(i = 0, ii = $rootElement.length; i < ii; i++) {
+ for (i = 0, ii = $rootElement.length; i < ii; i++) {
if ($rootElement[i] == firstElementToRemove) {
$rootElement[i++] = newNode;
for (var j = i, j2 = j + removeCount - 1,
@@ -7547,14 +8139,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) {
try {
linkFn(scope, $element, attrs, controllers, transcludeFn);
- } catch(e) {
+ } catch (e) {
$exceptionHandler(e, startingTag($element));
}
}
}];
}
-var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
+var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
/**
* Converts all accepted directives format into proper directive name.
* All of these will become 'myDirective':
@@ -7620,7 +8212,7 @@ function nodesetLinkingFn(
/* NodeList */ nodeList,
/* Element */ rootElement,
/* function(Function) */ boundTranscludeFn
-){}
+) {}
function directiveLinkingFn(
/* nodesetLinkingFn */ nodesetLinkingFn,
@@ -7628,7 +8220,7 @@ function directiveLinkingFn(
/* Node */ node,
/* Element */ rootElement,
/* function(Function) */ boundTranscludeFn
-){}
+) {}
function tokenDifference(str1, str2) {
var values = '',
@@ -7636,16 +8228,33 @@ function tokenDifference(str1, str2) {
tokens2 = str2.split(/\s+/);
outer:
- for(var i = 0; i < tokens1.length; i++) {
+ for (var i = 0; i < tokens1.length; i++) {
var token = tokens1[i];
- for(var j = 0; j < tokens2.length; j++) {
- if(token == tokens2[j]) continue outer;
+ for (var j = 0; j < tokens2.length; j++) {
+ if (token == tokens2[j]) continue outer;
}
values += (values.length > 0 ? ' ' : '') + token;
}
return values;
}
+function removeComments(jqNodes) {
+ jqNodes = jqLite(jqNodes);
+ var i = jqNodes.length;
+
+ if (i <= 1) {
+ return jqNodes;
+ }
+
+ while (i--) {
+ var node = jqNodes[i];
+ if (node.nodeType === NODE_TYPE_COMMENT) {
+ splice.call(jqNodes, i, 1);
+ }
+ }
+ return jqNodes;
+}
+
/**
* @ngdoc provider
* @name $controllerProvider
@@ -7705,6 +8314,10 @@ function $ControllerProvider() {
* * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
* `window` object (not recommended)
*
+ * The string can use the `controller as property` syntax, where the controller instance is published
+ * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
+ * to work correctly.
+ *
* @param {Object} locals Injection locals for Controller.
* @return {Object} Instance of given controller.
*
@@ -7728,7 +8341,7 @@ function $ControllerProvider() {
identifier = ident;
}
- if(isString(expression)) {
+ if (isString(expression)) {
match = expression.match(CNTRL_REG),
constructor = match[1],
identifier = identifier || match[3];
@@ -7750,10 +8363,10 @@ function $ControllerProvider() {
//
// This feature is not intended for use by applications, and is thus not documented
// publicly.
- var Constructor = function() {};
- Constructor.prototype = (isArray(expression) ?
+ // Object creation: http://jsperf.com/create-constructor/2
+ var controllerPrototype = (isArray(expression) ?
expression[expression.length - 1] : expression).prototype;
- instance = new Constructor();
+ instance = Object.create(controllerPrototype);
if (identifier) {
addIdentifier(locals, identifier, instance, constructor || expression.name);
@@ -7814,8 +8427,8 @@ function $ControllerProvider() {
*/
-function $DocumentProvider(){
- this.$get = ['$window', function(window){
+function $DocumentProvider() {
+ this.$get = ['$window', function(window) {
return jqLite(window.document);
}];
}
@@ -7836,8 +8449,8 @@ function $DocumentProvider(){
* ## Example:
*
* ```js
- * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () {
- * return function (exception, cause) {
+ * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
+ * return function(exception, cause) {
* exception.message += ' (caused by "' + cause + '")';
* throw exception;
* };
@@ -7847,6 +8460,14 @@ function $DocumentProvider(){
* This example will override the normal action of `$exceptionHandler`, to make angular
* exceptions fail hard when they happen, instead of just logging to the console.
*
+ *
+ * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind`
+ * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler}
+ * (unless executed during a digest).
+ *
+ * If you wish, you can manually delegate exceptions, e.g.
+ * `try { ... } catch(e) { $exceptionHandler(e); }`
+ *
* @param {Error} exception Exception associated with the error.
* @param {string=} cause optional information about the context in which
* the error was thrown.
@@ -7860,6 +8481,25 @@ function $ExceptionHandlerProvider() {
}];
}
+var APPLICATION_JSON = 'application/json';
+var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'};
+var JSON_START = /^\s*(\[|\{[^\{])/;
+var JSON_END = /[\}\]]\s*$/;
+var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
+
+function defaultHttpResponseTransform(data, headers) {
+ if (isString(data)) {
+ // strip json vulnerability protection prefix
+ data = data.replace(JSON_PROTECTION_PREFIX, '');
+ var contentType = headers('Content-Type');
+ if ((contentType && contentType.indexOf(APPLICATION_JSON) === 0 && data.trim()) ||
+ (JSON_START.test(data) && JSON_END.test(data))) {
+ data = fromJson(data);
+ }
+ }
+ return data;
+}
+
/**
* Parse headers into key value object
*
@@ -7867,7 +8507,7 @@ function $ExceptionHandlerProvider() {
* @returns {Object} Parsed headers as key value object
*/
function parseHeaders(headers) {
- var parsed = {}, key, val, i;
+ var parsed = createMap(), key, val, i;
if (!headers) return parsed;
@@ -7904,7 +8544,11 @@ function headersGetter(headers) {
if (!headersObj) headersObj = parseHeaders(headers);
if (name) {
- return headersObj[lowercase(name)] || null;
+ var value = headersObj[lowercase(name)];
+ if (value === void 0) {
+ value = null;
+ }
+ return value;
}
return headersObj;
@@ -7946,11 +8590,6 @@ function isSuccess(status) {
* Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
* */
function $HttpProvider() {
- var JSON_START = /^\s*(\[|\{[^\{])/,
- JSON_END = /[\}\]]\s*$/,
- PROTECTION_PREFIX = /^\)\]\}',?\n/,
- CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'};
-
/**
* @ngdoc property
* @name $httpProvider#defaults
@@ -7958,6 +8597,11 @@ function $HttpProvider() {
*
* Object containing default values for all {@link ng.$http $http} requests.
*
+ * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`}
+ * that will provide the cache for all requests who set their `cache` property to `true`.
+ * If you set the `default.cache = false` then only requests that specify their own custom
+ * cache object will be cached. See {@link $http#caching $http Caching} for more information.
+ *
* - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token.
* Defaults value is `'XSRF-TOKEN'`.
*
@@ -7971,18 +8615,11 @@ function $HttpProvider() {
* - **`defaults.headers.post`**
* - **`defaults.headers.put`**
* - **`defaults.headers.patch`**
+ *
**/
var defaults = this.defaults = {
// transform incoming response data
- transformResponse: [function(data) {
- if (isString(data)) {
- // strip json vulnerability protection prefix
- data = data.replace(PROTECTION_PREFIX, '');
- if (JSON_START.test(data) && JSON_END.test(data))
- data = fromJson(data);
- }
- return data;
- }],
+ transformResponse: [defaultHttpResponseTransform],
// transform outgoing request data
transformRequest: [function(d) {
@@ -8010,7 +8647,7 @@ function $HttpProvider() {
* @description
*
* Configure $http service to combine processing of multiple http responses received at around
- * the same time via {@link ng.$rootScope#applyAsync $rootScope.$applyAsync}. This can result in
+ * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in
* significant performance improvement for bigger applications that make many HTTP requests
* concurrently (common during application bootstrap).
*
@@ -8032,9 +8669,18 @@ function $HttpProvider() {
};
/**
- * Are ordered by request, i.e. they are applied in the same order as the
+ * @ngdoc property
+ * @name $httpProvider#interceptors
+ * @description
+ *
+ * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http}
+ * pre-processing of request or postprocessing of responses.
+ *
+ * These service factories are ordered by request, i.e. they are applied in the same order as the
* array, on request, but reverse order, on response.
- */
+ *
+ * {@link ng.$http#interceptors Interceptors detailed info}
+ **/
var interceptorFactories = this.interceptors = [];
this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector',
@@ -8086,7 +8732,8 @@ function $HttpProvider() {
* with two $http specific methods: `success` and `error`.
*
* ```js
- * $http({method: 'GET', url: '/someUrl'}).
+ * // Simple GET request example :
+ * $http.get('/someUrl').
* success(function(data, status, headers, config) {
* // this callback will be called asynchronously
* // when the response is available
@@ -8097,6 +8744,20 @@ function $HttpProvider() {
* });
* ```
*
+ * ```js
+ * // Simple POST request example (passing data) :
+ * $http.post('/someUrl', {msg:'hello word!'}).
+ * success(function(data, status, headers, config) {
+ * // this callback will be called asynchronously
+ * // when the response is available
+ * }).
+ * error(function(data, status, headers, config) {
+ * // called asynchronously if an error occurs
+ * // or server returns response with an error status.
+ * });
+ * ```
+ *
+ *
* Since the returned value of calling the $http function is a `promise`, you can also use
* the `then` method to register callbacks, and these callbacks will receive a single argument –
* an object representing the response. See the API signature and type info below for more
@@ -8169,6 +8830,21 @@ function $HttpProvider() {
* In addition, you can supply a `headers` property in the config object passed when
* calling `$http(config)`, which overrides the defaults without changing them globally.
*
+ * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis,
+ * Use the `headers` property, setting the desired header to `undefined`. For example:
+ *
+ * ```js
+ * var req = {
+ * method: 'POST',
+ * url: 'http://example.com',
+ * headers: {
+ * 'Content-Type': undefined
+ * },
+ * data: { test: 'test' },
+ * }
+ *
+ * $http(req).success(function(){...}).error(function(){...});
+ * ```
*
* ## Transforming Requests and Responses
*
@@ -8249,7 +8925,7 @@ function $HttpProvider() {
*
* You can change the default cache to a new object (built with
* {@link ng.$cacheFactory `$cacheFactory`}) by updating the
- * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set
+ * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set
* their `cache` property to `true` will now use this cache object.
*
* If you set the default cache to `false` then only requests that specify their own custom
@@ -8548,6 +9224,10 @@ function $HttpProvider() {
};
var headers = mergeHeaders(requestConfig);
+ if (!angular.isObject(requestConfig)) {
+ throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
+ }
+
extend(config, requestConfig);
config.headers = headers;
config.method = uppercase(config.method);
@@ -8586,7 +9266,7 @@ function $HttpProvider() {
}
});
- while(chain.length) {
+ while (chain.length) {
var thenFn = chain.shift();
var rejectFn = chain.shift();
@@ -8611,9 +9291,12 @@ function $HttpProvider() {
function transformResponse(response) {
// make a copy since the response must be cacheable
- var resp = extend({}, response, {
- data: transformData(response.data, response.headers, config.transformResponse)
- });
+ var resp = extend({}, response);
+ if (!response.data) {
+ resp.data = response.data;
+ } else {
+ resp.data = transformData(response.data, response.headers, config.transformResponse);
+ }
return (isSuccess(response.status))
? resp
: $q.reject(resp);
@@ -8898,7 +9581,7 @@ function $HttpProvider() {
status: status,
headers: headersGetter(headers),
config: config,
- statusText : statusText
+ statusText: statusText
});
}
@@ -8919,7 +9602,7 @@ function $HttpProvider() {
forEach(value, function(v) {
if (isObject(v)) {
- if (isDate(v)){
+ if (isDate(v)) {
v = v.toISOString();
} else {
v = toJson(v);
@@ -8929,7 +9612,7 @@ function $HttpProvider() {
encodeUriQuery(v));
});
});
- if(parts.length > 0) {
+ if (parts.length > 0) {
url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&');
}
return url;
@@ -8937,18 +9620,8 @@ function $HttpProvider() {
}];
}
-function createXhr(method) {
- //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest
- //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest
- //if it is available
- if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) ||
- !window.XMLHttpRequest)) {
- return new window.ActiveXObject("Microsoft.XMLHTTP");
- } else if (window.XMLHttpRequest) {
- return new window.XMLHttpRequest();
- }
-
- throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest.");
+function createXhr() {
+ return new window.XMLHttpRequest();
}
/**
@@ -8974,11 +9647,8 @@ function $HttpBackendProvider() {
}
function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
- var ABORTED = -1;
-
// TODO(vojta): fix the signature
return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
- var status;
$browser.$$incOutstandingRequestCount();
url = url || $browser.url();
@@ -8996,7 +9666,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
});
} else {
- var xhr = createXhr(method);
+ var xhr = createXhr();
xhr.open(method, url, true);
forEach(headers, function(value, key) {
@@ -9005,44 +9675,39 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
});
- // In IE6 and 7, this might be called synchronously when xhr.send below is called and the
- // response is in the cache. the promise api will ensure that to the app code the api is
- // always async
- xhr.onreadystatechange = function() {
- // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by
- // xhrs that are resolved while the app is in the background (see #5426).
- // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before
- // continuing
- //
- // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and
- // Safari respectively.
- if (xhr && xhr.readyState == 4) {
- var responseHeaders = null,
- response = null,
- statusText = '';
+ xhr.onload = function requestLoaded() {
+ var statusText = xhr.statusText || '';
- if(status !== ABORTED) {
- responseHeaders = xhr.getAllResponseHeaders();
+ // responseText is the old-school way of retrieving response (supported by IE8 & 9)
+ // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
+ var response = ('response' in xhr) ? xhr.response : xhr.responseText;
- // responseText is the old-school way of retrieving response (supported by IE8 & 9)
- // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
- response = ('response' in xhr) ? xhr.response : xhr.responseText;
- }
+ // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
+ var status = xhr.status === 1223 ? 204 : xhr.status;
- // Accessing statusText on an aborted xhr object will
- // throw an 'c00c023f error' in IE9 and lower, don't touch it.
- if (!(status === ABORTED && msie < 10)) {
- statusText = xhr.statusText;
- }
-
- completeRequest(callback,
- status || xhr.status,
- response,
- responseHeaders,
- statusText);
+ // fix status code when it is 0 (0 status is undocumented).
+ // Occurs when accessing file resources or on Android 4.1 stock browser
+ // while retrieving files from application cache.
+ if (status === 0) {
+ status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0;
}
+
+ completeRequest(callback,
+ status,
+ response,
+ xhr.getAllResponseHeaders(),
+ statusText);
};
+ var requestError = function() {
+ // The response is always empty
+ // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
+ completeRequest(callback, -1, null, null, '');
+ };
+
+ xhr.onerror = requestError;
+ xhr.onabort = requestError;
+
if (withCredentials) {
xhr.withCredentials = true;
}
@@ -9075,7 +9740,6 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
function timeoutRequest() {
- status = ABORTED;
jsonpDone && jsonpDone();
xhr && xhr.abort();
}
@@ -9085,17 +9749,6 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
timeoutId && $browserDefer.cancel(timeoutId);
jsonpDone = xhr = null;
- // fix status code when it is 0 (0 status is undocumented).
- // Occurs when accessing file resources or on Android 4.1 stock browser
- // while retrieving files from application cache.
- if (status === 0) {
- status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0;
- }
-
- // normalize IE bug (http://bugs.jquery.com/ticket/1450)
- status = status === 1223 ? 204 : status;
- statusText = statusText || '';
-
callback(status, response, headersString, statusText);
$browser.$$completeOutstandingRequest(noop);
}
@@ -9188,7 +9841,7 @@ function $InterpolateProvider() {
* @param {string=} value new value to set the starting symbol to.
* @returns {string|self} Returns the symbol when used as getter and self if used as setter.
*/
- this.startSymbol = function(value){
+ this.startSymbol = function(value) {
if (value) {
startSymbol = value;
return this;
@@ -9206,7 +9859,7 @@ function $InterpolateProvider() {
* @param {string=} value new value to set the ending symbol to.
* @returns {string|self} Returns the symbol when used as getter and self if used as setter.
*/
- this.endSymbol = function(value){
+ this.endSymbol = function(value) {
if (value) {
endSymbol = value;
return this;
@@ -9332,9 +9985,9 @@ function $InterpolateProvider() {
concat = [],
expressionPositions = [];
- while(index < textLength) {
- if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) &&
- ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) {
+ while (index < textLength) {
+ if (((startIndex = text.indexOf(startSymbol, index)) != -1) &&
+ ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) {
if (index !== startIndex) {
concat.push(unescapeText(text.substring(index, startIndex)));
}
@@ -9368,34 +10021,31 @@ function $InterpolateProvider() {
if (!mustHaveExpression || expressions.length) {
var compute = function(values) {
- for(var i = 0, ii = expressions.length; i < ii; i++) {
+ for (var i = 0, ii = expressions.length; i < ii; i++) {
if (allOrNothing && isUndefined(values[i])) return;
concat[expressionPositions[i]] = values[i];
}
return concat.join('');
};
- var getValue = function (value) {
+ var getValue = function(value) {
return trustedContext ?
$sce.getTrusted(trustedContext, value) :
$sce.valueOf(value);
};
- var stringify = function (value) {
+ var stringify = function(value) {
if (value == null) { // null || undefined
return '';
}
switch (typeof value) {
- case 'string': {
+ case 'string':
break;
- }
- case 'number': {
+ case 'number':
value = '' + value;
break;
- }
- default: {
+ default:
value = toJson(value);
- }
}
return value;
@@ -9412,7 +10062,7 @@ function $InterpolateProvider() {
}
return compute(values);
- } catch(err) {
+ } catch (err) {
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
err.toString());
$exceptionHandler(newErr);
@@ -9422,7 +10072,7 @@ function $InterpolateProvider() {
// all of these properties are undocumented for now
exp: text, //just for compatibility with regular watchers created via $watch
expressions: expressions,
- $$watchDelegate: function (scope, listener, objectEquality) {
+ $$watchDelegate: function(scope, listener, objectEquality) {
var lastValue;
return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
var currValue = compute(values);
@@ -9442,8 +10092,9 @@ function $InterpolateProvider() {
function parseStringifyInterceptor(value) {
try {
- return stringify(getValue(value));
- } catch(err) {
+ value = getValue(value);
+ return allOrNothing && !isDefined(value) ? value : stringify(value);
+ } catch (err) {
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
err.toString());
$exceptionHandler(newErr);
@@ -9682,7 +10333,7 @@ function $IntervalProvider() {
*
* * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
*/
-function $LocaleProvider(){
+function $LocaleProvider() {
this.$get = function() {
return {
id: 'en-us',
@@ -9725,7 +10376,7 @@ function $LocaleProvider(){
SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','),
AMPMS: ['AM','PM'],
medium: 'MMM d, y h:mm:ss a',
- short: 'M/d/yy h:mm a',
+ 'short': 'M/d/yy h:mm a',
fullDate: 'EEEE, MMMM d, y',
longDate: 'MMMM d, y',
mediumDate: 'MMM d, y',
@@ -9766,8 +10417,8 @@ function encodePath(path) {
return segments.join('/');
}
-function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) {
- var parsedUrl = urlResolve(absoluteUrl, appBase);
+function parseAbsoluteUrl(absoluteUrl, locationObj) {
+ var parsedUrl = urlResolve(absoluteUrl);
locationObj.$$protocol = parsedUrl.protocol;
locationObj.$$host = parsedUrl.hostname;
@@ -9775,12 +10426,12 @@ function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) {
}
-function parseAppUrl(relativeUrl, locationObj, appBase) {
+function parseAppUrl(relativeUrl, locationObj) {
var prefixed = (relativeUrl.charAt(0) !== '/');
if (prefixed) {
relativeUrl = '/' + relativeUrl;
}
- var match = urlResolve(relativeUrl, appBase);
+ var match = urlResolve(relativeUrl);
locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ?
match.pathname.substring(1) : match.pathname);
locationObj.$$search = parseKeyValue(match.search);
@@ -9835,12 +10486,12 @@ function LocationHtml5Url(appBase, basePrefix) {
this.$$html5 = true;
basePrefix = basePrefix || '';
var appBaseNoFile = stripFile(appBase);
- parseAbsoluteUrl(appBase, this, appBase);
+ parseAbsoluteUrl(appBase, this);
/**
* Parse given html5 (regular) url string into properties
- * @param {string} newAbsoluteUrl HTML5 url
+ * @param {string} url HTML5 url
* @private
*/
this.$$parse = function(url) {
@@ -9850,7 +10501,7 @@ function LocationHtml5Url(appBase, basePrefix) {
appBaseNoFile);
}
- parseAppUrl(pathUrl, this, appBase);
+ parseAppUrl(pathUrl, this);
if (!this.$$path) {
this.$$path = '/';
@@ -9881,14 +10532,14 @@ function LocationHtml5Url(appBase, basePrefix) {
var appUrl, prevAppUrl;
var rewrittenUrl;
- if ( (appUrl = beginsWith(appBase, url)) !== undefined ) {
+ if ((appUrl = beginsWith(appBase, url)) !== undefined) {
prevAppUrl = appUrl;
- if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) {
+ if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) {
rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
} else {
rewrittenUrl = appBase + prevAppUrl;
}
- } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) {
+ } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) {
rewrittenUrl = appBaseNoFile + appUrl;
} else if (appBaseNoFile == url + '/') {
rewrittenUrl = appBaseNoFile;
@@ -9913,7 +10564,7 @@ function LocationHtml5Url(appBase, basePrefix) {
function LocationHashbangUrl(appBase, hashPrefix) {
var appBaseNoFile = stripFile(appBase);
- parseAbsoluteUrl(appBase, this, appBase);
+ parseAbsoluteUrl(appBase, this);
/**
@@ -9933,7 +10584,7 @@ function LocationHashbangUrl(appBase, hashPrefix) {
throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url,
hashPrefix);
}
- parseAppUrl(withoutHashUrl, this, appBase);
+ parseAppUrl(withoutHashUrl, this);
this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase);
@@ -9950,7 +10601,7 @@ function LocationHashbangUrl(appBase, hashPrefix) {
* Inside of Angular, we're always using pathnames that
* do not include drive names for routing.
*/
- function removeWindowsDriveName (path, url, base) {
+ function removeWindowsDriveName(path, url, base) {
/*
Matches paths for file protocol on windows,
such as /C:/foo/bar, and captures only /foo/bar.
@@ -9987,7 +10638,7 @@ function LocationHashbangUrl(appBase, hashPrefix) {
};
this.$$parseLinkUrl = function(url, relHref) {
- if(stripHash(appBase) == stripHash(url)) {
+ if (stripHash(appBase) == stripHash(url)) {
this.$$parse(url);
return true;
}
@@ -10022,11 +10673,11 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
var rewrittenUrl;
var appUrl;
- if ( appBase == stripHash(url) ) {
+ if (appBase == stripHash(url)) {
rewrittenUrl = url;
- } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) {
+ } else if ((appUrl = beginsWith(appBaseNoFile, url))) {
rewrittenUrl = appBase + hashPrefix + appUrl;
- } else if ( appBaseNoFile === url + '/') {
+ } else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
}
if (rewrittenUrl) {
@@ -10047,9 +10698,7 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
}
-LocationHashbangInHtml5Url.prototype =
- LocationHashbangUrl.prototype =
- LocationHtml5Url.prototype = {
+var locationPrototype = {
/**
* Are we in html5 mode?
@@ -10058,7 +10707,7 @@ LocationHashbangInHtml5Url.prototype =
$$html5: false,
/**
- * Has any change been replacing ?
+ * Has any change been replacing?
* @private
*/
$$replace: false,
@@ -10073,6 +10722,13 @@ LocationHashbangInHtml5Url.prototype =
* Return full url representation with all segments encoded according to rules specified in
* [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
*
+ *
+ * ```js
+ * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * var absUrl = $location.absUrl();
+ * // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
+ * ```
+ *
* @return {string} full url
*/
absUrl: locationGetter('$$absUrl'),
@@ -10088,6 +10744,13 @@ LocationHashbangInHtml5Url.prototype =
*
* Change path, search and hash, when called with parameter and return `$location`.
*
+ *
+ * ```js
+ * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * var url = $location.url();
+ * // => "/some/path?foo=bar&baz=xoxo"
+ * ```
+ *
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
* @return {string} url
*/
@@ -10096,8 +10759,8 @@ LocationHashbangInHtml5Url.prototype =
return this.$$url;
var match = PATH_MATCH.exec(url);
- if (match[1]) this.path(decodeURIComponent(match[1]));
- if (match[2] || match[1]) this.search(match[3] || '');
+ if (match[1] || url === '') this.path(decodeURIComponent(match[1]));
+ if (match[2] || match[1] || url === '') this.search(match[3] || '');
this.hash(match[5] || '');
return this;
@@ -10112,6 +10775,13 @@ LocationHashbangInHtml5Url.prototype =
*
* Return protocol of current url.
*
+ *
+ * ```js
+ * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * var protocol = $location.protocol();
+ * // => "http"
+ * ```
+ *
* @return {string} protocol of current url
*/
protocol: locationGetter('$$protocol'),
@@ -10125,6 +10795,13 @@ LocationHashbangInHtml5Url.prototype =
*
* Return host of current url.
*
+ *
+ * ```js
+ * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * var host = $location.host();
+ * // => "example.com"
+ * ```
+ *
* @return {string} host of current url.
*/
host: locationGetter('$$host'),
@@ -10138,6 +10815,13 @@ LocationHashbangInHtml5Url.prototype =
*
* Return port of current url.
*
+ *
+ * ```js
+ * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * var port = $location.port();
+ * // => 80
+ * ```
+ *
* @return {Number} port
*/
port: locationGetter('$$port'),
@@ -10156,11 +10840,18 @@ LocationHashbangInHtml5Url.prototype =
* Note: Path should always begin with forward slash (/), this method will add the forward slash
* if it is missing.
*
+ *
+ * ```js
+ * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * var path = $location.path();
+ * // => "/some/path"
+ * ```
+ *
* @param {(string|number)=} path New path
* @return {string} path
*/
path: locationGetterSetter('$$path', function(path) {
- path = path ? path.toString() : '';
+ path = path !== null ? path.toString() : '';
return path.charAt(0) == '/' ? path : '/' + path;
}),
@@ -10181,10 +10872,9 @@ LocationHashbangInHtml5Url.prototype =
* var searchObject = $location.search();
* // => {foo: 'bar', baz: 'xoxo'}
*
- *
* // set foo to 'yipee'
* $location.search('foo', 'yipee');
- * // => $location
+ * // $location.search() => {foo: 'yipee', baz: 'xoxo'}
* ```
*
* @param {string|Object.|Object.>} search New search params - string or
@@ -10219,6 +10909,7 @@ LocationHashbangInHtml5Url.prototype =
search = search.toString();
this.$$search = parseKeyValue(search);
} else if (isObject(search)) {
+ search = copy(search, {});
// remove object undefined or null properties
forEach(search, function(value, key) {
if (value == null) delete search[key];
@@ -10253,11 +10944,18 @@ LocationHashbangInHtml5Url.prototype =
*
* Change hash fragment when called with parameter and return `$location`.
*
+ *
+ * ```js
+ * // given url http://example.com/some/path?foo=bar&baz=xoxo#hashValue
+ * var hash = $location.hash();
+ * // => "hashValue"
+ * ```
+ *
* @param {(string|number)=} hash New hash fragment
* @return {string} hash
*/
hash: locationGetterSetter('$$hash', function(hash) {
- return hash ? hash.toString() : '';
+ return hash !== null ? hash.toString() : '';
}),
/**
@@ -10274,6 +10972,46 @@ LocationHashbangInHtml5Url.prototype =
}
};
+forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) {
+ Location.prototype = Object.create(locationPrototype);
+
+ /**
+ * @ngdoc method
+ * @name $location#state
+ *
+ * @description
+ * This method is getter / setter.
+ *
+ * Return the history state object when called without any parameter.
+ *
+ * Change the history state object when called with one parameter and return `$location`.
+ * The state object is later passed to `pushState` or `replaceState`.
+ *
+ * NOTE: This method is supported only in HTML5 mode and only in browsers supporting
+ * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
+ * older browsers (like IE9 or Android < 4.0), don't use this method.
+ *
+ * @param {object=} state State object for pushState or replaceState
+ * @return {object} state
+ */
+ Location.prototype.state = function(state) {
+ if (!arguments.length)
+ return this.$$state;
+
+ if (Location !== LocationHtml5Url || !this.$$html5) {
+ throw $locationMinErr('nostate', 'History API state support is available only ' +
+ 'in HTML5 mode and only in browsers supporting HTML5 History API');
+ }
+ // The user might modify `stateObject` after invoking `$location.state(stateObject)`
+ // but we're changing the $$state reference to $browser.state() during the $digest
+ // so the modification window is narrow.
+ this.$$state = isUndefined(state) ? null : state;
+
+ return this;
+ };
+});
+
+
function locationGetter(property) {
return function() {
return this[property];
@@ -10326,11 +11064,12 @@ function locationGetterSetter(property, preprocess) {
* @description
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
*/
-function $LocationProvider(){
+function $LocationProvider() {
var hashPrefix = '',
html5Mode = {
enabled: false,
- requireBase: true
+ requireBase: true,
+ rewriteLinks: true
};
/**
@@ -10354,15 +11093,17 @@ function $LocationProvider(){
* @name $locationProvider#html5Mode
* @description
* @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value.
- * If object, sets `enabled` and `requireBase` to respective values.
- * - **enabled** – `{boolean}` – Sets `html5Mode.enabled`. If true, will rely on
- * `history.pushState` to change urls where supported. Will fall back to hash-prefixed paths
- * in browsers that do not support `pushState`.
- * - **requireBase** - `{boolean}` - Sets `html5Mode.requireBase` (default: `true`). When
- * html5Mode is enabled, specifies whether or not a tag is required to be present. If
- * `enabled` and `requireBase` are true, and a base tag is not present, an error will be
- * thrown when `$location` is injected. See the
- * {@link guide/$location $location guide for more information}
+ * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported
+ * properties:
+ * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to
+ * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not
+ * support `pushState`.
+ * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies
+ * whether or not a tag is required to be present. If `enabled` and `requireBase` are
+ * true, and a base tag is not present, an error will be thrown when `$location` is injected.
+ * See the {@link guide/$location $location guide for more information}
+ * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled,
+ * enables/disables url rewriting for relative links.
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
@@ -10371,12 +11112,19 @@ function $LocationProvider(){
html5Mode.enabled = mode;
return this;
} else if (isObject(mode)) {
- html5Mode.enabled = isBoolean(mode.enabled) ?
- mode.enabled :
- html5Mode.enabled;
- html5Mode.requireBase = isBoolean(mode.requireBase) ?
- mode.requireBase :
- html5Mode.requireBase;
+
+ if (isBoolean(mode.enabled)) {
+ html5Mode.enabled = mode.enabled;
+ }
+
+ if (isBoolean(mode.requireBase)) {
+ html5Mode.requireBase = mode.requireBase;
+ }
+
+ if (isBoolean(mode.rewriteLinks)) {
+ html5Mode.rewriteLinks = mode.rewriteLinks;
+ }
+
return this;
} else {
return html5Mode;
@@ -10388,14 +11136,21 @@ function $LocationProvider(){
* @name $location#$locationChangeStart
* @eventType broadcast on root scope
* @description
- * Broadcasted before a URL will change. This change can be prevented by calling
+ * Broadcasted before a URL will change.
+ *
+ * This change can be prevented by calling
* `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more
* details about event object. Upon successful change
- * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired.
+ * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired.
+ *
+ * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
+ * the browser supports the HTML5 History API.
*
* @param {Object} angularEvent Synthetic event object.
* @param {string} newUrl New URL
* @param {string=} oldUrl URL that was before it was changed.
+ * @param {string=} newState New history state object
+ * @param {string=} oldState History state object that was before it was changed.
*/
/**
@@ -10405,13 +11160,18 @@ function $LocationProvider(){
* @description
* Broadcasted after a URL was changed.
*
+ * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
+ * the browser supports the HTML5 History API.
+ *
* @param {Object} angularEvent Synthetic event object.
* @param {string} newUrl New URL
* @param {string=} oldUrl URL that was before it was changed.
+ * @param {string=} newState New history state object
+ * @param {string=} oldState History state object that was before it was changed.
*/
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
- function( $rootScope, $browser, $sniffer, $rootElement) {
+ function($rootScope, $browser, $sniffer, $rootElement) {
var $location,
LocationMode,
baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to ''
@@ -10432,13 +11192,34 @@ function $LocationProvider(){
$location = new LocationMode(appBase, '#' + hashPrefix);
$location.$$parseLinkUrl(initialUrl, initialUrl);
+ $location.$$state = $browser.state();
+
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
+ function setBrowserUrlWithFallback(url, replace, state) {
+ var oldUrl = $location.url();
+ var oldState = $location.$$state;
+ try {
+ $browser.url(url, replace, state);
+
+ // Make sure $location.state() returns referentially identical (not just deeply equal)
+ // state object; this makes possible quick checking if the state changed in the digest
+ // loop. Checking deep equality would be too expensive.
+ $location.$$state = $browser.state();
+ } catch (e) {
+ // Restore old values if pushState fails
+ $location.url(oldUrl);
+ $location.$$state = oldState;
+
+ throw e;
+ }
+ }
+
$rootElement.on('click', function(event) {
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
- if (event.ctrlKey || event.metaKey || event.which == 2) return;
+ if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.which == 2) return;
var elm = jqLite(event.target);
@@ -10464,6 +11245,9 @@ function $LocationProvider(){
if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) {
if ($location.$$parseLinkUrl(absHref, relHref)) {
+ // We do a preventDefault for all urls that are part of the angular application,
+ // in html5mode and also without, so that we are able to abort navigation without
+ // getting double entries in the location history.
event.preventDefault();
// update location manually
if ($location.absUrl() != $browser.url()) {
@@ -10481,52 +11265,81 @@ function $LocationProvider(){
$browser.url($location.absUrl(), true);
}
- // update $location when $browser url changes
- $browser.onUrlChange(function(newUrl) {
- if ($location.absUrl() != newUrl) {
- $rootScope.$evalAsync(function() {
- var oldUrl = $location.absUrl();
+ var initializing = true;
- $location.$$parse(newUrl);
- if ($rootScope.$broadcast('$locationChangeStart', newUrl,
- oldUrl).defaultPrevented) {
- $location.$$parse(oldUrl);
- $browser.url(oldUrl);
- } else {
- afterLocationChange(oldUrl);
- }
- });
- if (!$rootScope.$$phase) $rootScope.$digest();
- }
+ // update $location when $browser url changes
+ $browser.onUrlChange(function(newUrl, newState) {
+ $rootScope.$evalAsync(function() {
+ var oldUrl = $location.absUrl();
+ var oldState = $location.$$state;
+ var defaultPrevented;
+
+ $location.$$parse(newUrl);
+ $location.$$state = newState;
+
+ defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
+ newState, oldState).defaultPrevented;
+
+ // if the location was changed by a `$locationChangeStart` handler then stop
+ // processing this location change
+ if ($location.absUrl() !== newUrl) return;
+
+ if (defaultPrevented) {
+ $location.$$parse(oldUrl);
+ $location.$$state = oldState;
+ setBrowserUrlWithFallback(oldUrl, false, oldState);
+ } else {
+ initializing = false;
+ afterLocationChange(oldUrl, oldState);
+ }
+ });
+ if (!$rootScope.$$phase) $rootScope.$digest();
});
// update browser
- var changeCounter = 0;
$rootScope.$watch(function $locationWatch() {
var oldUrl = $browser.url();
+ var oldState = $browser.state();
var currentReplace = $location.$$replace;
+ var urlOrStateChanged = oldUrl !== $location.absUrl() ||
+ ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
+
+ if (initializing || urlOrStateChanged) {
+ initializing = false;
- if (!changeCounter || oldUrl != $location.absUrl()) {
- changeCounter++;
$rootScope.$evalAsync(function() {
- if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
- defaultPrevented) {
+ var newUrl = $location.absUrl();
+ var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
+ $location.$$state, oldState).defaultPrevented;
+
+ // if the location was changed by a `$locationChangeStart` handler then stop
+ // processing this location change
+ if ($location.absUrl() !== newUrl) return;
+
+ if (defaultPrevented) {
$location.$$parse(oldUrl);
+ $location.$$state = oldState;
} else {
- $browser.url($location.absUrl(), currentReplace);
- afterLocationChange(oldUrl);
+ if (urlOrStateChanged) {
+ setBrowserUrlWithFallback(newUrl, currentReplace,
+ oldState === $location.$$state ? null : $location.$$state);
+ }
+ afterLocationChange(oldUrl, oldState);
}
});
}
+
$location.$$replace = false;
- return changeCounter;
+ // we don't need to return anything because $evalAsync will make the digest loop dirty when
+ // there is a change
});
return $location;
- function afterLocationChange(oldUrl) {
- $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
+ function afterLocationChange(oldUrl, oldState) {
+ $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl,
+ $location.$$state, oldState);
}
}];
}
@@ -10574,7 +11387,7 @@ function $LocationProvider(){
* @description
* Use the `$logProvider` to configure how the application logs messages
*/
-function $LogProvider(){
+function $LogProvider() {
var debug = true,
self = this;
@@ -10594,7 +11407,7 @@ function $LogProvider(){
}
};
- this.$get = ['$window', function($window){
+ this.$get = ['$window', function($window) {
return {
/**
* @ngdoc method
@@ -10639,7 +11452,7 @@ function $LogProvider(){
* @description
* Write a debug message
*/
- debug: (function () {
+ debug: (function() {
var fn = consoleLog('debug');
return function() {
@@ -10698,7 +11511,7 @@ var $parseMinErr = minErr('$parse');
// Sandboxing Angular Expressions
// ------------------------------
// Angular expressions are generally considered safe because these expressions only have direct
-// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by
+// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by
// obtaining a reference to native JS functions such as the Function constructor.
//
// As an example, consider the following Angular expression:
@@ -10707,7 +11520,7 @@ var $parseMinErr = minErr('$parse');
//
// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
// against the expression language, but not to prevent exploits that were enabled by exposing
-// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good
+// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good
// practice and therefore we are not even trying to protect against interaction with an object
// explicitly exposed in this way.
//
@@ -10715,6 +11528,8 @@ var $parseMinErr = minErr('$parse');
// window or some DOM object that has a reference to window is published onto a Scope.
// Similarly we prevent invocations of function known to be dangerous, as well as assignments to
// native objects.
+//
+// See https://docs.angularjs.org/guide/security
function ensureSafeMemberName(name, fullExpression) {
@@ -10723,7 +11538,7 @@ function ensureSafeMemberName(name, fullExpression) {
|| name === "__proto__") {
throw $parseMinErr('isecfld',
'Attempting to access a disallowed field in Angular expressions! '
- +'Expression: {0}', fullExpression);
+ + 'Expression: {0}', fullExpression);
}
return name;
}
@@ -10785,10 +11600,14 @@ forEach({
CONSTANTS[name] = constantGetter;
});
+//Not quite a constant, but can be lex/parsed the same
+CONSTANTS['this'] = function(self) { return self; };
+CONSTANTS['this'].sharedGetter = true;
+
+
//Operators - will be wrapped by binaryFn/unaryFn/assignment/filter
var OPERATORS = extend(createMap(), {
- /* jshint bitwise : false */
- '+':function(self, locals, a,b){
+ '+':function(self, locals, a, b) {
a=a(self, locals); b=b(self, locals);
if (isDefined(a)) {
if (isDefined(b)) {
@@ -10796,33 +11615,30 @@ var OPERATORS = extend(createMap(), {
}
return a;
}
- return isDefined(b)?b:undefined;},
- '-':function(self, locals, a,b){
+ return isDefined(b) ? b : undefined;},
+ '-':function(self, locals, a, b) {
a=a(self, locals); b=b(self, locals);
- return (isDefined(a)?a:0)-(isDefined(b)?b:0);
+ return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0);
},
- '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);},
- '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);},
- '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);},
- '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);},
- '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);},
- '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);},
- '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);},
- '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);},
- '<':function(self, locals, a,b){return a(self, locals)':function(self, locals, a,b){return a(self, locals)>b(self, locals);},
- '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);},
- '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);},
- '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);},
- '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);},
- '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);},
- '!':function(self, locals, a){return !a(self, locals);},
+ '*':function(self, locals, a, b) {return a(self, locals) * b(self, locals);},
+ '/':function(self, locals, a, b) {return a(self, locals) / b(self, locals);},
+ '%':function(self, locals, a, b) {return a(self, locals) % b(self, locals);},
+ '===':function(self, locals, a, b) {return a(self, locals) === b(self, locals);},
+ '!==':function(self, locals, a, b) {return a(self, locals) !== b(self, locals);},
+ '==':function(self, locals, a, b) {return a(self, locals) == b(self, locals);},
+ '!=':function(self, locals, a, b) {return a(self, locals) != b(self, locals);},
+ '<':function(self, locals, a, b) {return a(self, locals) < b(self, locals);},
+ '>':function(self, locals, a, b) {return a(self, locals) > b(self, locals);},
+ '<=':function(self, locals, a, b) {return a(self, locals) <= b(self, locals);},
+ '>=':function(self, locals, a, b) {return a(self, locals) >= b(self, locals);},
+ '&&':function(self, locals, a, b) {return a(self, locals) && b(self, locals);},
+ '||':function(self, locals, a, b) {return a(self, locals) || b(self, locals);},
+ '!':function(self, locals, a) {return !a(self, locals);},
//Tokenized as operators but parsed as assignment/filters
'=':true,
'|':true
});
-/* jshint bitwise: true */
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
@@ -10832,54 +11648,41 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'
/**
* @constructor
*/
-var Lexer = function (options) {
+var Lexer = function(options) {
this.options = options;
};
Lexer.prototype = {
constructor: Lexer,
- lex: function (text) {
+ lex: function(text) {
this.text = text;
this.index = 0;
- this.ch = undefined;
this.tokens = [];
while (this.index < this.text.length) {
- this.ch = this.text.charAt(this.index);
- if (this.is('"\'')) {
- this.readString(this.ch);
- } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) {
+ var ch = this.text.charAt(this.index);
+ if (ch === '"' || ch === "'") {
+ this.readString(ch);
+ } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
this.readNumber();
- } else if (this.isIdent(this.ch)) {
+ } else if (this.isIdent(ch)) {
this.readIdent();
- } else if (this.is('(){}[].,;:?')) {
- this.tokens.push({
- index: this.index,
- text: this.ch
- });
+ } else if (this.is(ch, '(){}[].,;:?')) {
+ this.tokens.push({index: this.index, text: ch});
this.index++;
- } else if (this.isWhitespace(this.ch)) {
+ } else if (this.isWhitespace(ch)) {
this.index++;
} else {
- var ch2 = this.ch + this.peek();
+ var ch2 = ch + this.peek();
var ch3 = ch2 + this.peek(2);
- var fn = OPERATORS[this.ch];
- var fn2 = OPERATORS[ch2];
- var fn3 = OPERATORS[ch3];
- if (fn3) {
- this.tokens.push({index: this.index, text: ch3, fn: fn3});
- this.index += 3;
- } else if (fn2) {
- this.tokens.push({index: this.index, text: ch2, fn: fn2});
- this.index += 2;
- } else if (fn) {
- this.tokens.push({
- index: this.index,
- text: this.ch,
- fn: fn
- });
- this.index += 1;
+ var op1 = OPERATORS[ch];
+ var op2 = OPERATORS[ch2];
+ var op3 = OPERATORS[ch3];
+ if (op1 || op2 || op3) {
+ var token = op3 ? ch3 : (op2 ? ch2 : ch);
+ this.tokens.push({index: this.index, text: token, operator: true});
+ this.index += token.length;
} else {
this.throwError('Unexpected next character ', this.index, this.index + 1);
}
@@ -10888,8 +11691,8 @@ Lexer.prototype = {
return this.tokens;
},
- is: function(chars) {
- return chars.indexOf(this.ch) !== -1;
+ is: function(ch, chars) {
+ return chars.indexOf(ch) !== -1;
},
peek: function(i) {
@@ -10898,7 +11701,7 @@ Lexer.prototype = {
},
isNumber: function(ch) {
- return ('0' <= ch && ch <= '9');
+ return ('0' <= ch && ch <= '9') && typeof ch === "string";
},
isWhitespace: function(ch) {
@@ -10951,79 +11754,28 @@ Lexer.prototype = {
}
this.index++;
}
- number = 1 * number;
this.tokens.push({
index: start,
text: number,
constant: true,
- fn: function() { return number; }
+ value: Number(number)
});
},
readIdent: function() {
- var expression = this.text;
-
- var ident = '';
var start = this.index;
-
- var lastDot, peekIndex, methodName, ch;
-
while (this.index < this.text.length) {
- ch = this.text.charAt(this.index);
- if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) {
- if (ch === '.') lastDot = this.index;
- ident += ch;
- } else {
+ var ch = this.text.charAt(this.index);
+ if (!(this.isIdent(ch) || this.isNumber(ch))) {
break;
}
this.index++;
}
-
- //check if the identifier ends with . and if so move back one char
- if (lastDot && ident[ident.length - 1] === '.') {
- this.index--;
- ident = ident.slice(0, -1);
- lastDot = ident.lastIndexOf('.');
- if (lastDot === -1) {
- lastDot = undefined;
- }
- }
-
- //check if this is not a method invocation and if it is back out to last dot
- if (lastDot) {
- peekIndex = this.index;
- while (peekIndex < this.text.length) {
- ch = this.text.charAt(peekIndex);
- if (ch === '(') {
- methodName = ident.substr(lastDot - start + 1);
- ident = ident.substr(0, lastDot - start);
- this.index = peekIndex;
- break;
- }
- if (this.isWhitespace(ch)) {
- peekIndex++;
- } else {
- break;
- }
- }
- }
-
this.tokens.push({
index: start,
- text: ident,
- fn: CONSTANTS[ident] || getterFn(ident, this.options, expression)
+ text: this.text.slice(start, this.index),
+ identifier: true
});
-
- if (methodName) {
- this.tokens.push({
- index: lastDot,
- text: '.'
- });
- this.tokens.push({
- index: lastDot + 1,
- text: methodName
- });
- }
},
readString: function(quote) {
@@ -11054,9 +11806,8 @@ Lexer.prototype = {
this.tokens.push({
index: start,
text: rawString,
- string: string,
constant: true,
- fn: function() { return string; }
+ value: string
});
return;
} else {
@@ -11076,13 +11827,13 @@ function isConstant(exp) {
/**
* @constructor
*/
-var Parser = function (lexer, $filter, options) {
+var Parser = function(lexer, $filter, options) {
this.lexer = lexer;
this.$filter = $filter;
this.options = options;
};
-Parser.ZERO = extend(function () {
+Parser.ZERO = extend(function() {
return 0;
}, {
sharedGetter: true,
@@ -11092,7 +11843,7 @@ Parser.ZERO = extend(function () {
Parser.prototype = {
constructor: Parser,
- parse: function (text) {
+ parse: function(text) {
this.text = text;
this.tokens = this.lexer.lex(text);
@@ -11108,7 +11859,7 @@ Parser.prototype = {
return value;
},
- primary: function () {
+ primary: function() {
var primary;
if (this.expect('(')) {
primary = this.filterChain();
@@ -11117,16 +11868,12 @@ Parser.prototype = {
primary = this.arrayDeclaration();
} else if (this.expect('{')) {
primary = this.object();
+ } else if (this.peek().identifier) {
+ primary = this.identifier();
+ } else if (this.peek().constant) {
+ primary = this.constant();
} else {
- var token = this.expect();
- primary = token.fn;
- if (!primary) {
- this.throwError('not a primary expression', token);
- }
- if (token.constant) {
- primary.constant = true;
- primary.literal = true;
- }
+ this.throwError('not a primary expression', this.peek());
}
var next, context;
@@ -11160,8 +11907,11 @@ Parser.prototype = {
},
peek: function(e1, e2, e3, e4) {
- if (this.tokens.length > 0) {
- var token = this.tokens[0];
+ return this.peekAhead(0, e1, e2, e3, e4);
+ },
+ peekAhead: function(i, e1, e2, e3, e4) {
+ if (this.tokens.length > i) {
+ var token = this.tokens[i];
var t = token.text;
if (t === e1 || t === e2 || t === e3 || t === e4 ||
(!e1 && !e2 && !e3 && !e4)) {
@@ -11171,7 +11921,7 @@ Parser.prototype = {
return false;
},
- expect: function(e1, e2, e3, e4){
+ expect: function(e1, e2, e3, e4) {
var token = this.peek(e1, e2, e3, e4);
if (token) {
this.tokens.shift();
@@ -11180,13 +11930,20 @@ Parser.prototype = {
return false;
},
- consume: function(e1){
- if (!this.expect(e1)) {
+ consume: function(e1) {
+ if (this.tokens.length === 0) {
+ throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
+ }
+
+ var token = this.expect(e1);
+ if (!token) {
this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
}
+ return token;
},
- unaryFn: function(fn, right) {
+ unaryFn: function(op, right) {
+ var fn = OPERATORS[op];
return extend(function $parseUnaryFn(self, locals) {
return fn(self, locals, right);
}, {
@@ -11195,7 +11952,8 @@ Parser.prototype = {
});
},
- binaryFn: function(left, fn, right, isBranching) {
+ binaryFn: function(left, op, right, isBranching) {
+ var fn = OPERATORS[op];
return extend(function $parseBinaryFn(self, locals) {
return fn(self, locals, left, right);
}, {
@@ -11204,6 +11962,28 @@ Parser.prototype = {
});
},
+ identifier: function() {
+ var id = this.consume().text;
+
+ //Continue reading each `.identifier` unless it is a method invocation
+ while (this.peek('.') && this.peekAhead(1).identifier && !this.peekAhead(2, '(')) {
+ id += this.consume().text + this.consume().text;
+ }
+
+ return CONSTANTS[id] || getterFn(id, this.options, this.text);
+ },
+
+ constant: function() {
+ var value = this.consume().value;
+
+ return extend(function $parseConstant() {
+ return value;
+ }, {
+ constant: true,
+ literal: true
+ });
+ },
+
statements: function() {
var statements = [];
while (true) {
@@ -11235,8 +12015,7 @@ Parser.prototype = {
},
filter: function(inputFn) {
- var token = this.expect();
- var fn = this.$filter(token.text);
+ var fn = this.$filter(this.consume().text);
var argsFn;
var args;
@@ -11299,17 +12078,14 @@ Parser.prototype = {
var token;
if ((token = this.expect('?'))) {
middle = this.assignment();
- if ((token = this.expect(':'))) {
+ if (this.consume(':')) {
var right = this.assignment();
- return extend(function $parseTernary(self, locals){
+ return extend(function $parseTernary(self, locals) {
return left(self, locals) ? middle(self, locals) : right(self, locals);
}, {
constant: left.constant && middle.constant && right.constant
});
-
- } else {
- this.throwError('expected :', token);
}
}
@@ -11320,7 +12096,7 @@ Parser.prototype = {
var left = this.logicalAND();
var token;
while ((token = this.expect('||'))) {
- left = this.binaryFn(left, token.fn, this.logicalAND(), true);
+ left = this.binaryFn(left, token.text, this.logicalAND(), true);
}
return left;
},
@@ -11329,7 +12105,7 @@ Parser.prototype = {
var left = this.equality();
var token;
if ((token = this.expect('&&'))) {
- left = this.binaryFn(left, token.fn, this.logicalAND(), true);
+ left = this.binaryFn(left, token.text, this.logicalAND(), true);
}
return left;
},
@@ -11338,7 +12114,7 @@ Parser.prototype = {
var left = this.relational();
var token;
if ((token = this.expect('==','!=','===','!=='))) {
- left = this.binaryFn(left, token.fn, this.equality());
+ left = this.binaryFn(left, token.text, this.equality());
}
return left;
},
@@ -11347,7 +12123,7 @@ Parser.prototype = {
var left = this.additive();
var token;
if ((token = this.expect('<', '>', '<=', '>='))) {
- left = this.binaryFn(left, token.fn, this.relational());
+ left = this.binaryFn(left, token.text, this.relational());
}
return left;
},
@@ -11356,7 +12132,7 @@ Parser.prototype = {
var left = this.multiplicative();
var token;
while ((token = this.expect('+','-'))) {
- left = this.binaryFn(left, token.fn, this.multiplicative());
+ left = this.binaryFn(left, token.text, this.multiplicative());
}
return left;
},
@@ -11365,7 +12141,7 @@ Parser.prototype = {
var left = this.unary();
var token;
while ((token = this.expect('*','/','%'))) {
- left = this.binaryFn(left, token.fn, this.unary());
+ left = this.binaryFn(left, token.text, this.unary());
}
return left;
},
@@ -11375,9 +12151,9 @@ Parser.prototype = {
if (this.expect('+')) {
return this.primary();
} else if ((token = this.expect('-'))) {
- return this.binaryFn(Parser.ZERO, token.fn, this.unary());
+ return this.binaryFn(Parser.ZERO, token.text, this.unary());
} else if ((token = this.expect('!'))) {
- return this.unaryFn(token.fn, this.unary());
+ return this.unaryFn(token.text, this.unary());
} else {
return this.primary();
}
@@ -11385,7 +12161,7 @@ Parser.prototype = {
fieldAccess: function(object) {
var expression = this.text;
- var field = this.expect().text;
+ var field = this.consume().text;
var getter = getterFn(field, this.options, expression);
return extend(function $parseFieldAccess(scope, locals, self) {
@@ -11462,7 +12238,7 @@ Parser.prototype = {
},
// This is used with json array declaration
- arrayDeclaration: function () {
+ arrayDeclaration: function() {
var elementFns = [];
if (this.peekToken().text !== ']') {
do {
@@ -11470,8 +12246,7 @@ Parser.prototype = {
// Support trailing commas per ES5.1.
break;
}
- var elementFn = this.expression();
- elementFns.push(elementFn);
+ elementFns.push(this.expression());
} while (this.expect(','));
}
this.consume(']');
@@ -11489,7 +12264,7 @@ Parser.prototype = {
});
},
- object: function () {
+ object: function() {
var keys = [], valueFns = [];
if (this.peekToken().text !== '}') {
do {
@@ -11497,11 +12272,16 @@ Parser.prototype = {
// Support trailing commas per ES5.1.
break;
}
- var token = this.expect();
- keys.push(token.string || token.text);
+ var token = this.consume();
+ if (token.constant) {
+ keys.push(token.value);
+ } else if (token.identifier) {
+ keys.push(token.text);
+ } else {
+ this.throwError("invalid key", token);
+ }
this.consume(':');
- var value = this.expression();
- valueFns.push(value);
+ valueFns.push(this.expression());
} while (this.expect(','));
}
this.consume('}');
@@ -11544,64 +12324,85 @@ function setter(obj, path, setValue, fullExp) {
return setValue;
}
-var getterFnCache = createMap();
+var getterFnCacheDefault = createMap();
+var getterFnCacheExpensive = createMap();
+
+function isPossiblyDangerousMemberName(name) {
+ return name == 'constructor';
+}
/**
* Implementation of the "Black Hole" variant from:
* - http://jsperf.com/angularjs-parse-getter/4
* - http://jsperf.com/path-evaluation-simplified/7
*/
-function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
+function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, expensiveChecks) {
ensureSafeMemberName(key0, fullExp);
ensureSafeMemberName(key1, fullExp);
ensureSafeMemberName(key2, fullExp);
ensureSafeMemberName(key3, fullExp);
ensureSafeMemberName(key4, fullExp);
+ var eso = function(o) {
+ return ensureSafeObject(o, fullExp);
+ };
+ var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity;
+ var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity;
+ var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity;
+ var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity;
+ var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity;
return function cspSafeGetter(scope, locals) {
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope;
if (pathVal == null) return pathVal;
- pathVal = pathVal[key0];
+ pathVal = eso0(pathVal[key0]);
if (!key1) return pathVal;
if (pathVal == null) return undefined;
- pathVal = pathVal[key1];
+ pathVal = eso1(pathVal[key1]);
if (!key2) return pathVal;
if (pathVal == null) return undefined;
- pathVal = pathVal[key2];
+ pathVal = eso2(pathVal[key2]);
if (!key3) return pathVal;
if (pathVal == null) return undefined;
- pathVal = pathVal[key3];
+ pathVal = eso3(pathVal[key3]);
if (!key4) return pathVal;
if (pathVal == null) return undefined;
- pathVal = pathVal[key4];
+ pathVal = eso4(pathVal[key4]);
return pathVal;
};
}
-function getterFn(path, options, fullExp) {
- var fn = getterFnCache[path];
+function getterFnWithEnsureSafeObject(fn, fullExpression) {
+ return function(s, l) {
+ return fn(s, l, ensureSafeObject, fullExpression);
+ };
+}
+function getterFn(path, options, fullExp) {
+ var expensiveChecks = options.expensiveChecks;
+ var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault);
+ var fn = getterFnCache[path];
if (fn) return fn;
+
var pathKeys = path.split('.'),
pathKeysLength = pathKeys.length;
// http://jsperf.com/angularjs-parse-getter/6
if (options.csp) {
if (pathKeysLength < 6) {
- fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp);
+ fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks);
} else {
fn = function cspSafeGetter(scope, locals) {
var i = 0, val;
do {
val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++],
- pathKeys[i++], fullExp)(scope, locals);
+ pathKeys[i++], fullExp, expensiveChecks)(scope, locals);
locals = undefined; // clear after first iteration
scope = val;
@@ -11611,22 +12412,33 @@ function getterFn(path, options, fullExp) {
}
} else {
var code = '';
+ if (expensiveChecks) {
+ code += 's = eso(s, fe);\nl = eso(l, fe);\n';
+ }
+ var needsEnsureSafeObject = expensiveChecks;
forEach(pathKeys, function(key, index) {
ensureSafeMemberName(key, fullExp);
- code += 'if(s == null) return undefined;\n' +
- 's='+ (index
+ var lookupJs = (index
// we simply dereference 's' on any .dot notation
? 's'
// but if we are first then we check locals first, and if so read it first
- : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key + ';\n';
+ : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key;
+ if (expensiveChecks || isPossiblyDangerousMemberName(key)) {
+ lookupJs = 'eso(' + lookupJs + ', fe)';
+ needsEnsureSafeObject = true;
+ }
+ code += 'if(s == null) return undefined;\n' +
+ 's=' + lookupJs + ';\n';
});
code += 'return s;';
/* jshint -W054 */
- var evaledFnGetter = new Function('s', 'l', code); // s=scope, l=locals
+ var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject
/* jshint +W054 */
evaledFnGetter.toString = valueFn(code);
-
+ if (needsEnsureSafeObject) {
+ evaledFnGetter = getterFnWithEnsureSafeObject(evaledFnGetter, fullExp);
+ }
fn = evaledFnGetter;
}
@@ -11638,6 +12450,12 @@ function getterFn(path, options, fullExp) {
return fn;
}
+var objectValueOf = Object.prototype.valueOf;
+
+function getValueOf(value) {
+ return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value);
+}
+
///////////////////////////////////
/**
@@ -11690,15 +12508,20 @@ function getterFn(path, options, fullExp) {
* service.
*/
function $ParseProvider() {
- var cache = createMap();
+ var cacheDefault = createMap();
+ var cacheExpensive = createMap();
- var $parseOptions = {
- csp: false
- };
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
- $parseOptions.csp = $sniffer.csp;
+ var $parseOptions = {
+ csp: $sniffer.csp,
+ expensiveChecks: false
+ },
+ $parseOptionsExpensive = {
+ csp: $sniffer.csp,
+ expensiveChecks: true
+ };
function wrapSharedExpression(exp) {
var wrapped = exp;
@@ -11715,13 +12538,14 @@ function $ParseProvider() {
return wrapped;
}
- return function $parse(exp, interceptorFn) {
+ return function $parse(exp, interceptorFn, expensiveChecks) {
var parsedExpression, oneTime, cacheKey;
switch (typeof exp) {
case 'string':
cacheKey = exp = exp.trim();
+ var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
parsedExpression = cache[cacheKey];
if (!parsedExpression) {
@@ -11730,8 +12554,9 @@ function $ParseProvider() {
exp = exp.substring(2);
}
- var lexer = new Lexer($parseOptions);
- var parser = new Parser(lexer, $filter, $parseOptions);
+ var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
+ var lexer = new Lexer(parseOptions);
+ var parser = new Parser(lexer, $filter, parseOptions);
parsedExpression = parser.parse(exp);
if (parsedExpression.constant) {
@@ -11784,7 +12609,7 @@ function $ParseProvider() {
// attempt to convert the value to a primitive type
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
// be cheaply dirty-checked
- newValue = newValue.valueOf();
+ newValue = getValueOf(newValue);
if (typeof newValue === 'object') {
// objects/arrays are not supported - deep-watching them would be too expensive
@@ -11811,7 +12636,7 @@ function $ParseProvider() {
var newInputValue = inputExpressions(scope);
if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) {
lastResult = parsedExpression(scope);
- oldInputValue = newInputValue && newInputValue.valueOf();
+ oldInputValue = newInputValue && getValueOf(newInputValue);
}
return lastResult;
}, listener, objectEquality);
@@ -11828,7 +12653,7 @@ function $ParseProvider() {
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) {
- oldInputValueOfValues[i] = newInputValue && newInputValue.valueOf();
+ oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
}
@@ -11850,7 +12675,7 @@ function $ParseProvider() {
listener.apply(this, arguments);
}
if (isDefined(value)) {
- scope.$$postDigest(function () {
+ scope.$$postDigest(function() {
if (isDefined(lastValue)) {
unwatch();
}
@@ -11860,23 +12685,24 @@ function $ParseProvider() {
}
function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
- var unwatch;
+ var unwatch, lastValue;
return unwatch = scope.$watch(function oneTimeWatch(scope) {
return parsedExpression(scope);
}, function oneTimeListener(value, old, scope) {
+ lastValue = value;
if (isFunction(listener)) {
listener.call(this, value, old, scope);
}
if (isAllDefined(value)) {
- scope.$$postDigest(function () {
- if(isAllDefined(value)) unwatch();
+ scope.$$postDigest(function() {
+ if (isAllDefined(lastValue)) unwatch();
});
}
}, objectEquality);
function isAllDefined(value) {
var allDefined = true;
- forEach(value, function (val) {
+ forEach(value, function(val) {
if (!isDefined(val)) allDefined = false;
});
return allDefined;
@@ -11897,8 +12723,16 @@ function $ParseProvider() {
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
+ var watchDelegate = parsedExpression.$$watchDelegate;
- var fn = function interceptedExpression(scope, locals) {
+ var regularWatch =
+ watchDelegate !== oneTimeLiteralWatchDelegate &&
+ watchDelegate !== oneTimeWatchDelegate;
+
+ var fn = regularWatch ? function regularInterceptedExpression(scope, locals) {
+ var value = parsedExpression(scope, locals);
+ return interceptorFn(value, scope, locals);
+ } : function oneTimeInterceptedExpression(scope, locals) {
var value = parsedExpression(scope, locals);
var result = interceptorFn(value, scope, locals);
// we only return the interceptor's result if the
@@ -11928,7 +12762,11 @@ function $ParseProvider() {
* @requires $rootScope
*
* @description
- * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
+ * A service that helps you run functions asynchronously, and use their return values (or exceptions)
+ * when they are done processing.
+ *
+ * This is an implementation of promises/deferred objects inspired by
+ * [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
* implementations, and the other which resembles ES6 promises to some degree.
@@ -11945,24 +12783,27 @@ function $ParseProvider() {
* It can be used like so:
*
* ```js
- * return $q(function(resolve, reject) {
- * // perform some asynchronous operation, resolve or reject the promise when appropriate.
- * setInterval(function() {
- * if (pollStatus > 0) {
- * resolve(polledValue);
- * } else if (pollStatus < 0) {
- * reject(polledValue);
- * } else {
- * pollStatus = pollAgain(function(value) {
- * polledValue = value;
- * });
- * }
- * }, 10000);
- * }).
- * then(function(value) {
- * // handle success
+ * // for the purpose of this example let's assume that variables `$q` and `okToGreet`
+ * // are available in the current lexical scope (they could have been injected or passed in).
+ *
+ * function asyncGreet(name) {
+ * // perform some asynchronous operation, resolve or reject the promise when appropriate.
+ * return $q(function(resolve, reject) {
+ * setTimeout(function() {
+ * if (okToGreet(name)) {
+ * resolve('Hello, ' + name + '!');
+ * } else {
+ * reject('Greeting ' + name + ' is not allowed.');
+ * }
+ * }, 1000);
+ * });
+ * }
+ *
+ * var promise = asyncGreet('Robin Hood');
+ * promise.then(function(greeting) {
+ * alert('Success: ' + greeting);
* }, function(reason) {
- * // handle failure
+ * alert('Failed: ' + reason);
* });
* ```
*
@@ -11978,7 +12819,7 @@ function $ParseProvider() {
* asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming.
*
* ```js
- * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
+ * // for the purpose of this example let's assume that variables `$q` and `okToGreet`
* // are available in the current lexical scope (they could have been injected or passed in).
*
* function asyncGreet(name) {
@@ -12061,16 +12902,12 @@ function $ParseProvider() {
*
* - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
*
- * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise,
+ * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise,
* but to do so without modifying the final value. This is useful to release resources or do some
* clean-up that needs to be done whether the promise was rejected or resolved. See the [full
* specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
* more information.
*
- * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as
- * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to
- * make your code IE8 and Android 2.x compatible.
- *
* # Chaining promises
*
* Because calling the `then` method of a promise returns a new derived promise, it is easily
@@ -12237,7 +13074,7 @@ function qFactory(nextTick, exceptionHandler) {
} else {
promise.reject(state.value);
}
- } catch(e) {
+ } catch (e) {
promise.reject(e);
exceptionHandler(e);
}
@@ -12287,7 +13124,7 @@ function qFactory(nextTick, exceptionHandler) {
this.promise.$$state.status = 1;
scheduleProcessQueue(this.promise.$$state);
}
- } catch(e) {
+ } catch (e) {
fns[1](e);
exceptionHandler(e);
}
@@ -12315,7 +13152,7 @@ function qFactory(nextTick, exceptionHandler) {
callback = callbacks[i][3];
try {
result.notify(isFunction(callback) ? callback(progress) : progress);
- } catch(e) {
+ } catch (e) {
exceptionHandler(e);
}
}
@@ -12380,7 +13217,7 @@ function qFactory(nextTick, exceptionHandler) {
var callbackOutput = null;
try {
if (isFunction(callback)) callbackOutput = callback();
- } catch(e) {
+ } catch (e) {
return makePromise(e, false);
}
if (isPromiseLike(callbackOutput)) {
@@ -12488,7 +13325,7 @@ function qFactory(nextTick, exceptionHandler) {
return $Q;
}
-function $$RAFProvider(){ //rAF
+function $$RAFProvider() { //rAF
this.$get = ['$window', '$timeout', function($window, $timeout) {
var requestAnimationFrame = $window.requestAnimationFrame ||
$window.webkitRequestAnimationFrame ||
@@ -12587,7 +13424,7 @@ function $$RAFProvider(){ //rAF
* They also provide an event emission/broadcast and subscription facility. See the
* {@link guide/scope developer guide on scopes}.
*/
-function $RootScopeProvider(){
+function $RootScopeProvider() {
var TTL = 10;
var $rootScopeMinErr = minErr('$rootScope');
var lastDirtyWatch = null;
@@ -12601,7 +13438,7 @@ function $RootScopeProvider(){
};
this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
- function( $injector, $exceptionHandler, $parse, $browser) {
+ function($injector, $exceptionHandler, $parse, $browser) {
/**
* @ngdoc type
@@ -12633,6 +13470,10 @@ function $RootScopeProvider(){
expect(parent.salutation).toEqual('Hello');
* ```
*
+ * When interacting with `Scope` in tests, additional helper methods are available on the
+ * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional
+ * details.
+ *
*
* @param {Object.=} providers Map of service factory which need to be
* provided for the current scope. Defaults to {@link ng}.
@@ -12648,14 +13489,11 @@ function $RootScopeProvider(){
this.$$phase = this.$parent = this.$$watchers =
this.$$nextSibling = this.$$prevSibling =
this.$$childHead = this.$$childTail = null;
- this['this'] = this.$root = this;
+ this.$root = this;
this.$$destroyed = false;
- this.$$asyncQueue = [];
- this.$$postDigestQueue = [];
this.$$listeners = {};
this.$$listenerCount = {};
this.$$isolateBindings = null;
- this.$$applyAsyncQueue = [];
}
/**
@@ -12704,18 +13542,23 @@ function $RootScopeProvider(){
* When creating widgets, it is useful for the widget to not accidentally read parent
* state.
*
+ * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent`
+ * of the newly created scope. Defaults to `this` scope if not provided.
+ * This is used when creating a transclude scope to correctly place it
+ * in the scope hierarchy while maintaining the correct prototypical
+ * inheritance.
+ *
* @returns {Object} The newly created child scope.
*
*/
- $new: function(isolate) {
+ $new: function(isolate, parent) {
var child;
+ parent = parent || this;
+
if (isolate) {
child = new Scope();
child.$root = this.$root;
- // ensure that there is just one async queue per $rootScope and its children
- child.$$asyncQueue = this.$$asyncQueue;
- child.$$postDigestQueue = this.$$postDigestQueue;
} else {
// Only create a child scope class if somebody asks for one,
// but cache it to allow the VM to optimize lookups.
@@ -12732,16 +13575,27 @@ function $RootScopeProvider(){
}
child = new this.$$ChildScope();
}
- child['this'] = child;
- child.$parent = this;
- child.$$prevSibling = this.$$childTail;
- if (this.$$childHead) {
- this.$$childTail.$$nextSibling = child;
- this.$$childTail = child;
+ child.$parent = parent;
+ child.$$prevSibling = parent.$$childTail;
+ if (parent.$$childHead) {
+ parent.$$childTail.$$nextSibling = child;
+ parent.$$childTail = child;
} else {
- this.$$childHead = this.$$childTail = child;
+ parent.$$childHead = parent.$$childTail = child;
}
+
+ // When the new scope is not isolated or we inherit from `this`, and
+ // the parent scope is destroyed, the property `$$destroyed` is inherited
+ // prototypically. In all other cases, this property needs to be set
+ // when the parent scope is destroyed.
+ // The listener needs to be added after the parent is set
+ if (isolate || parent != this) child.$on('$destroy', destroyChild);
+
return child;
+
+ function destroyChild() {
+ child.$$destroyed = true;
+ }
},
/**
@@ -12931,7 +13785,7 @@ function $RootScopeProvider(){
if (!watchExpressions.length) {
// No expressions means we call the listener ASAP
var shouldCall = true;
- self.$evalAsync(function () {
+ self.$evalAsync(function() {
if (shouldCall) listener(newValues, newValues, self);
});
return function deregisterWatchGroup() {
@@ -12948,7 +13802,7 @@ function $RootScopeProvider(){
});
}
- forEach(watchExpressions, function (expr, i) {
+ forEach(watchExpressions, function(expr, i) {
var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) {
newValues[i] = value;
oldValues[i] = oldValue;
@@ -13058,6 +13912,9 @@ function $RootScopeProvider(){
newValue = _value;
var newLength, key, bothNaN, newItem, oldItem;
+ // If the new value is undefined, then return undefined as the watch may be a one-time watch
+ if (isUndefined(newValue)) return;
+
if (!isObject(newValue)) { // if primitive
if (oldValue !== newValue) {
oldValue = newValue;
@@ -13120,7 +13977,7 @@ function $RootScopeProvider(){
if (oldLength > newLength) {
// we used to have more keys, need to find them and destroy them.
changeDetected++;
- for(key in oldValue) {
+ for (key in oldValue) {
if (!newValue.hasOwnProperty(key)) {
oldLength--;
delete oldValue[key];
@@ -13217,8 +14074,6 @@ function $RootScopeProvider(){
$digest: function() {
var watch, value, last,
watchers,
- asyncQueue = this.$$asyncQueue,
- postDigestQueue = this.$$postDigestQueue,
length,
dirty, ttl = TTL,
next, current, target = this,
@@ -13242,7 +14097,7 @@ function $RootScopeProvider(){
dirty = false;
current = target;
- while(asyncQueue.length) {
+ while (asyncQueue.length) {
try {
asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
@@ -13275,11 +14130,11 @@ function $RootScopeProvider(){
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
- logMsg = (isFunction(watch.exp))
- ? 'fn: ' + (watch.exp.name || watch.exp.toString())
- : watch.exp;
- logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
- watchLog[logIdx].push(logMsg);
+ watchLog[logIdx].push({
+ msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
+ newVal: value,
+ oldVal: last
+ });
}
} else if (watch === lastDirtyWatch) {
// If the most recently dirty watcher is now clean, short circuit since the remaining watchers
@@ -13299,7 +14154,7 @@ function $RootScopeProvider(){
// this piece should be kept in sync with the traversal in $broadcast
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
- while(current !== target && !(next = current.$$nextSibling)) {
+ while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
@@ -13307,19 +14162,19 @@ function $RootScopeProvider(){
// `break traverseScopesLoop;` takes us to here
- if((dirty || asyncQueue.length) && !(ttl--)) {
+ if ((dirty || asyncQueue.length) && !(ttl--)) {
clearPhase();
throw $rootScopeMinErr('infdig',
'{0} $digest() iterations reached. Aborting!\n' +
'Watchers fired in the last 5 iterations: {1}',
- TTL, toJson(watchLog));
+ TTL, watchLog);
}
} while (dirty || asyncQueue.length);
clearPhase();
- while(postDigestQueue.length) {
+ while (postDigestQueue.length) {
try {
postDigestQueue.shift()();
} catch (e) {
@@ -13383,6 +14238,10 @@ function $RootScopeProvider(){
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
+ // Disable listeners, watchers and apply/digest methods
+ this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
+ this.$on = this.$watch = this.$watchGroup = function() { return noop; };
+ this.$$listeners = {};
// All of the code below is bogus code that works around V8's memory leak via optimized code
// and inline caches.
@@ -13393,15 +14252,7 @@ function $RootScopeProvider(){
// - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
- this.$$childTail = this.$root = null;
-
- // don't reset these to null in case some async task tries to register a listener/watch/task
- this.$$listeners = {};
- this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
-
- // prevent NPEs since these methods have references to properties we nulled out
- this.$destroy = this.$digest = this.$apply = noop;
- this.$on = this.$watch = this.$watchGroup = function() { return noop; };
+ this.$$childTail = this.$root = this.$$watchers = null;
},
/**
@@ -13468,19 +14319,19 @@ function $RootScopeProvider(){
$evalAsync: function(expr) {
// if we are outside of an $digest loop and this is the first time we are scheduling async
// task also schedule async auto-flush
- if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
+ if (!$rootScope.$$phase && !asyncQueue.length) {
$browser.defer(function() {
- if ($rootScope.$$asyncQueue.length) {
+ if (asyncQueue.length) {
$rootScope.$digest();
}
});
}
- this.$$asyncQueue.push({scope: this, expression: expr});
+ asyncQueue.push({scope: this, expression: expr});
},
- $$postDigest : function(fn) {
- this.$$postDigestQueue.push(fn);
+ $$postDigest: function(fn) {
+ postDigestQueue.push(fn);
},
/**
@@ -13564,7 +14415,7 @@ function $RootScopeProvider(){
*/
$applyAsync: function(expr) {
var scope = this;
- expr && $rootScope.$$applyAsyncQueue.push($applyAsyncExpression);
+ expr && applyAsyncQueue.push($applyAsyncExpression);
scheduleApplyAsync();
function $applyAsyncExpression() {
@@ -13616,8 +14467,11 @@ function $RootScopeProvider(){
var self = this;
return function() {
- namedListeners[namedListeners.indexOf(listener)] = null;
- decrementListenerCount(self, 1, name);
+ var indexOfListener = namedListeners.indexOf(listener);
+ if (indexOfListener !== -1) {
+ namedListeners[indexOfListener] = null;
+ decrementListenerCount(self, 1, name);
+ }
};
},
@@ -13664,7 +14518,7 @@ function $RootScopeProvider(){
do {
namedListeners = scope.$$listeners[name] || empty;
event.currentScope = scope;
- for (i=0, length=namedListeners.length; i= 8 ) {
- normalizedVal = urlResolve(uri).href;
- if (normalizedVal !== '' && !normalizedVal.match(regex)) {
- return 'unsafe:'+normalizedVal;
- }
+ normalizedVal = urlResolve(uri).href;
+ if (normalizedVal !== '' && !normalizedVal.match(regex)) {
+ return 'unsafe:' + normalizedVal;
}
return uri;
};
@@ -13914,15 +14769,6 @@ var SCE_CONTEXTS = {
// Helper functions follow.
-// Copied from:
-// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962
-// Prereq: s is a string.
-function escapeForRegexp(s) {
- return s.replace(/([-()\[\]{}+?*.$\^|,:# to learn more about them.
* You can ensure your document is in standards mode and not quirks mode by adding ``
@@ -14357,7 +15203,7 @@ function $SceDelegateProvider() {
*
* In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted
* $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link
- * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
+ * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
* {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals.
*
* As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
@@ -14573,7 +15419,7 @@ function $SceProvider() {
* @description
* Enables/disables SCE and returns the current value.
*/
- this.enabled = function (value) {
+ this.enabled = function(value) {
if (arguments.length) {
enabled = !!value;
}
@@ -14627,13 +15473,13 @@ function $SceProvider() {
* sce.js and sceSpecs.js would need to be aware of this detail.
*/
- this.$get = ['$parse', '$sniffer', '$sceDelegate', function(
- $parse, $sniffer, $sceDelegate) {
- // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows
+ this.$get = ['$parse', '$sceDelegate', function(
+ $parse, $sceDelegate) {
+ // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow
// the "expression(javascript expression)" syntax which is insecure.
- if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) {
+ if (enabled && msie < 8) {
throw $sceMinErr('iequirks',
- 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' +
+ 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' +
'mode. You can fix this by adding the text to the top of your HTML ' +
'document. See http://docs.angularjs.org/api/ng.$sce for more information.');
}
@@ -14651,7 +15497,7 @@ function $SceProvider() {
* @description
* Returns a boolean indicating if SCE is enabled.
*/
- sce.isEnabled = function () {
+ sce.isEnabled = function() {
return enabled;
};
sce.trustAs = $sceDelegate.trustAs;
@@ -14687,7 +15533,7 @@ function $SceProvider() {
if (parsed.literal && parsed.constant) {
return parsed;
} else {
- return $parse(expr, function (value) {
+ return $parse(expr, function(value) {
return sce.getTrusted(type, value);
});
}
@@ -14856,7 +15702,7 @@ function $SceProvider() {
*
* @description
* Shorthand method. `$sce.parseAsHtml(expression string)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`}
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -14873,7 +15719,7 @@ function $SceProvider() {
*
* @description
* Shorthand method. `$sce.parseAsCss(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`}
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -14890,7 +15736,7 @@ function $SceProvider() {
*
* @description
* Shorthand method. `$sce.parseAsUrl(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`}
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -14907,7 +15753,7 @@ function $SceProvider() {
*
* @description
* Shorthand method. `$sce.parseAsResourceUrl(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`}
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -14924,7 +15770,7 @@ function $SceProvider() {
*
* @description
* Shorthand method. `$sce.parseAsJs(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`}
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -14940,15 +15786,15 @@ function $SceProvider() {
getTrusted = sce.getTrusted,
trustAs = sce.trustAs;
- forEach(SCE_CONTEXTS, function (enumValue, name) {
+ forEach(SCE_CONTEXTS, function(enumValue, name) {
var lName = lowercase(name);
- sce[camelCase("parse_as_" + lName)] = function (expr) {
+ sce[camelCase("parse_as_" + lName)] = function(expr) {
return parse(enumValue, expr);
};
- sce[camelCase("get_trusted_" + lName)] = function (value) {
+ sce[camelCase("get_trusted_" + lName)] = function(value) {
return getTrusted(enumValue, value);
};
- sce[camelCase("trust_as_" + lName)] = function (value) {
+ sce[camelCase("trust_as_" + lName)] = function(value) {
return trustAs(enumValue, value);
};
});
@@ -14965,7 +15811,6 @@ function $SceProvider() {
* @requires $document
*
* @property {boolean} history Does the browser support html5 history api ?
- * @property {boolean} hashchange Does the browser support hashchange event ?
* @property {boolean} transitions Does the browser support CSS transition events ?
* @property {boolean} animations Does the browser support CSS animation events ?
*
@@ -14979,31 +15824,30 @@ function $SnifferProvider() {
int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
document = $document[0] || {},
- documentMode = document.documentMode,
vendorPrefix,
- vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/,
+ vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/,
bodyStyle = document.body && document.body.style,
transitions = false,
animations = false,
match;
if (bodyStyle) {
- for(var prop in bodyStyle) {
- if(match = vendorRegex.exec(prop)) {
+ for (var prop in bodyStyle) {
+ if (match = vendorRegex.exec(prop)) {
vendorPrefix = match[0];
vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1);
break;
}
}
- if(!vendorPrefix) {
+ if (!vendorPrefix) {
vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit';
}
transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle));
animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
- if (android && (!transitions||!animations)) {
+ if (android && (!transitions || !animations)) {
transitions = isString(document.body.style.webkitTransition);
animations = isString(document.body.style.webkitAnimation);
}
@@ -15022,9 +15866,6 @@ function $SnifferProvider() {
// jshint -W018
history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
// jshint +W018
- hashchange: 'onhashchange' in $window &&
- // IE8 compatible mode lies
- (!documentMode || documentMode > 7),
hasEvent: function(event) {
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
// it. In particular the event is not fired when backspace or delete key are pressed or
@@ -15040,11 +15881,9 @@ function $SnifferProvider() {
},
csp: csp(),
vendorPrefix: vendorPrefix,
- transitions : transitions,
- animations : animations,
- android: android,
- msie : msie,
- msieDocumentMode: documentMode
+ transitions: transitions,
+ animations: animations,
+ android: android
};
}];
}
@@ -15074,13 +15913,29 @@ function $TemplateRequestProvider() {
var self = handleRequestFn;
self.totalPendingRequests++;
- return $http.get(tpl, { cache : $templateCache })
+ var transformResponse = $http.defaults && $http.defaults.transformResponse;
+
+ if (isArray(transformResponse)) {
+ var original = transformResponse;
+ transformResponse = [];
+ for (var i = 0; i < original.length; ++i) {
+ var transformer = original[i];
+ if (transformer !== defaultHttpResponseTransform) {
+ transformResponse.push(transformer);
+ }
+ }
+ } else if (transformResponse === defaultHttpResponseTransform) {
+ transformResponse = null;
+ }
+
+ var httpOptions = {
+ cache: $templateCache,
+ transformResponse: transformResponse
+ };
+
+ return $http.get(tpl, httpOptions)
.then(function(response) {
var html = response.data;
- if(!html || html.length === 0) {
- return handleError();
- }
-
self.totalPendingRequests--;
$templateCache.put(tpl, html);
return html;
@@ -15134,7 +15989,7 @@ function $$TestabilityProvider() {
if (dataBinding) {
forEach(dataBinding, function(bindingName) {
if (opt_exactMatch) {
- var matcher = new RegExp('(^|\\s)' + expression + '(\\s|\\||$)');
+ var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)');
if (matcher.test(bindingName)) {
matches.push(binding);
}
@@ -15256,7 +16111,7 @@ function $TimeoutProvider() {
timeoutId = $browser.defer(function() {
try {
deferred.resolve(fn());
- } catch(e) {
+ } catch (e) {
deferred.reject(e);
$exceptionHandler(e);
}
@@ -15307,7 +16162,7 @@ function $TimeoutProvider() {
// exactly the behavior needed here. There is little value is mocking these out for this
// service.
var urlParsingNode = document.createElement("a");
-var originUrl = urlResolve(window.location.href, true);
+var originUrl = urlResolve(window.location.href);
/**
@@ -15362,7 +16217,7 @@ var originUrl = urlResolve(window.location.href, true);
* | pathname | The pathname, beginning with "/"
*
*/
-function urlResolve(url, base) {
+function urlResolve(url) {
var href = url;
if (msie) {
@@ -15422,7 +16277,7 @@ function urlIsSameOrigin(requestUrl) {