diff --git a/CHANGELOG.md b/CHANGELOG.md index c984fcfe..885a9574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Security +- Upgrade vendored JS libraries to patched versions, resolving Trivy findings: + - `underscore` 1.7.0 → 1.13.8 (CVE-2021-23358 critical, CVE-2026-27601 high) + - `angular` 1.4.14 → 1.8.3 (CVE-2019-10768 high) + - `prism` 1.0.1 → 1.30.0 (CVE-2021-23341, CVE-2021-32723 high) + ## [2.1.2] - 2026-07-22 diff --git a/js/bower.json b/js/bower.json index ea4506b2..cdebfd2f 100644 --- a/js/bower.json +++ b/js/bower.json @@ -2,13 +2,13 @@ "name": "owncloud-notes", "version": "0.0.1", "dependencies": { - "angular": "1.4.*", + "angular": "1.8.3", "restangular": "1.3.*", "jquery": "2.1.*", - "underscore": "1.7.*", - "angular-mocks": "1.4.*", - "angular-route": "1.4.*", - "prism": "~1.0.1", + "underscore": "1.13.*", + "angular-mocks": "1.8.3", + "angular-route": "1.8.3", + "prism": "1.30.*", "mdEdit": "https://github.com/jbt/mdEdit.git#master" } } diff --git a/js/vendor/angular-mocks/.bower.json b/js/vendor/angular-mocks/.bower.json index 0d4de133..4f9071ed 100644 --- a/js/vendor/angular-mocks/.bower.json +++ b/js/vendor/angular-mocks/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-mocks", - "version": "1.4.14", + "version": "1.8.3", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.4.14" + "angular": "1.8.3" }, "homepage": "https://github.com/angular/bower-angular-mocks", - "_release": "1.4.14", + "_release": "1.8.3", "_resolution": { "type": "version", - "tag": "v1.4.14", - "commit": "d6d76294fbc8ee9e5d5b581968b775910b4e67d0" + "tag": "v1.8.3", + "commit": "3e622cd9a90cb953c5d877e2efe3504ac98b6c74" }, "_source": "https://github.com/angular/bower-angular-mocks.git", - "_target": "1.4.*", + "_target": "1.8.3", "_originalSource": "angular-mocks" } \ No newline at end of file diff --git a/js/vendor/angular-mocks/README.md b/js/vendor/angular-mocks/README.md index 61b9d8c0..ac5acf71 100644 --- a/js/vendor/angular-mocks/README.md +++ b/js/vendor/angular-mocks/README.md @@ -1,8 +1,11 @@ # packaged angular-mocks -This repo is for distribution on `npm` and `bower`. The source for this module is in the -[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngMock). -Please file issues and pull requests against that repo. +**This package contains the legacy AngularJS (version 1.x). AngularJS support has officially ended +as of January 2022. +[See what ending support means](https://docs.angularjs.org/misc/version-support-status) and +[read the end of life announcement](https://goo.gle/angularjs-end-of-life).** + +**[See `@angular/core` for the actively supported Angular](https://npmjs.com/@angular/core).** ## Install @@ -42,7 +45,7 @@ Documentation is available on the The MIT License -Copyright (c) 2010-2015 Google, Inc. http://angularjs.org +Copyright (c) 2022 Google LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/js/vendor/angular-mocks/angular-mocks.js b/js/vendor/angular-mocks/angular-mocks.js index fc82ed86..f9435299 100644 --- a/js/vendor/angular-mocks/angular-mocks.js +++ b/js/vendor/angular-mocks/angular-mocks.js @@ -1,18 +1,68 @@ /** - * @license AngularJS v1.4.14 - * (c) 2010-2015 Google, Inc. http://angularjs.org + * @license AngularJS v1.8.3 + * (c) 2010-2020 Google LLC. http://angularjs.org * License: MIT */ -(function(window, angular, undefined) { +(function(window, angular) { 'use strict'; +/* global routeToRegExp: true */ + +/** + * @param {string} path - The path to parse. (It is assumed to have query and hash stripped off.) + * @param {Object} opts - Options. + * @return {Object} - An object containing an array of path parameter names (`keys`) and a regular + * expression (`regexp`) that can be used to identify a matching URL and extract the path + * parameter values. + * + * @description + * Parses the given path, extracting path parameter names and a regular expression to match URLs. + * + * Originally inspired by `pathRexp` in `visionmedia/express/lib/utils.js`. + */ +function routeToRegExp(path, opts) { + var keys = []; + + var pattern = path + .replace(/([().])/g, '\\$1') + .replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) { + var optional = option === '?' || option === '*?'; + var star = option === '*' || option === '*?'; + keys.push({name: key, optional: optional}); + slash = slash || ''; + return ( + (optional ? '(?:' + slash : slash + '(?:') + + (star ? '(.+?)' : '([^/]+)') + + (optional ? '?)?' : ')') + ); + }) + .replace(/([/$*])/g, '\\$1'); + + if (opts.ignoreTrailingSlashes) { + pattern = pattern.replace(/\/+$/, '') + '/*'; + } + + return { + keys: keys, + regexp: new RegExp( + '^' + pattern + '(?:[?#]|$)', + opts.caseInsensitiveMatch ? 'i' : '' + ) + }; +} + +'use strict'; + +/* global routeToRegExp: false */ + /** * @ngdoc object * @name angular.mock * @description * * Namespace from 'angular-mocks.js' which contains testing related code. + * */ angular.mock = {}; @@ -24,29 +74,33 @@ angular.mock = {}; * @description * This service is a mock implementation of {@link ng.$browser}. It provides fake * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, - * cookies, etc... + * cookies, etc. * * The api of this service is the same as that of the real {@link ng.$browser $browser}, except * that there are several helper methods available which can be used in tests. */ angular.mock.$BrowserProvider = function() { - this.$get = function() { - return new angular.mock.$Browser(); - }; + this.$get = [ + '$log', '$$taskTrackerFactory', + function($log, $$taskTrackerFactory) { + return new angular.mock.$Browser($log, $$taskTrackerFactory); + } + ]; }; -angular.mock.$Browser = function() { +angular.mock.$Browser = function($log, $$taskTrackerFactory) { var self = this; + var taskTracker = $$taskTrackerFactory($log); this.isMock = true; - self.$$url = "http://server/"; + self.$$url = 'http://server/'; self.$$lastUrl = self.$$url; // used by url polling fn self.pollFns = []; - // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = angular.noop; - self.$$incOutstandingRequestCount = angular.noop; - + // Task-tracking API + self.$$completeOutstandingRequest = taskTracker.completeTask; + self.$$incOutstandingRequestCount = taskTracker.incTaskCount; + self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks; // register url polling fn @@ -70,11 +124,22 @@ angular.mock.$Browser = function() { self.deferredFns = []; self.deferredNextId = 0; - self.defer = function(fn, delay) { + self.defer = function(fn, delay, taskType) { + var timeoutId = self.deferredNextId++; + delay = delay || 0; - self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); - self.deferredFns.sort(function(a, b) { return a.time - b.time;}); - return self.deferredNextId++; + taskType = taskType || taskTracker.DEFAULT_TASK_TYPE; + + taskTracker.incTaskCount(taskType); + self.deferredFns.push({ + id: timeoutId, + type: taskType, + time: (self.defer.now + delay), + fn: fn + }); + self.deferredFns.sort(function(a, b) { return a.time - b.time; }); + + return timeoutId; }; @@ -88,14 +153,15 @@ angular.mock.$Browser = function() { self.defer.cancel = function(deferId) { - var fnIndex; + var taskIndex; - angular.forEach(self.deferredFns, function(fn, index) { - if (fn.id === deferId) fnIndex = index; + angular.forEach(self.deferredFns, function(task, index) { + if (task.id === deferId) taskIndex = index; }); - if (angular.isDefined(fnIndex)) { - self.deferredFns.splice(fnIndex, 1); + if (angular.isDefined(taskIndex)) { + var task = self.deferredFns.splice(taskIndex, 1)[0]; + taskTracker.completeTask(angular.noop, task.type); return true; } @@ -109,21 +175,83 @@ angular.mock.$Browser = function() { * @description * Flushes all pending requests and executes the defer callbacks. * + * See {@link ngMock.$flushPendingsTasks} for more info. + * * @param {number=} number of milliseconds to flush. See {@link #defer.now} */ self.defer.flush = function(delay) { + var nextTime; + if (angular.isDefined(delay)) { - self.defer.now += delay; + // A delay was passed so compute the next time + nextTime = self.defer.now + delay; + } else if (self.deferredFns.length) { + // No delay was passed so set the next time so that it clears the deferred queue + nextTime = self.deferredFns[self.deferredFns.length - 1].time; } else { - if (self.deferredFns.length) { - self.defer.now = self.deferredFns[self.deferredFns.length - 1].time; - } else { - throw new Error('No deferred tasks to be flushed'); - } + // No delay passed, but there are no deferred tasks so flush - indicates an error! + throw new Error('No deferred tasks to be flushed'); + } + + while (self.deferredFns.length && self.deferredFns[0].time <= nextTime) { + // Increment the time and call the next deferred function + self.defer.now = self.deferredFns[0].time; + var task = self.deferredFns.shift(); + taskTracker.completeTask(task.fn, task.type); } - while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { - self.deferredFns.shift().fn(); + // Ensure that the current time is correct + self.defer.now = nextTime; + }; + + /** + * @name $browser#defer.getPendingTasks + * + * @description + * Returns the currently pending tasks that need to be flushed. + * You can request a specific type of tasks only, by specifying a `taskType`. + * + * @param {string=} taskType - The type tasks to return. + */ + self.defer.getPendingTasks = function(taskType) { + return !taskType + ? self.deferredFns + : self.deferredFns.filter(function(task) { return task.type === taskType; }); + }; + + /** + * @name $browser#defer.formatPendingTasks + * + * @description + * Formats each task in a list of pending tasks as a string, suitable for use in error messages. + * + * @param {Array} pendingTasks - A list of task objects. + * @return {Array} A list of stringified tasks. + */ + self.defer.formatPendingTasks = function(pendingTasks) { + return pendingTasks.map(function(task) { + return '{id: ' + task.id + ', type: ' + task.type + ', time: ' + task.time + '}'; + }); + }; + + /** + * @name $browser#defer.verifyNoPendingTasks + * + * @description + * Verifies that there are no pending tasks that need to be flushed. + * You can check for a specific type of tasks only, by specifying a `taskType`. + * + * See {@link $verifyNoPendingTasks} for more info. + * + * @param {string=} taskType - The type tasks to check for. + */ + self.defer.verifyNoPendingTasks = function(taskType) { + var pendingTasks = self.defer.getPendingTasks(taskType); + + if (pendingTasks.length) { + var formattedTasks = self.defer.formatPendingTasks(pendingTasks).join('\n '); + throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' + + formattedTasks); } }; @@ -134,12 +262,12 @@ angular.mock.$Browser = function() { }; angular.mock.$Browser.prototype = { -/** - * @name $browser#poll - * - * @description - * run all fns in pollFns - */ + /** + * @name $browser#poll + * + * @description + * run all fns in pollFns + */ poll: function poll() { angular.forEach(this.pollFns, function(pollFn) { pollFn(); @@ -151,7 +279,8 @@ angular.mock.$Browser.prototype = { state = null; } if (url) { - this.$$url = url; + // The `$browser` service trims empty hashes; simulate it. + this.$$url = url.replace(/#$/, ''); // Native pushState serializes & copies the object; simulate it. this.$$state = angular.copy(state); return this; @@ -162,13 +291,85 @@ angular.mock.$Browser.prototype = { state: function() { return this.$$state; - }, - - notifyWhenNoOutstandingRequests: function(fn) { - fn(); } }; +/** + * @ngdoc service + * @name $flushPendingTasks + * + * @description + * Flushes all currently pending tasks and executes the corresponding callbacks. + * + * Optionally, you can also pass a `delay` argument to only flush tasks that are scheduled to be + * executed within `delay` milliseconds. Currently, `delay` only applies to timeouts, since all + * other tasks have a delay of 0 (i.e. they are scheduled to be executed as soon as possible, but + * still asynchronously). + * + * If no delay is specified, it uses a delay such that all currently pending tasks are flushed. + * + * The types of tasks that are flushed include: + * + * - Pending timeouts (via {@link $timeout}). + * - Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}. + * - Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises). + * + *
+ * Periodic tasks scheduled via {@link $interval} use a different queue and are not flushed by + * `$flushPendingTasks()`. Use {@link ngMock.$interval#flush $interval.flush(millis)} instead. + *
+ * + * @param {number=} delay - The number of milliseconds to flush. + */ +angular.mock.$FlushPendingTasksProvider = function() { + this.$get = [ + '$browser', + function($browser) { + return function $flushPendingTasks(delay) { + return $browser.defer.flush(delay); + }; + } + ]; +}; + +/** + * @ngdoc service + * @name $verifyNoPendingTasks + * + * @description + * Verifies that there are no pending tasks that need to be flushed. It throws an error if there are + * still pending tasks. + * + * You can check for a specific type of tasks only, by specifying a `taskType`. + * + * Available task types: + * + * - `$timeout`: Pending timeouts (via {@link $timeout}). + * - `$http`: Pending HTTP requests (via {@link $http}). + * - `$route`: In-progress route transitions (via {@link $route}). + * - `$applyAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}. + * - `$evalAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises). + * + *
+ * Periodic tasks scheduled via {@link $interval} use a different queue and are not taken into + * account by `$verifyNoPendingTasks()`. There is currently no way to verify that there are no + * pending {@link $interval} tasks. + *
+ * + * @param {string=} taskType - The type of tasks to check for. + */ +angular.mock.$VerifyNoPendingTasksProvider = function() { + this.$get = [ + '$browser', + function($browser) { + return function $verifyNoPendingTasks(taskType) { + return $browser.defer.verifyNoPendingTasks(taskType); + }; + } + ]; +}; /** * @ngdoc provider @@ -226,13 +427,13 @@ angular.mock.$ExceptionHandlerProvider = function() { * @param {string} mode Mode of operation, defaults to `rethrow`. * * - `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()} + * 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()}. * - `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. - * For any implementations that expect exceptions to be thrown, the `rethrow` mode - * will also maintain a log of thrown errors. + * is a bug in the application or test, so this mock will make these tests fail. For any + * implementations that expect exceptions to be thrown, the `rethrow` mode will also maintain + * a log of thrown errors in `$exceptionHandler.errors`. */ this.mode = function(mode) { @@ -241,19 +442,19 @@ angular.mock.$ExceptionHandlerProvider = function() { case 'rethrow': var errors = []; handler = function(e) { - if (arguments.length == 1) { + if (arguments.length === 1) { errors.push(e); } else { errors.push([].slice.call(arguments, 0)); } - if (mode === "rethrow") { + if (mode === 'rethrow') { throw e; } }; handler.errors = errors; break; default: - throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); + throw new Error('Unknown mode \'' + mode + '\', only \'log\'/\'rethrow\' modes are allowed!'); } }; @@ -403,8 +604,8 @@ angular.mock.$LogProvider = function() { }); }); if (errors.length) { - errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " + - "an expected log message was not checked and removed:"); + 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')); } @@ -437,62 +638,40 @@ angular.mock.$LogProvider = function() { * @returns {promise} A promise which will be notified on each iteration. */ angular.mock.$IntervalProvider = function() { - this.$get = ['$browser', '$rootScope', '$q', '$$q', - function($browser, $rootScope, $q, $$q) { + this.$get = ['$browser', '$$intervalFactory', + function($browser, $$intervalFactory) { var repeatFns = [], nextRepeatId = 0, - now = 0; - - var $interval = function(fn, delay, count, invokeApply) { - var hasParams = arguments.length > 4, - args = hasParams ? Array.prototype.slice.call(arguments, 4) : [], - iteration = 0, - skipApply = (angular.isDefined(invokeApply) && !invokeApply), - deferred = (skipApply ? $$q : $q).defer(), - promise = deferred.promise; - - count = (angular.isDefined(count)) ? count : 0; - promise.then(null, null, (!hasParams) ? fn : function() { - fn.apply(null, args); - }); - - promise.$$intervalId = nextRepeatId; - - function tick() { - deferred.notify(iteration++); - - if (count > 0 && iteration >= count) { - var fnIndex; - deferred.resolve(iteration); - - angular.forEach(repeatFns, function(fn, index) { - if (fn.id === promise.$$intervalId) fnIndex = index; + now = 0, + setIntervalFn = function(tick, delay, deferred, skipApply) { + var id = nextRepeatId++; + var fn = !skipApply ? tick : function() { + tick(); + $browser.defer.flush(); + }; + + repeatFns.push({ + nextTime: (now + (delay || 0)), + delay: delay || 1, + fn: fn, + id: id, + deferred: deferred }); + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime; }); - if (angular.isDefined(fnIndex)) { - repeatFns.splice(fnIndex, 1); + return id; + }, + clearIntervalFn = function(id) { + for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) { + if (repeatFns[fnIndex].id === id) { + repeatFns.splice(fnIndex, 1); + break; + } } - } - - if (skipApply) { - $browser.defer.flush(); - } else { - $rootScope.$apply(); - } - } + }; - repeatFns.push({ - nextTime:(now + delay), - delay: delay, - fn: tick, - id: nextRepeatId, - deferred: deferred - }); - repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); + var $interval = $$intervalFactory(setIntervalFn, clearIntervalFn); - nextRepeatId++; - return promise; - }; /** * @ngdoc method * @name $interval#cancel @@ -505,16 +684,15 @@ angular.mock.$IntervalProvider = function() { */ $interval.cancel = function(promise) { if (!promise) return false; - var fnIndex; - - angular.forEach(repeatFns, function(fn, index) { - if (fn.id === promise.$$intervalId) fnIndex = index; - }); - if (angular.isDefined(fnIndex)) { - repeatFns[fnIndex].deferred.reject('canceled'); - repeatFns.splice(fnIndex, 1); - return true; + for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) { + if (repeatFns[fnIndex].id === promise.$$intervalId) { + var deferred = repeatFns[fnIndex].deferred; + deferred.promise.then(undefined, function() {}); + deferred.reject('canceled'); + repeatFns.splice(fnIndex, 1); + return true; + } } return false; @@ -527,15 +705,21 @@ angular.mock.$IntervalProvider = function() { * * Runs interval tasks scheduled to be run in the next `millis` milliseconds. * - * @param {number=} millis maximum timeout amount to flush up until. + * @param {number} millis maximum timeout amount to flush up until. * * @return {number} The amount of time moved forward. */ $interval.flush = function(millis) { + var before = now; now += millis; while (repeatFns.length && repeatFns[0].nextTime <= now) { var task = repeatFns[0]; task.fn(); + if (task.nextTime === before) { + // this can only happen the first time + // a zero-delay interval gets triggered + task.nextTime++; + } task.nextTime += task.delay; repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); } @@ -547,16 +731,13 @@ angular.mock.$IntervalProvider = function() { }; -/* jshint -W101 */ -/* The R_ISO8061_STR regex is never going to fit into the 100 char limit! - * This directive should go inside the anonymous function but a bug in JSHint means that it would - * not be enacted early enough to prevent the warning. - */ -var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; - function jsonStringToDate(string) { + // The R_ISO8061_STR regex is never going to fit into the 100 char limit! + // eslit-disable-next-line max-len + var R_ISO8061_STR = /^(-?\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; + var match; - if (match = string.match(R_ISO8061_STR)) { + if ((match = string.match(R_ISO8061_STR))) { var date = new Date(0), tzHour = 0, tzMin = 0; @@ -578,7 +759,7 @@ function toInt(str) { return parseInt(str, 10); } -function padNumber(num, digits, trim) { +function padNumberInMock(num, digits, trim) { var neg = ''; if (num < 0) { neg = '-'; @@ -639,9 +820,10 @@ angular.mock.TzDate = function(offset, timestamp) { timestamp = self.origDate.getTime(); if (isNaN(timestamp)) { + // eslint-disable-next-line no-throw-literal throw { - name: "Illegal Argument", - message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" + name: 'Illegal Argument', + message: 'Arg \'' + tsStr + '\' passed into TzDate constructor is not a valid date string' }; } } else { @@ -727,13 +909,13 @@ angular.mock.TzDate = function(offset, timestamp) { // provide this method only on browsers that already have it if (self.toISOString) { self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; + return padNumberInMock(self.origDate.getUTCFullYear(), 4) + '-' + + padNumberInMock(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumberInMock(self.origDate.getUTCDate(), 2) + 'T' + + padNumberInMock(self.origDate.getUTCHours(), 2) + ':' + + padNumberInMock(self.origDate.getUTCMinutes(), 2) + ':' + + padNumberInMock(self.origDate.getUTCSeconds(), 2) + '.' + + padNumberInMock(self.origDate.getUTCMilliseconds(), 3) + 'Z'; }; } @@ -747,7 +929,7 @@ angular.mock.TzDate = function(offset, timestamp) { angular.forEach(unimplementedMethods, function(methodName) { self[methodName] = function() { - throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); + throw new Error('Method \'' + methodName + '\' is not implemented in the TzDate mock'); }; }); @@ -756,7 +938,6 @@ angular.mock.TzDate = function(offset, timestamp) { //make "tzDateInstance instanceof Date" return true angular.mock.TzDate.prototype = Date.prototype; -/* jshint +W101 */ /** @@ -766,8 +947,11 @@ angular.mock.TzDate.prototype = Date.prototype; * @description * Mock implementation of the {@link ng.$animate `$animate`} service. Exposes two additional methods * for testing animations. + * + * You need to require the `ngAnimateMock` module in your test suite for instance `beforeEach(module('ngAnimateMock'))` */ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) + .info({ angularVersion: '1.8.3' }) .config(['$provide', function($provide) { @@ -931,13 +1115,10 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) * @name angular.mock.dump * @description * - * *NOTE*: this is not an injectable instance, just a globally available function. + * *NOTE*: This is not an injectable instance, just a globally available function. * - * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for - * debugging. - * - * This method is also available on window, where it can be used to display objects on debug - * console. + * Method for serializing common AngularJS objects (scope, elements, etc..) into strings. + * It is useful for logging objects to the console when debugging. * * @param {*} object - any object to turn into string. * @return {string} a serialized string of the argument @@ -1003,8 +1184,10 @@ angular.mock.dump = function(object) { * Fake HTTP backend implementation suitable for unit testing applications that use the * {@link ng.$http $http service}. * - * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less + *
+ * **Note**: For fake HTTP backend implementation suitable for end-to-end testing or backend-less * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. + *
* * During unit testing, we want our unit tests to run quickly and have no external dependencies so * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or @@ -1016,7 +1199,7 @@ angular.mock.dump = function(object) { * This mock implementation can be used to respond with static or dynamic responses via the * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). * - * When an Angular application needs some data from a server, it calls the $http service, which + * When an AngularJS application needs some data from a server, it calls the $http service, which * sends the request to a real server using $httpBackend service. With dependency injection, it is * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify * the requests and respond with some testing data without sending a request to a real server. @@ -1028,7 +1211,7 @@ angular.mock.dump = function(object) { * - `$httpBackend.when` - specifies a backend definition * * - * # Request Expectations vs Backend Definitions + * ## Request Expectations vs Backend Definitions * * Request expectations provide a way to make assertions about requests made by the application and * to define responses for those requests. The test will fail if the expected requests are not made @@ -1084,7 +1267,7 @@ angular.mock.dump = function(object) { * the request. The response from the first matched definition is returned. * * - * # Flushing HTTP requests + * ## Flushing HTTP requests * * The $httpBackend used in production always responds to requests asynchronously. If we preserved * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, @@ -1094,7 +1277,7 @@ angular.mock.dump = function(object) { * the async api of the backend, while allowing the test to execute synchronously. * * - * # Unit testing with mock $httpBackend + * ## Unit testing with mock $httpBackend * The following code shows how to setup and use the mock backend when unit testing a controller. * First we create the controller under test: * @@ -1108,18 +1291,20 @@ angular.mock.dump = function(object) { function MyController($scope, $http) { var authToken; - $http.get('/auth.py').success(function(data, status, headers) { - authToken = headers('A-Token'); - $scope.user = data; + $http.get('/auth.py').then(function(response) { + authToken = response.headers('A-Token'); + $scope.user = response.data; + }).catch(function() { + $scope.status = 'Failed...'; }); $scope.saveMessage = function(message) { var headers = { 'Authorization': authToken }; $scope.status = 'Saving...'; - $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) { + $http.post('/add-msg.py', message, { headers: headers } ).then(function(response) { $scope.status = ''; - }).error(function() { + }).catch(function() { $scope.status = 'Failed...'; }); }; @@ -1203,18 +1388,97 @@ angular.mock.dump = function(object) { $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { // check if the header was sent, if it wasn't the expectation won't // match the request and the test will fail - return headers['Authorization'] == 'xxx'; + return headers['Authorization'] === 'xxx'; }).respond(201, ''); $rootScope.saveMessage('whatever'); $httpBackend.flush(); }); }); - ``` + ``` + * + * ## Dynamic responses + * + * You define a response to a request by chaining a call to `respond()` onto a definition or expectation. + * If you provide a **callback** as the first parameter to `respond(callback)` then you can dynamically generate + * a response based on the properties of the request. + * + * The `callback` function should be of the form `function(method, url, data, headers, params)`. + * + * ### Query parameters + * + * By default, query parameters on request URLs are parsed into the `params` object. So a request URL + * of `/list?q=searchstr&orderby=-name` would set `params` to be `{q: 'searchstr', orderby: '-name'}`. + * + * ### Regex parameter matching + * + * If an expectation or definition uses a **regex** to match the URL, you can provide an array of **keys** via a + * `params` argument. The index of each **key** in the array will match the index of a **group** in the + * **regex**. + * + * The `params` object in the **callback** will now have properties with these keys, which hold the value of the + * corresponding **group** in the **regex**. + * + * This also applies to the `when` and `expect` shortcut methods. + * + * + * ```js + * $httpBackend.expect('GET', /\/user\/(.+)/, undefined, undefined, ['id']) + * .respond(function(method, url, data, headers, params) { + * // for requested url of '/user/1234' params is {id: '1234'} + * }); + * + * $httpBackend.whenPATCH(/\/user\/(.+)\/article\/(.+)/, undefined, undefined, ['user', 'article']) + * .respond(function(method, url, data, headers, params) { + * // for url of '/user/1234/article/567' params is {user: '1234', article: '567'} + * }); + * ``` + * + * ## Matching route requests + * + * For extra convenience, `whenRoute` and `expectRoute` shortcuts are available. These methods offer colon + * delimited matching of the url path, ignoring the query string and trailing slashes. This allows declarations + * similar to how application routes are configured with `$routeProvider`. Because these methods convert + * the definition url to regex, declaration order is important. Combined with query parameter parsing, + * the following is possible: + * + ```js + $httpBackend.whenRoute('GET', '/users/:id') + .respond(function(method, url, data, headers, params) { + return [200, MockUserList[Number(params.id)]]; + }); + + $httpBackend.whenRoute('GET', '/users') + .respond(function(method, url, data, headers, params) { + var userList = angular.copy(MockUserList), + defaultSort = 'lastName', + count, pages, isPrevious, isNext; + + // paged api response '/v1/users?page=2' + params.page = Number(params.page) || 1; + + // query for last names '/v1/users?q=Archer' + if (params.q) { + userList = $filter('filter')({lastName: params.q}); + } + + pages = Math.ceil(userList.length / pagingLength); + isPrevious = params.page > 1; + isNext = params.page < pages; + + return [200, { + count: userList.length, + previous: isPrevious, + next: isNext, + // sort field -> '/v1/users?sortBy=firstName' + results: $filter('orderBy')(userList, params.sortBy || defaultSort) + .splice((params.page - 1) * pagingLength, pagingLength) + }]; + }); + ``` */ -angular.mock.$HttpBackendProvider = function() { - this.$get = ['$rootScope', '$timeout', createHttpBackendMock]; -}; +angular.mock.$httpBackendDecorator = + ['$rootScope', '$timeout', '$delegate', createHttpBackendMock]; /** * General factory function for $httpBackend mock. @@ -1233,27 +1497,34 @@ angular.mock.$HttpBackendProvider = function() { function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { var definitions = [], expectations = [], + matchLatestDefinition = false, responses = [], responsesPush = angular.bind(responses, responses.push), - copy = angular.copy; + copy = angular.copy, + // We cache the original backend so that if both ngMock and ngMockE2E override the + // service the ngMockE2E version can pass through to the real backend + originalHttpBackend = $delegate.$$originalHttpBackend || $delegate; function createResponse(status, data, headers, statusText) { if (angular.isFunction(status)) return status; return function() { return angular.isNumber(status) - ? [status, data, headers, statusText] - : [200, status, data, headers]; + ? [status, data, headers, statusText, 'complete'] + : [200, status, data, headers, 'complete']; }; } // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType) { + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { var xhr = new MockXhr(), expectation = expectations[0], wasExpected = false; + xhr.$$events = eventHandlers; + xhr.upload.$$events = uploadEventHandlers; + function prettyPrint(data) { return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) ? data @@ -1262,39 +1533,57 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { function wrapResponse(wrapped) { if (!$browser && timeout) { - timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout); + if (timeout.then) { + timeout.then(function() { + handlePrematureEnd(angular.isDefined(timeout.$$timeoutId) ? 'timeout' : 'abort'); + }); + } else { + $timeout(function() { + handlePrematureEnd('timeout'); + }, timeout); + } } + handleResponse.description = method + ' ' + url; return handleResponse; function handleResponse() { - var response = wrapped.response(method, url, data, headers); + var response = wrapped.response(method, url, data, headers, wrapped.params(url)); xhr.$$respHeaders = response[2]; callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), - copy(response[3] || '')); + copy(response[3] || ''), copy(response[4])); } - function handleTimeout() { + function handlePrematureEnd(reason) { for (var i = 0, ii = responses.length; i < ii; i++) { if (responses[i] === handleResponse) { responses.splice(i, 1); - callback(-1, undefined, ''); + callback(-1, undefined, '', undefined, reason); break; } } } } + function createFatalError(message) { + var error = new Error(message); + // In addition to being converted to a rejection, these errors also need to be passed to + // the $exceptionHandler and be rethrown (so that the test fails). + error.$$passToExceptionHandler = true; + return error; + } + if (expectation && expectation.match(method, url)) { if (!expectation.matchData(data)) { - throw new Error('Expected ' + expectation + ' with different data\n' + - 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + throw createFatalError('Expected ' + expectation + ' with different data\n' + + 'EXPECTED: ' + prettyPrint(expectation.data) + '\n' + + 'GOT: ' + data); } if (!expectation.matchHeaders(headers)) { - throw new Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + - prettyPrint(headers)); + throw createFatalError('Expected ' + expectation + ' with different headers\n' + + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\n' + + 'GOT: ' + prettyPrint(headers)); } expectations.shift(); @@ -1306,22 +1595,26 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { wasExpected = true; } - var i = -1, definition; - while ((definition = definitions[++i])) { + var i = matchLatestDefinition ? definitions.length : -1, definition; + + while ((definition = definitions[matchLatestDefinition ? --i : ++i])) { if (definition.match(method, url, data, headers || {})) { if (definition.response) { // if $browser specified, we do auto flush all requests ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout, withCredentials, responseType); - } else throw new Error('No response defined !'); + originalHttpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers); + } else throw createFatalError('No response defined !'); return; } } - throw wasExpected ? - new Error('No response defined !') : - new Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); + + if (wasExpected) { + throw createFatalError('No response defined !'); + } + + throw createFatalError('Unexpected request: ' + method + ' ' + url + '\n' + + (expectation ? 'Expected ' + expectation : 'No more request expected')); } /** @@ -1331,26 +1624,33 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. * * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` + * ```js + * {function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). The respond method returns the - * `requestHandler` object for possible overrides. + * return an array containing response status (number), response data (Array|Object|string), + * response headers (Object), HTTP status text (string), and XMLHttpRequest status (string: + * `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler` + * object for possible overrides. */ - $httpBackend.when = function(method, url, data, headers) { - var definition = new MockHttpExpectation(method, url, data, headers), + $httpBackend.when = function(method, url, data, headers, keys) { + + assertArgDefined(arguments, 1, 'url'); + + var definition = new MockHttpExpectation(method, url, data, headers, keys), chain = { respond: function(status, data, headers, statusText) { definition.passThrough = undefined; @@ -1371,15 +1671,58 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { return chain; }; + /** + * @ngdoc method + * @name $httpBackend#matchLatestDefinitionEnabled + * @description + * This method can be used to change which mocked responses `$httpBackend` returns, when defining + * them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods). + * By default, `$httpBackend` returns the first definition that matches. When setting + * `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the + * one that was added last. + * + * ```js + * hb.when('GET', '/url1').respond(200, 'content', {}); + * hb.when('GET', '/url1').respond(201, 'another', {}); + * hb('GET', '/url1'); // receives "content" + * + * $http.matchLatestDefinitionEnabled(true) + * hb('GET', '/url1'); // receives "another" + * + * hb.when('GET', '/url1').respond(201, 'onemore', {}); + * hb('GET', '/url1'); // receives "onemore" + * ``` + * + * This is useful if a you have a default response that is overriden inside specific tests. + * + * Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed + * even when the application is already running. + * + * @param {Boolean=} value value to set, either `true` or `false`. Default is `false`. + * If omitted, it will return the current value. + * @return {$httpBackend|Boolean} self when used as a setter, and the current value when used + * as a getter + */ + $httpBackend.matchLatestDefinitionEnabled = function(value) { + if (angular.isDefined(value)) { + matchLatestDefinition = value; + return this; + } else { + return matchLatestDefinition; + } + }; + /** * @ngdoc method * @name $httpBackend#whenGET * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1391,9 +1734,11 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1405,9 +1750,11 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1419,11 +1766,13 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1435,11 +1784,13 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1451,14 +1802,32 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ createShortMethods('when'); + /** + * @ngdoc method + * @name $httpBackend#whenRoute + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * See {@link ngMock.$httpBackend#when `when`} for more info. + */ + $httpBackend.whenRoute = function(method, url) { + var parsed = parseRouteUrl(url); + return $httpBackend.when(method, parsed.regexp, undefined, undefined, parsed.keys); + }; /** * @ngdoc method @@ -1467,27 +1836,34 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * Creates a new request expectation. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. * * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` + * ```js + * {function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). The respond method returns the - * `requestHandler` object for possible overrides. + * return an array containing response status (number), response data (Array|Object|string), + * response headers (Object), HTTP status text (string), and XMLHttpRequest status (string: + * `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler` + * object for possible overrides. */ - $httpBackend.expect = function(method, url, data, headers) { - var expectation = new MockHttpExpectation(method, url, data, headers), + $httpBackend.expect = function(method, url, data, headers, keys) { + + assertArgDefined(arguments, 1, 'url'); + + var expectation = new MockHttpExpectation(method, url, data, headers, keys), chain = { respond: function(status, data, headers, statusText) { expectation.response = createResponse(status, data, headers, statusText); @@ -1499,16 +1875,17 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { return chain; }; - /** * @ngdoc method * @name $httpBackend#expectGET * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {Object=} headers HTTP headers. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. See #expect for more info. @@ -1520,9 +1897,11 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {Object=} headers HTTP headers. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1534,9 +1913,11 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {Object=} headers HTTP headers. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1548,12 +1929,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. - * @param {Object=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1565,12 +1948,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. - * @param {Object=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1582,12 +1967,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. - * @param {Object=} headers HTTP headers. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1599,37 +1986,66 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives an url - * and returns true if the url matches the current definition. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives an url + * and returns true if the url matches the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ createShortMethods('expect'); + /** + * @ngdoc method + * @name $httpBackend#expectRoute + * @description + * Creates a new request expectation that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * See {@link ngMock.$httpBackend#expect `expect`} for more info. + */ + $httpBackend.expectRoute = function(method, url) { + var parsed = parseRouteUrl(url); + return $httpBackend.expect(method, parsed.regexp, undefined, undefined, parsed.keys); + }; + /** * @ngdoc method * @name $httpBackend#flush * @description - * Flushes all pending requests using the trained responses. + * Flushes pending requests using the trained responses. Requests are flushed in the order they + * were made, but it is also possible to skip one or more requests (for example to have them + * flushed later). This is useful for simulating scenarios where responses arrive from the server + * in any order. + * + * If there are no pending requests to flush when the method is called, an exception is thrown (as + * this is typically a sign of programming error). * - * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, - * all pending requests will be flushed. If there are no pending requests when the flush method - * is called an exception is thrown (as this typically a sign of programming error). + * @param {number=} count - Number of responses to flush. If undefined/null, all pending requests + * (starting after `skip`) will be flushed. + * @param {number=} [skip=0] - Number of pending requests to skip. For example, a value of `5` + * would skip the first 5 pending requests and start flushing from the 6th onwards. */ - $httpBackend.flush = function(count, digest) { + $httpBackend.flush = function(count, skip, digest) { if (digest !== false) $rootScope.$digest(); - if (!responses.length) throw new Error('No pending request to flush !'); + + skip = skip || 0; + if (skip >= responses.length) throw new Error('No pending request to flush !'); if (angular.isDefined(count) && count !== null) { while (count--) { - if (!responses.length) throw new Error('No more pending request to flush !'); - responses.shift()(); + var part = responses.splice(skip, 1); + if (!part.length) throw new Error('No more pending request to flush !'); + part[0](); } } else { - while (responses.length) { - responses.shift()(); + while (responses.length > skip) { + responses.splice(skip, 1)[0](); } } $httpBackend.verifyNoOutstandingExpectation(digest); @@ -1671,9 +2087,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * afterEach($httpBackend.verifyNoOutstandingRequest); * ``` */ - $httpBackend.verifyNoOutstandingRequest = function() { + $httpBackend.verifyNoOutstandingRequest = function(digest) { + if (digest !== false) $rootScope.$digest(); if (responses.length) { - throw new Error('Unflushed requests: ' + responses.length); + var unflushedDescriptions = responses.map(function(res) { return res.description; }); + throw new Error('Unflushed requests: ' + responses.length + '\n ' + + unflushedDescriptions.join('\n ')); } }; @@ -1691,83 +2110,186 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { responses.length = 0; }; + $httpBackend.$$originalHttpBackend = originalHttpBackend; + return $httpBackend; function createShortMethods(prefix) { angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { - $httpBackend[prefix + method] = function(url, headers) { - return $httpBackend[prefix](method, url, undefined, headers); + $httpBackend[prefix + method] = function(url, headers, keys) { + assertArgDefined(arguments, 0, 'url'); + + // Change url to `null` if `undefined` to stop it throwing an exception further down + if (angular.isUndefined(url)) url = null; + + return $httpBackend[prefix](method, url, undefined, headers, keys); }; }); angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { - $httpBackend[prefix + method] = function(url, data, headers) { - return $httpBackend[prefix](method, url, data, headers); + $httpBackend[prefix + method] = function(url, data, headers, keys) { + assertArgDefined(arguments, 0, 'url'); + + // Change url to `null` if `undefined` to stop it throwing an exception further down + if (angular.isUndefined(url)) url = null; + + return $httpBackend[prefix](method, url, data, headers, keys); }; }); } + + function parseRouteUrl(url) { + var strippedUrl = stripQueryAndHash(url); + var parseOptions = {caseInsensitiveMatch: true, ignoreTrailingSlashes: true}; + return routeToRegExp(strippedUrl, parseOptions); + } +} + +function assertArgDefined(args, index, name) { + if (args.length > index && angular.isUndefined(args[index])) { + throw new Error('Undefined argument `' + name + '`; the argument is provided but not defined'); + } +} + +function stripQueryAndHash(url) { + return url.replace(/[?#].*$/, ''); } -function MockHttpExpectation(method, url, data, headers) { +function MockHttpExpectation(expectedMethod, expectedUrl, expectedData, expectedHeaders, + expectedKeys) { - this.data = data; - this.headers = headers; + this.data = expectedData; + this.headers = expectedHeaders; - this.match = function(m, u, d, h) { - if (method != m) return false; - if (!this.matchUrl(u)) return false; - if (angular.isDefined(d) && !this.matchData(d)) return false; - if (angular.isDefined(h) && !this.matchHeaders(h)) return false; + this.match = function(method, url, data, headers) { + if (expectedMethod !== method) return false; + if (!this.matchUrl(url)) return false; + if (angular.isDefined(data) && !this.matchData(data)) return false; + if (angular.isDefined(headers) && !this.matchHeaders(headers)) return false; return true; }; - this.matchUrl = function(u) { - if (!url) return true; - if (angular.isFunction(url.test)) return url.test(u); - if (angular.isFunction(url)) return url(u); - return url == u; + this.matchUrl = function(url) { + if (!expectedUrl) return true; + if (angular.isFunction(expectedUrl.test)) return expectedUrl.test(url); + if (angular.isFunction(expectedUrl)) return expectedUrl(url); + return (expectedUrl === url || compareUrlWithQuery(url)); }; - this.matchHeaders = function(h) { - if (angular.isUndefined(headers)) return true; - if (angular.isFunction(headers)) return headers(h); - return angular.equals(headers, h); + this.matchHeaders = function(headers) { + if (angular.isUndefined(expectedHeaders)) return true; + if (angular.isFunction(expectedHeaders)) return expectedHeaders(headers); + return angular.equals(expectedHeaders, headers); }; - this.matchData = function(d) { - if (angular.isUndefined(data)) return true; - if (data && angular.isFunction(data.test)) return data.test(d); - if (data && angular.isFunction(data)) return data(d); - if (data && !angular.isString(data)) { - return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d)); + this.matchData = function(data) { + if (angular.isUndefined(expectedData)) return true; + if (expectedData && angular.isFunction(expectedData.test)) return expectedData.test(data); + if (expectedData && angular.isFunction(expectedData)) return expectedData(data); + if (expectedData && !angular.isString(expectedData)) { + return angular.equals(angular.fromJson(angular.toJson(expectedData)), angular.fromJson(data)); } - return data == d; + // eslint-disable-next-line eqeqeq + return expectedData == data; }; this.toString = function() { - return method + ' ' + url; + return expectedMethod + ' ' + expectedUrl; }; -} -function createMockXhr() { - return new MockXhr(); -} + this.params = function(url) { + var queryStr = url.indexOf('?') === -1 ? '' : url.substring(url.indexOf('?') + 1); + var strippedUrl = stripQueryAndHash(url); -function MockXhr() { + return angular.extend(extractParamsFromQuery(queryStr), extractParamsFromPath(strippedUrl)); + }; - // hack for testing $http, $httpBackend - MockXhr.$$lastInstance = this; + function compareUrlWithQuery(url) { + var urlWithQueryRe = /^([^?]*)\?(.*)$/; - this.open = function(method, url, async) { - this.$$method = method; - this.$$url = url; - this.$$async = async; - this.$$reqHeaders = {}; - this.$$respHeaders = {}; - }; + var expectedMatch = urlWithQueryRe.exec(expectedUrl); + var actualMatch = urlWithQueryRe.exec(url); - this.send = function(data) { + return !!(expectedMatch && actualMatch) && + (expectedMatch[1] === actualMatch[1]) && + (normalizeQuery(expectedMatch[2]) === normalizeQuery(actualMatch[2])); + } + + function normalizeQuery(queryStr) { + return queryStr.split('&').sort().join('&'); + } + + function extractParamsFromPath(strippedUrl) { + var keyObj = {}; + + if (!expectedUrl || !angular.isFunction(expectedUrl.test) || + !expectedKeys || !expectedKeys.length) return keyObj; + + var match = expectedUrl.exec(strippedUrl); + if (!match) return keyObj; + + for (var i = 1, len = match.length; i < len; ++i) { + var key = expectedKeys[i - 1]; + var val = match[i]; + if (key && val) { + keyObj[key.name || key] = val; + } + } + + return keyObj; + } + + function extractParamsFromQuery(queryStr) { + var obj = {}, + keyValuePairs = queryStr.split('&'). + filter(angular.identity). // Ignore empty segments. + map(function(keyValue) { return keyValue.replace(/\+/g, '%20').split('='); }); + + angular.forEach(keyValuePairs, function(pair) { + var key = tryDecodeURIComponent(pair[0]); + if (angular.isDefined(key)) { + var val = angular.isDefined(pair[1]) ? tryDecodeURIComponent(pair[1]) : true; + if (!hasOwnProperty.call(obj, key)) { + obj[key] = val; + } else if (angular.isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key], val]; + } + } + }); + + return obj; + } + + function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch (e) { + // Ignore any invalid uri component + } + } +} + +function createMockXhr() { + return new MockXhr(); +} + +function MockXhr() { + + // hack for testing $http, $httpBackend + MockXhr.$$lastInstance = this; + + this.open = function(method, url, async) { + this.$$method = method; + this.$$url = url; + this.$$async = async; + this.$$reqHeaders = {}; + this.$$respHeaders = {}; + }; + + this.send = function(data) { this.$$data = data; }; @@ -1781,13 +2303,13 @@ function MockXhr() { var header = this.$$respHeaders[name]; if (header) return header; - name = angular.lowercase(name); + name = angular.$$lowercase(name); header = this.$$respHeaders[name]; if (header) return header; header = undefined; angular.forEach(this.$$respHeaders, function(headerVal, headerName) { - if (!header && angular.lowercase(headerName) == name) header = headerVal; + if (!header && angular.$$lowercase(headerName) === name) header = headerVal; }); return header; }; @@ -1801,7 +2323,25 @@ function MockXhr() { return lines.join('\n'); }; - this.abort = angular.noop; + this.abort = function() { + if (isFunction(this.onabort)) { + this.onabort(); + } + }; + + // This section simulates the events on a real XHR object (and the upload object) + // When we are testing $httpBackend (inside the AngularJS project) we make partial use of this + // but store the events directly ourselves on `$$events`, instead of going through the `addEventListener` + this.$$events = {}; + this.addEventListener = function(name, listener) { + if (angular.isUndefined(this.$$events[name])) this.$$events[name] = []; + this.$$events[name].push(listener); + }; + + this.upload = { + $$events: {}, + addEventListener: this.addEventListener + }; } @@ -1819,39 +2359,86 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $ /** * @ngdoc method * @name $timeout#flush + * + * @deprecated + * sinceVersion="1.7.3" + * + * This method flushes all types of tasks (not only timeouts), which is unintuitive. + * It is recommended to use {@link ngMock.$flushPendingTasks} instead. + * * @description * * Flushes the queue of pending tasks. * + * _This method is essentially an alias of {@link ngMock.$flushPendingTasks}._ + * + *
+ * For historical reasons, this method will also flush non-`$timeout` pending tasks, such as + * {@link $q} promises and tasks scheduled via + * {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and + * {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + *
+ * * @param {number=} delay maximum timeout amount to flush up until */ $delegate.flush = function(delay) { + // For historical reasons, `$timeout.flush()` flushes all types of pending tasks. + // Keep the same behavior for backwards compatibility (and because it doesn't make sense to + // selectively flush scheduled events out of order). $browser.defer.flush(delay); }; /** * @ngdoc method * @name $timeout#verifyNoPendingTasks + * + * @deprecated + * sinceVersion="1.7.3" + * + * This method takes all types of tasks (not only timeouts) into account, which is unintuitive. + * It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally + * allows checking for timeouts only (with `$verifyNoPendingTasks('$timeout')`). + * * @description * - * Verifies that there are no pending tasks that need to be flushed. + * Verifies that there are no pending tasks that need to be flushed. It throws an error if there + * are still pending tasks. + * + * _This method is essentially an alias of {@link ngMock.$verifyNoPendingTasks} (called with no + * arguments)._ + * + *
+ *

+ * For historical reasons, this method will also verify non-`$timeout` pending tasks, such as + * pending {@link $http} requests, in-progress {@link $route} transitions, unresolved + * {@link $q} promises and tasks scheduled via + * {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and + * {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + *

+ *

+ * It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally + * supports verifying a specific type of tasks. For example, you can verify there are no + * pending timeouts with `$verifyNoPendingTasks('$timeout')`. + *

+ *
*/ $delegate.verifyNoPendingTasks = function() { - if ($browser.deferredFns.length) { - throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + - formatPendingTasksAsString($browser.deferredFns)); + // For historical reasons, `$timeout.verifyNoPendingTasks()` takes all types of pending tasks + // into account. Keep the same behavior for backwards compatibility. + var pendingTasks = $browser.defer.getPendingTasks(); + + if (pendingTasks.length) { + var formattedTasks = $browser.defer.formatPendingTasks(pendingTasks).join('\n '); + var hasPendingTimeout = pendingTasks.some(function(task) { return task.type === '$timeout'; }); + var extraMessage = hasPendingTimeout ? '' : '\n\nNone of the pending tasks are timeouts. ' + + 'If you only want to verify pending timeouts, use ' + + '`$verifyNoPendingTasks(\'$timeout\')` instead.'; + + throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' + + formattedTasks + extraMessage); } }; - function formatPendingTasksAsString(tasks) { - var result = []; - angular.forEach(tasks, function(task) { - result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); - }); - - return result.join(', '); - } - return $delegate; }]; @@ -1886,10 +2473,12 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) { /** * */ +var originalRootElement; angular.mock.$RootElementProvider = function() { - this.$get = function() { - return angular.element('
'); - }; + this.$get = ['$injector', function($injector) { + originalRootElement = angular.element('
').data('$injector', $injector); + return originalRootElement; + }]; }; /** @@ -1899,7 +2488,6 @@ angular.mock.$RootElementProvider = function() { * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. * - * * ## Example * * ```js @@ -1917,18 +2505,24 @@ angular.mock.$RootElementProvider = function() { * // Controller definition ... * * myMod.controller('MyDirectiveController', ['$log', function($log) { - * $log.info(this.name); + * this.log = function() { + * $log.info(this.name); + * }; * }]); * * * // In a test ... * * describe('myDirectiveController', function() { - * it('should write the bound name to the log', inject(function($controller, $log) { - * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); - * expect(ctrl.name).toEqual('Clark Kent'); - * expect($log.info.logs).toEqual(['Clark Kent']); - * })); + * describe('log()', function() { + * it('should write the bound name to the log', inject(function($controller, $log) { + * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); + * ctrl.log(); + * + * expect(ctrl.name).toEqual('Clark Kent'); + * expect($log.info.logs).toEqual(['Clark Kent']); + * })); + * }); * }); * * ``` @@ -1939,27 +2533,84 @@ angular.mock.$RootElementProvider = function() { * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * 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. + * @param {Object=} bindings Properties to add to the controller instance. This is used to simulate + * the `bindToController` feature and simplify certain kinds of tests. + * @return {Object} Instance of given controller. + */ +function createControllerDecorator() { + angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var instantiate = $delegate(expression, locals, true, ident); + var instance = instantiate(); + angular.extend(instance, later); + return instance; + } + return $delegate(expression, locals, later, ident); + }; + }]; + + return angular.mock.$ControllerDecorator; +} + +/** + * @ngdoc service + * @name $componentController + * @description + * A service that can be used to create instances of component controllers. Useful for unit-testing. + * + * Be aware that the controller will be instantiated and attached to the scope as specified in + * the component definition object. If you do not provide a `$scope` object in the `locals` param + * then the helper will create a new isolated scope as a child of `$rootScope`. + * + * If you are using `$element` or `$attrs` in the controller, make sure to provide them as `locals`. + * The `$element` must be a jqLite-wrapped DOM element, and `$attrs` should be an object that + * has all properties / functions that you are using in the controller. If this is getting too complex, + * you should compile the component instead and access the component's controller via the + * {@link angular.element#methods `controller`} function. + * + * See also the section on {@link guide/component#unit-testing-component-controllers unit-testing component controllers} + * in the guide. + * + * @param {string} componentName the name of the component whose controller we want to instantiate + * @param {Object} locals Injection locals for Controller. * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used * to simulate the `bindToController` feature and simplify certain kinds of tests. - * @return {Object} Instance of given controller. + * @param {string=} ident Override the property name to use when attaching the controller to the scope. + * @return {Object} Instance of requested controller. */ -angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { - return function(expression, locals, later, ident) { - if (later && typeof later === 'object') { - var create = $delegate(expression, locals, true, ident); - angular.extend(create.instance, later); - return create(); - } - return $delegate(expression, locals, later, ident); - }; +angular.mock.$ComponentControllerProvider = ['$compileProvider', + function ComponentControllerProvider($compileProvider) { + this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) { + return function $componentController(componentName, locals, bindings, ident) { + // get all directives associated to the component name + var directives = $injector.get(componentName + 'Directive'); + // look for those directives that are components + var candidateDirectives = directives.filter(function(directiveInfo) { + // components have controller, controllerAs and restrict:'E' + return directiveInfo.controller && directiveInfo.controllerAs && directiveInfo.restrict === 'E'; + }); + // check if valid directives found + if (candidateDirectives.length === 0) { + throw new Error('No component found'); + } + if (candidateDirectives.length > 1) { + throw new Error('Too many components found'); + } + // get the info of the component + var directiveInfo = candidateDirectives[0]; + // create a scope if needed + locals = locals || {}; + locals.$scope = locals.$scope || $rootScope.$new(true); + return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs); + }; + }]; }]; @@ -1969,29 +2620,56 @@ angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { * @packageName angular-mocks * @description * - * # ngMock - * - * The `ngMock` module provides support to inject and mock Angular services into unit tests. - * In addition, ngMock also extends various core ng services such that they can be + * The `ngMock` module provides support to inject and mock AngularJS services into unit tests. + * In addition, ngMock also extends various core AngularJS services such that they can be * inspected and controlled in a synchronous manner within test code. * + * @installation * - *
+ * First, download the file: + * * [Google CDN](https://developers.google.com/speed/libraries/devguide#angularjs) e.g. + * `"//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/angular-mocks.js"` + * * [NPM](https://www.npmjs.com/) e.g. `npm install angular-mocks@X.Y.Z` + * * [Yarn](https://yarnpkg.com) e.g. `yarn add angular-mocks@X.Y.Z` + * * [Bower](http://bower.io) e.g. `bower install angular-mocks#X.Y.Z` + * * [code.angularjs.org](https://code.angularjs.org/) (discouraged for production use) e.g. + * `"//code.angularjs.org/X.Y.Z/angular-mocks.js"` + * + * where X.Y.Z is the AngularJS version you are running. + * + * Then, configure your test runner to load `angular-mocks.js` after `angular.js`. + * This example uses Karma: + * + * ``` + * config.set({ + * files: [ + * 'build/angular.js', // and other module files you need + * 'build/angular-mocks.js', + * '', + * '' + * ] + * }); + * ``` * + * Including the `angular-mocks.js` file automatically adds the `ngMock` module, so your tests + * are ready to go! */ angular.module('ngMock', ['ng']).provider({ $browser: angular.mock.$BrowserProvider, $exceptionHandler: angular.mock.$ExceptionHandlerProvider, $log: angular.mock.$LogProvider, $interval: angular.mock.$IntervalProvider, - $httpBackend: angular.mock.$HttpBackendProvider, - $rootElement: angular.mock.$RootElementProvider -}).config(['$provide', function($provide) { + $rootElement: angular.mock.$RootElementProvider, + $componentController: angular.mock.$ComponentControllerProvider, + $flushPendingTasks: angular.mock.$FlushPendingTasksProvider, + $verifyNoPendingTasks: angular.mock.$VerifyNoPendingTasksProvider +}).config(['$provide', '$compileProvider', function($provide, $compileProvider) { $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator); $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); - $provide.decorator('$controller', angular.mock.$ControllerDecorator); -}]); + $provide.decorator('$controller', createControllerDecorator($compileProvider)); + $provide.decorator('$httpBackend', angular.mock.$httpBackendDecorator); +}]).info({ angularVersion: '1.8.3' }); /** * @ngdoc module @@ -2000,13 +2678,13 @@ angular.module('ngMock', ['ng']).provider({ * @packageName angular-mocks * @description * - * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. + * The `ngMockE2E` is an AngularJS module which contains mocks suitable for end-to-end testing. * Currently there is only one mock present in this module - * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. */ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); -}]); +}]).info({ angularVersion: '1.8.3' }); /** * @ngdoc service @@ -2016,8 +2694,10 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of * applications that use the {@link ng.$http $http service}. * - * *Note*: For fake http backend implementation suitable for unit testing please see + *
+ * **Note**: For fake http backend implementation suitable for unit testing please see * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. + *
* * This implementation can be used to respond with static or dynamic responses via the `when` api * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the @@ -2038,9 +2718,9 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * on the `ngMockE2E` and your application modules and defines the fake backend: * * ```js - * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); + * var myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); * myAppDev.run(function($httpBackend) { - * phones = [{name: 'phone1'}, {name: 'phone2'}]; + * var phones = [{name: 'phone1'}, {name: 'phone2'}]; * * // returns the current list of phones * $httpBackend.whenGET('/phones').respond(phones); @@ -2051,12 +2731,74 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * phones.push(phone); * return [200, phone, {}]; * }); - * $httpBackend.whenGET(/^\/templates\//).passThrough(); + * $httpBackend.whenGET(/^\/templates\//).passThrough(); // Requests for templates are handled by the real server * //... * }); * ``` * * Afterwards, bootstrap your app with this new module. + * + * @example + * + * + * var myApp = angular.module('myApp', []); + * + * myApp.controller('MainCtrl', function MainCtrl($http) { + * var ctrl = this; + * + * ctrl.phones = []; + * ctrl.newPhone = { + * name: '' + * }; + * + * ctrl.getPhones = function() { + * $http.get('/phones').then(function(response) { + * ctrl.phones = response.data; + * }); + * }; + * + * ctrl.addPhone = function(phone) { + * $http.post('/phones', phone).then(function() { + * ctrl.newPhone = {name: ''}; + * return ctrl.getPhones(); + * }); + * }; + * + * ctrl.getPhones(); + * }); + * + * + * var myAppDev = angular.module('myAppE2E', ['myApp', 'ngMockE2E']); + * + * myAppDev.run(function($httpBackend) { + * var phones = [{name: 'phone1'}, {name: 'phone2'}]; + * + * // returns the current list of phones + * $httpBackend.whenGET('/phones').respond(phones); + * + * // adds a new phone to the phones array + * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { + * var phone = angular.fromJson(data); + * phones.push(phone); + * return [200, phone, {}]; + * }); + * }); + * + * + *
+ *
+ * + * + *
+ *

Phones

+ *
    + *
  • {{phone.name}}
  • + *
+ *
+ *
+ *
+ * + * */ /** @@ -2067,21 +2809,26 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. * * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` + * ``` + * { function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string), response headers - * (Object), and the text for the status (string). + * an array containing response status (number), response data (Array|Object|string), response + * headers (Object), and the text for the status (string). * - passThrough – `{function()}` – Any request matching a backend definition with * `passThrough` handler will be passed through to the real backend (an XHR request will be made * to the server.) @@ -2095,9 +2842,11 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. @@ -2110,9 +2859,11 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. @@ -2125,9 +2876,11 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. @@ -2140,10 +2893,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. @@ -2156,10 +2912,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. @@ -2172,10 +2931,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. @@ -2188,12 +2950,60 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. */ +/** + * @ngdoc method + * @name $httpBackend#whenRoute + * @module ngMockE2E + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ +/** + * @ngdoc method + * @name $httpBackend#matchLatestDefinitionEnabled + * @module ngMockE2E + * @description + * This method can be used to change which mocked responses `$httpBackend` returns, when defining + * them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods). + * By default, `$httpBackend` returns the first definition that matches. When setting + * `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the + * one that was added last. + * + * ```js + * hb.when('GET', '/url1').respond(200, 'content', {}); + * hb.when('GET', '/url1').respond(201, 'another', {}); + * hb('GET', '/url1'); // receives "content" + * + * $http.matchLatestDefinitionEnabled(true) + * hb('GET', '/url1'); // receives "another" + * + * hb.when('GET', '/url1').respond(201, 'onemore', {}); + * hb('GET', '/url1'); // receives "onemore" + * ``` + * + * This is useful if a you have a default response that is overriden inside specific tests. + * + * Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed + * even when the application is already running. + * + * @param {Boolean=} value value to set, either `true` or `false`. Default is `false`. + * If omitted, it will return the current value. + * @return {$httpBackend|Boolean} self when used as a setter, and the current value when used + * as a getter + */ angular.mock.e2e = {}; angular.mock.e2e.$httpBackendDecorator = ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; @@ -2225,6 +3035,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { * @ngdoc method * @name $rootScope.Scope#$countChildScopes * @module ngMock + * @this $rootScope.Scope * @description * Counts all the direct and indirect child scopes of the current scope. * @@ -2233,7 +3044,6 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { * @returns {number} Total number of child scopes. */ function countChildScopes() { - // jshint validthis: true var count = 0; // exclude the current scope var pendingChildHeads = [this.$$childHead]; var currentScope; @@ -2255,6 +3065,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { /** * @ngdoc method * @name $rootScope.Scope#$countWatchers + * @this $rootScope.Scope * @module ngMock * @description * Counts all the watchers of direct and indirect child scopes of the current scope. @@ -2265,7 +3076,6 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { * @returns {number} Total number of watchers. */ function countWatchers() { - // jshint validthis: true var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope var pendingChildHeads = [this.$$childHead]; var currentScope; @@ -2285,11 +3095,16 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { }]; -if (window.jasmine || window.mocha) { +(function(jasmineOrMocha) { + + if (!jasmineOrMocha) { + return; + } var currentSpec = null, + injectorState = new InjectorState(), annotatedFunctions = [], - isSpecRunning = function() { + wasInjectorCreated = function() { return !!currentSpec; }; @@ -2301,46 +3116,6 @@ if (window.jasmine || window.mocha) { return angular.mock.$$annotate.apply(this, arguments); }; - - (window.beforeEach || window.setup)(function() { - annotatedFunctions = []; - currentSpec = this; - }); - - (window.afterEach || window.teardown)(function() { - var injector = currentSpec.$injector; - - annotatedFunctions.forEach(function(fn) { - delete fn.$inject; - }); - - angular.forEach(currentSpec.$modules, function(module) { - if (module && module.$$hashKey) { - module.$$hashKey = undefined; - } - }); - - currentSpec.$injector = null; - currentSpec.$modules = null; - currentSpec = null; - - if (injector) { - injector.get('$rootElement').off(); - } - - // clean up jquery's fragment cache - angular.forEach(angular.element.fragments, function(val, key) { - delete angular.element.fragments[key]; - }); - - MockXhr.$$lastInstance = null; - - angular.forEach(angular.callbacks, function(val, key) { - delete angular.callbacks[key]; - }); - angular.callbacks.counter = 0; - }); - /** * @ngdoc function * @name angular.mock.module @@ -2361,30 +3136,188 @@ if (window.jasmine || window.mocha) { * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate * with the value on the injector. */ - window.module = angular.mock.module = function() { + var module = window.module = angular.mock.module = function() { var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; + return wasInjectorCreated() ? workFn() : workFn; ///////////////////// function workFn() { if (currentSpec.$injector) { throw new Error('Injector already created, can not register a module!'); } else { - var modules = currentSpec.$modules || (currentSpec.$modules = []); + var fn, modules = currentSpec.$modules || (currentSpec.$modules = []); angular.forEach(moduleFns, function(module) { if (angular.isObject(module) && !angular.isArray(module)) { - modules.push(function($provide) { + fn = ['$provide', function($provide) { angular.forEach(module, function(value, key) { $provide.value(key, value); }); - }); + }]; } else { - modules.push(module); + fn = module; + } + if (currentSpec.$providerInjector) { + currentSpec.$providerInjector.invoke(fn); + } else { + modules.push(fn); } }); } } }; + module.$$beforeAllHook = (window.before || window.beforeAll); + module.$$afterAllHook = (window.after || window.afterAll); + + // purely for testing ngMock itself + module.$$currentSpec = function(to) { + if (arguments.length === 0) return to; + currentSpec = to; + }; + + /** + * @ngdoc function + * @name angular.mock.module.sharedInjector + * @description + * + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function ensures a single injector will be used for all tests in a given describe context. + * This contrasts with the default behaviour where a new injector is created per test case. + * + * Use sharedInjector when you want to take advantage of Jasmine's `beforeAll()`, or mocha's + * `before()` methods. Call `module.sharedInjector()` before you setup any other hooks that + * will create (i.e call `module()`) or use (i.e call `inject()`) the injector. + * + * You cannot call `sharedInjector()` from within a context already using `sharedInjector()`. + * + * ## Example + * + * Typically beforeAll is used to make many assertions about a single operation. This can + * cut down test run-time as the test setup doesn't need to be re-run, and enabling focussed + * tests each with a single assertion. + * + * ```js + * describe("Deep Thought", function() { + * + * module.sharedInjector(); + * + * beforeAll(module("UltimateQuestion")); + * + * beforeAll(inject(function(DeepThought) { + * expect(DeepThought.answer).toBeUndefined(); + * DeepThought.generateAnswer(); + * })); + * + * it("has calculated the answer correctly", inject(function(DeepThought) { + * // Because of sharedInjector, we have access to the instance of the DeepThought service + * // that was provided to the beforeAll() hook. Therefore we can test the generated answer + * expect(DeepThought.answer).toBe(42); + * })); + * + * it("has calculated the answer within the expected time", inject(function(DeepThought) { + * expect(DeepThought.runTimeMillennia).toBeLessThan(8000); + * })); + * + * it("has double checked the answer", inject(function(DeepThought) { + * expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true); + * })); + * + * }); + * + * ``` + */ + module.sharedInjector = function() { + if (!(module.$$beforeAllHook && module.$$afterAllHook)) { + throw Error('sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll'); + } + + var initialized = false; + + module.$$beforeAllHook(/** @this */ function() { + if (injectorState.shared) { + injectorState.sharedError = Error('sharedInjector() cannot be called inside a context that has already called sharedInjector()'); + throw injectorState.sharedError; + } + initialized = true; + currentSpec = this; + injectorState.shared = true; + }); + + module.$$afterAllHook(function() { + if (initialized) { + injectorState = new InjectorState(); + module.$$cleanup(); + } else { + injectorState.sharedError = null; + } + }); + }; + + module.$$beforeEach = function() { + if (injectorState.shared && currentSpec && currentSpec !== this) { + var state = currentSpec; + currentSpec = this; + angular.forEach(['$injector','$modules','$providerInjector', '$injectorStrict'], function(k) { + currentSpec[k] = state[k]; + state[k] = null; + }); + } else { + currentSpec = this; + originalRootElement = null; + annotatedFunctions = []; + } + }; + + module.$$afterEach = function() { + if (injectorState.cleanupAfterEach()) { + module.$$cleanup(); + } + }; + + module.$$cleanup = function() { + var injector = currentSpec.$injector; + + annotatedFunctions.forEach(function(fn) { + delete fn.$inject; + }); + + currentSpec.$injector = null; + currentSpec.$modules = null; + currentSpec.$providerInjector = null; + currentSpec = null; + + if (injector) { + // Ensure `$rootElement` is instantiated, before checking `originalRootElement` + var $rootElement = injector.get('$rootElement'); + var rootNode = $rootElement && $rootElement[0]; + var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]]; + if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) { + cleanUpNodes.push(rootNode); + } + angular.element.cleanData(cleanUpNodes); + + // Ensure `$destroy()` is available, before calling it + // (a mocked `$rootScope` might not implement it (or not even be an object at all)) + var $rootScope = injector.get('$rootScope'); + if ($rootScope && $rootScope.$destroy) $rootScope.$destroy(); + } + + // clean up jquery's fragment cache + angular.forEach(angular.element.fragments, function(val, key) { + delete angular.element.fragments[key]; + }); + + MockXhr.$$lastInstance = null; + + angular.forEach(angular.callbacks, function(val, key) { + delete angular.callbacks[key]; + }); + angular.callbacks.$$counter = 0; + }; + + (window.beforeEach || window.setup)(module.$$beforeEach); + (window.afterEach || window.teardown)(module.$$afterEach); + /** * @ngdoc function * @name angular.mock.inject @@ -2409,7 +3342,7 @@ if (window.jasmine || window.mocha) { * These are ignored by the injector when the reference name is resolved. * * For example, the parameter `_myService_` would be resolved as the reference `myService`. - * Since it is available in the function body as _myService_, we can then assign it to a variable + * Since it is available in the function body as `_myService_`, we can then assign it to a variable * defined in an outer scope. * * ``` @@ -2473,7 +3406,7 @@ if (window.jasmine || window.mocha) { - var ErrorAddingDeclarationLocationStack = function(e, errorForStack) { + var ErrorAddingDeclarationLocationStack = function ErrorAddingDeclarationLocationStack(e, errorForStack) { this.message = e.message; this.name = e.name; if (e.line) this.line = e.line; @@ -2482,7 +3415,7 @@ if (window.jasmine || window.mocha) { this.stack = e.stack + '\n' + errorForStack.stack; if (e.stackArray) this.stackArray = e.stackArray; }; - ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString; + ErrorAddingDeclarationLocationStack.prototype = Error.prototype; window.inject = angular.mock.inject = function() { var blockFns = Array.prototype.slice.call(arguments, 0); @@ -2491,13 +3424,16 @@ if (window.jasmine || window.mocha) { if (!errorForStack.stack) { try { throw errorForStack; - } catch (e) {} + } catch (e) { /* empty */ } } - return isSpecRunning() ? workFn.call(currentSpec) : workFn; + return wasInjectorCreated() ? WorkFn.call(currentSpec) : WorkFn; ///////////////////// - function workFn() { + function WorkFn() { var modules = currentSpec.$modules || []; var strictDi = !!currentSpec.$injectorStrict; + modules.unshift(['$injector', function($injector) { + currentSpec.$providerInjector = $injector; + }]); modules.unshift('ngMock'); modules.unshift('ng'); var injector = currentSpec.$injector; @@ -2505,7 +3441,7 @@ if (window.jasmine || window.mocha) { if (strictDi) { // If strictDi is enabled, annotate the providerInjector blocks angular.forEach(modules, function(moduleFn) { - if (typeof moduleFn === "function") { + if (typeof moduleFn === 'function') { angular.injector.$$annotate(moduleFn); } }); @@ -2520,9 +3456,7 @@ if (window.jasmine || window.mocha) { injector.annotate(blockFns[i]); } try { - /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ injector.invoke(blockFns[i] || angular.noop, this); - /* jshint +W040 */ } catch (e) { if (e.stack && errorForStack) { throw new ErrorAddingDeclarationLocationStack(e, errorForStack); @@ -2538,7 +3472,7 @@ if (window.jasmine || window.mocha) { angular.mock.inject.strictDi = function(value) { value = arguments.length ? !!value : true; - return isSpecRunning() ? workFn() : workFn; + return wasInjectorCreated() ? workFn() : workFn; function workFn() { if (value !== currentSpec.$injectorStrict) { @@ -2550,7 +3484,274 @@ if (window.jasmine || window.mocha) { } } }; -} + + function InjectorState() { + this.shared = false; + this.sharedError = null; + + this.cleanupAfterEach = function() { + return !this.shared || this.sharedError; + }; + } +})(window.jasmine || window.mocha); + +'use strict'; + +(function() { + /** + * @ngdoc function + * @name browserTrigger + * @description + * + * This is a global (window) function that is only available when the {@link ngMock} module is + * included. + * + * It can be used to trigger a native browser event on an element, which is useful for unit testing. + * + * + * @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement + * @param {string=} eventType Optional event type. If none is specified, the function tries + * to determine the right event type for the element, e.g. `change` for + * `input[text]`. + * @param {Object=} eventData An optional object which contains additional event data that is used + * when creating the event: + * + * - `bubbles`: [Event.bubbles](https://developer.mozilla.org/docs/Web/API/Event/bubbles). + * Not applicable to all events. + * + * - `cancelable`: [Event.cancelable](https://developer.mozilla.org/docs/Web/API/Event/cancelable). + * Not applicable to all events. + * + * - `charcode`: [charCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/charcode) + * for keyboard events (keydown, keypress, and keyup). + * + * - `data`: [data](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/data) for + * [CompositionEvents](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent). + * + * - `elapsedTime`: the elapsedTime for + * [TransitionEvent](https://developer.mozilla.org/docs/Web/API/TransitionEvent) + * and [AnimationEvent](https://developer.mozilla.org/docs/Web/API/AnimationEvent). + * + * - `keycode`: [keyCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/keycode) + * for keyboard events (keydown, keypress, and keyup). + * + * - `keys`: an array of possible modifier keys (ctrl, alt, shift, meta) for + * [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) and + * keyboard events (keydown, keypress, and keyup). + * + * - `relatedTarget`: the + * [relatedTarget](https://developer.mozilla.org/docs/Web/API/MouseEvent/relatedTarget) + * for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent). + * + * - `which`: [which](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/which) + * for keyboard events (keydown, keypress, and keyup). + * + * - `x`: x-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) + * and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent). + * + * - `y`: y-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) + * and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent). + * + */ + window.browserTrigger = function browserTrigger(element, eventType, eventData) { + if (element && !element.nodeName) element = element[0]; + if (!element) return; + + eventData = eventData || {}; + var relatedTarget = eventData.relatedTarget || element; + var keys = eventData.keys; + var x = eventData.x; + var y = eventData.y; + + var inputType = (element.type) ? element.type.toLowerCase() : null, + nodeName = element.nodeName.toLowerCase(); + if (!eventType) { + eventType = { + 'text': 'change', + 'textarea': 'change', + 'hidden': 'change', + 'password': 'change', + 'button': 'click', + 'submit': 'click', + 'reset': 'click', + 'image': 'click', + 'checkbox': 'click', + 'radio': 'click', + 'select-one': 'change', + 'select-multiple': 'change', + '_default_': 'click' + }[inputType || '_default_']; + } + + if (nodeName === 'option') { + element.parentNode.value = element.value; + element = element.parentNode; + eventType = 'change'; + } + + keys = keys || []; + function pressed(key) { + return keys.indexOf(key) !== -1; + } + + var evnt; + if (/transitionend/.test(eventType)) { + if (window.WebKitTransitionEvent) { + evnt = new window.WebKitTransitionEvent(eventType, eventData); + evnt.initEvent(eventType, eventData.bubbles, true); + } else { + try { + evnt = new window.TransitionEvent(eventType, eventData); + } catch (e) { + evnt = window.document.createEvent('TransitionEvent'); + evnt.initTransitionEvent(eventType, eventData.bubbles, null, null, eventData.elapsedTime || 0); + } + } + } else if (/animationend/.test(eventType)) { + if (window.WebKitAnimationEvent) { + evnt = new window.WebKitAnimationEvent(eventType, eventData); + evnt.initEvent(eventType, eventData.bubbles, true); + } else { + try { + evnt = new window.AnimationEvent(eventType, eventData); + } catch (e) { + evnt = window.document.createEvent('AnimationEvent'); + evnt.initAnimationEvent(eventType, eventData.bubbles, null, null, eventData.elapsedTime || 0); + } + } + } else if (/touch/.test(eventType) && supportsTouchEvents()) { + evnt = createTouchEvent(element, eventType, x, y); + } else if (/key/.test(eventType)) { + evnt = window.document.createEvent('Events'); + evnt.initEvent(eventType, eventData.bubbles, eventData.cancelable); + evnt.view = window; + evnt.ctrlKey = pressed('ctrl'); + evnt.altKey = pressed('alt'); + evnt.shiftKey = pressed('shift'); + evnt.metaKey = pressed('meta'); + evnt.keyCode = eventData.keyCode; + evnt.charCode = eventData.charCode; + evnt.which = eventData.which; + } else if (/composition/.test(eventType)) { + try { + evnt = new window.CompositionEvent(eventType, { + data: eventData.data + }); + } catch (e) { + // Support: IE9+ + evnt = window.document.createEvent('CompositionEvent', {}); + evnt.initCompositionEvent( + eventType, + eventData.bubbles, + eventData.cancelable, + window, + eventData.data, + null + ); + } + + } else { + evnt = window.document.createEvent('MouseEvents'); + x = x || 0; + y = y || 0; + evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), + pressed('alt'), pressed('shift'), pressed('meta'), 0, relatedTarget); + } + + /* we're unable to change the timeStamp value directly so this + * is only here to allow for testing where the timeStamp value is + * read */ + evnt.$manualTimeStamp = eventData.timeStamp; + + if (!evnt) return; + + if (!eventData.bubbles || supportsEventBubblingInDetachedTree() || isAttachedToDocument(element)) { + return element.dispatchEvent(evnt); + } else { + triggerForPath(element, evnt); + } + }; + + function supportsTouchEvents() { + if ('_cached' in supportsTouchEvents) { + return supportsTouchEvents._cached; + } + if (!window.document.createTouch || !window.document.createTouchList) { + supportsTouchEvents._cached = false; + return false; + } + try { + window.document.createEvent('TouchEvent'); + } catch (e) { + supportsTouchEvents._cached = false; + return false; + } + supportsTouchEvents._cached = true; + return true; + } + + function createTouchEvent(element, eventType, x, y) { + var evnt = new window.Event(eventType); + x = x || 0; + y = y || 0; + + var touch = window.document.createTouch(window, element, Date.now(), x, y, x, y); + var touches = window.document.createTouchList(touch); + + evnt.touches = touches; + + return evnt; + } + + function supportsEventBubblingInDetachedTree() { + if ('_cached' in supportsEventBubblingInDetachedTree) { + return supportsEventBubblingInDetachedTree._cached; + } + supportsEventBubblingInDetachedTree._cached = false; + var doc = window.document; + if (doc) { + var parent = doc.createElement('div'), + child = parent.cloneNode(); + parent.appendChild(child); + parent.addEventListener('e', function() { + supportsEventBubblingInDetachedTree._cached = true; + }); + var evnt = window.document.createEvent('Events'); + evnt.initEvent('e', true, true); + child.dispatchEvent(evnt); + } + return supportsEventBubblingInDetachedTree._cached; + } + + function triggerForPath(element, evnt) { + var stop = false; + + var _stopPropagation = evnt.stopPropagation; + evnt.stopPropagation = function() { + stop = true; + _stopPropagation.apply(evnt, arguments); + }; + patchEventTargetForBubbling(evnt, element); + do { + element.dispatchEvent(evnt); + // eslint-disable-next-line no-unmodified-loop-condition + } while (!stop && (element = element.parentNode)); + } + + function patchEventTargetForBubbling(event, target) { + event._target = target; + Object.defineProperty(event, 'target', {get: function() { return this._target;}}); + } + + function isAttachedToDocument(element) { + while ((element = element.parentNode)) { + if (element === window) { + return true; + } + } + return false; + } +})(); })(window, window.angular); diff --git a/js/vendor/angular-mocks/bower.json b/js/vendor/angular-mocks/bower.json index 1266add2..6ab5a8b8 100644 --- a/js/vendor/angular-mocks/bower.json +++ b/js/vendor/angular-mocks/bower.json @@ -1,10 +1,10 @@ { "name": "angular-mocks", - "version": "1.4.14", + "version": "1.8.3", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.4.14" + "angular": "1.8.3" } } diff --git a/js/vendor/angular-mocks/package.json b/js/vendor/angular-mocks/package.json index 0fb4f1d5..9e2308de 100644 --- a/js/vendor/angular-mocks/package.json +++ b/js/vendor/angular-mocks/package.json @@ -1,6 +1,6 @@ { "name": "angular-mocks", - "version": "1.4.14", + "version": "1.8.3", "description": "AngularJS mocks for testing", "main": "angular-mocks.js", "scripts": { diff --git a/js/vendor/angular-route/.bower.json b/js/vendor/angular-route/.bower.json index 59a67162..bcd2054a 100644 --- a/js/vendor/angular-route/.bower.json +++ b/js/vendor/angular-route/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-route", - "version": "1.4.14", + "version": "1.8.3", "license": "MIT", "main": "./angular-route.js", "ignore": [], "dependencies": { - "angular": "1.4.14" + "angular": "1.8.3" }, "homepage": "https://github.com/angular/bower-angular-route", - "_release": "1.4.14", + "_release": "1.8.3", "_resolution": { "type": "version", - "tag": "v1.4.14", - "commit": "785dd6ff64784a33840ab4535503f3f576e78751" + "tag": "v1.8.3", + "commit": "01f39b724c144ffc556bef7f3bca43b1b5662fe7" }, "_source": "https://github.com/angular/bower-angular-route.git", - "_target": "1.4.*", + "_target": "1.8.3", "_originalSource": "angular-route" } \ No newline at end of file diff --git a/js/vendor/angular-route/README.md b/js/vendor/angular-route/README.md index 2cd4f909..359e5659 100644 --- a/js/vendor/angular-route/README.md +++ b/js/vendor/angular-route/README.md @@ -1,8 +1,11 @@ # packaged angular-route -This repo is for distribution on `npm` and `bower`. The source for this module is in the -[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngRoute). -Please file issues and pull requests against that repo. +**This package contains the legacy AngularJS (version 1.x). AngularJS support has officially ended +as of January 2022. +[See what ending support means](https://docs.angularjs.org/misc/version-support-status) and +[read the end of life announcement](https://goo.gle/angularjs-end-of-life).** + +**[See `@angular/core` for the actively supported Angular](https://npmjs.com/@angular/core).** ## Install @@ -47,7 +50,7 @@ Documentation is available on the The MIT License -Copyright (c) 2010-2015 Google, Inc. http://angularjs.org +Copyright (c) 2022 Google LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/js/vendor/angular-route/angular-route.js b/js/vendor/angular-route/angular-route.js index 519e92a2..c08ca5fc 100644 --- a/js/vendor/angular-route/angular-route.js +++ b/js/vendor/angular-route/angular-route.js @@ -1,45 +1,137 @@ /** - * @license AngularJS v1.4.14 - * (c) 2010-2015 Google, Inc. http://angularjs.org + * @license AngularJS v1.8.3 + * (c) 2010-2020 Google LLC. http://angularjs.org * License: MIT */ -(function(window, angular, undefined) {'use strict'; +(function(window, angular) {'use strict'; + +/* global shallowCopy: true */ + +/** + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there are no proto properties for objects. + */ +function shallowCopy(src, dst) { + if (isArray(src)) { + dst = dst || []; + + for (var i = 0, ii = src.length; i < ii; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + } + + return dst || src; +} + +/* global routeToRegExp: true */ + +/** + * @param {string} path - The path to parse. (It is assumed to have query and hash stripped off.) + * @param {Object} opts - Options. + * @return {Object} - An object containing an array of path parameter names (`keys`) and a regular + * expression (`regexp`) that can be used to identify a matching URL and extract the path + * parameter values. + * + * @description + * Parses the given path, extracting path parameter names and a regular expression to match URLs. + * + * Originally inspired by `pathRexp` in `visionmedia/express/lib/utils.js`. + */ +function routeToRegExp(path, opts) { + var keys = []; + + var pattern = path + .replace(/([().])/g, '\\$1') + .replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) { + var optional = option === '?' || option === '*?'; + var star = option === '*' || option === '*?'; + keys.push({name: key, optional: optional}); + slash = slash || ''; + return ( + (optional ? '(?:' + slash : slash + '(?:') + + (star ? '(.+?)' : '([^/]+)') + + (optional ? '?)?' : ')') + ); + }) + .replace(/([/$*])/g, '\\$1'); + + if (opts.ignoreTrailingSlashes) { + pattern = pattern.replace(/\/+$/, '') + '/*'; + } + + return { + keys: keys, + regexp: new RegExp( + '^' + pattern + '(?:[?#]|$)', + opts.caseInsensitiveMatch ? 'i' : '' + ) + }; +} + +/* global routeToRegExp: false */ +/* global shallowCopy: false */ + +// `isArray` and `isObject` are necessary for `shallowCopy()` (included via `src/shallowCopy.js`). +// They are initialized inside the `$RouteProvider`, to ensure `window.angular` is available. +var isArray; +var isObject; +var isDefined; +var noop; /** * @ngdoc module * @name ngRoute * @description * - * # ngRoute - * - * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. + * The `ngRoute` module provides routing and deeplinking services and directives for AngularJS apps. * * ## Example - * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. - * + * See {@link ngRoute.$route#examples $route} for an example of configuring and using `ngRoute`. * - *
*/ - /* global -ngRouteModule */ -var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider), - $routeMinErr = angular.$$minErr('ngRoute'); +/* global -ngRouteModule */ +var ngRouteModule = angular. + module('ngRoute', []). + info({ angularVersion: '1.8.3' }). + provider('$route', $RouteProvider). + // Ensure `$route` will be instantiated in time to capture the initial `$locationChangeSuccess` + // event (unless explicitly disabled). This is necessary in case `ngView` is included in an + // asynchronously loaded template. + run(instantiateRoute); +var $routeMinErr = angular.$$minErr('ngRoute'); +var isEagerInstantiationEnabled; + /** * @ngdoc provider * @name $routeProvider + * @this * * @description * * Used for configuring routes. * * ## Example - * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. + * See {@link ngRoute.$route#examples $route} for an example of configuring and using `ngRoute`. * * ## Dependencies * Requires the {@link ngRoute `ngRoute`} module to be installed. */ function $RouteProvider() { + isArray = angular.isArray; + isObject = angular.isObject; + isDefined = angular.isDefined; + noop = angular.noop; + function inherit(parent, extra) { return angular.extend(Object.create(parent), extra); } @@ -75,12 +167,12 @@ function $RouteProvider() { * * Object properties: * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with + * - `controller` – `{(string|Function)=}` – Controller fn that should be associated with * newly created scope or the name of a {@link angular.Module#controller registered * controller} if passed as a string. * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. * If present, the controller will be published to scope under the `controllerAs` name. - * - `template` – `{string=|function()=}` – html template as a string or a function that + * - `template` – `{(string|Function)=}` – html template as a string or a function that * returns an html template as a string which should be used by {@link * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. * This property takes precedence over `templateUrl`. @@ -90,7 +182,9 @@ function $RouteProvider() { * - `{Array.}` - route parameters extracted from the current * `$location.path()` by applying the current route * - * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * One of `template` or `templateUrl` is required. + * + * - `templateUrl` – `{(string|Function)=}` – path or function that returns a path to an html * template that should be used by {@link ngRoute.directive:ngView ngView}. * * If `templateUrl` is a function, it will be called with the following parameters: @@ -98,25 +192,39 @@ function $RouteProvider() { * - `{Array.}` - route parameters extracted from the current * `$location.path()` by applying the current route * - * - `resolve` - `{Object.=}` - An optional map of dependencies which should + * One of `templateUrl` or `template` is required. + * + * - `resolve` - `{Object.=}` - An optional map of dependencies which should * be injected into the controller. If any of these dependencies are promises, the router * will wait for them all to be resolved or one to be rejected before the controller is * instantiated. * If all the promises are resolved successfully, the values of the resolved promises are * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is * fired. If any of the promises are rejected the - * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object - * is: + * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. + * For easier access to the resolved dependencies from the template, the `resolve` map will + * be available on the scope of the route, under `$resolve` (by default) or a custom name + * specified by the `resolveAs` property (see below). This can be particularly useful, when + * working with {@link angular.Module#component components} as route templates.
+ *
+ * **Note:** If your scope already contains a property with this name, it will be hidden + * or overwritten. Make sure, you specify an appropriate name for this property, that + * does not collide with other properties on the scope. + *
+ * The map object is: * * - `key` – `{string}`: a name of a dependency to be injected into the controller. - * - `factory` - `{string|function}`: If `string` then it is an alias for a service. + * - `factory` - `{string|Function}`: If `string` then it is an alias for a service. * Otherwise if function, then it is {@link auto.$injector#invoke injected} * and the return value is treated as the dependency. If the result is a promise, it is * resolved before its value is injected into the controller. Be aware that * `ngRoute.$routeParams` will still refer to the previous route within these resolve * functions. Use `$route.current.params` to access the new route parameters, instead. * - * - `redirectTo` – {(string|function())=} – value to update + * - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on + * the scope of the route. If omitted, defaults to `$resolve`. + * + * - `redirectTo` – `{(string|Function)=}` – value to update * {@link ng.$location $location} path with and trigger route redirection. * * If `redirectTo` is a function, it will be called with the following parameters: @@ -127,15 +235,50 @@ function $RouteProvider() { * - `{Object}` - current `$location.search()` * * The custom `redirectTo` function is expected to return a string which will be used - * to update `$location.path()` and `$location.search()`. + * to update `$location.url()`. If the function throws an error, no further processing will + * take place and the {@link ngRoute.$route#$routeChangeError $routeChangeError} event will + * be fired. + * + * Routes that specify `redirectTo` will not have their controllers, template functions + * or resolves called, the `$location` will be changed to the redirect url and route + * processing will stop. The exception to this is if the `redirectTo` is a function that + * returns `undefined`. In this case the route transition occurs as though there was no + * redirection. * - * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` + * - `resolveRedirectTo` – `{Function=}` – a function that will (eventually) return the value + * to update {@link ng.$location $location} URL with and trigger route redirection. In + * contrast to `redirectTo`, dependencies can be injected into `resolveRedirectTo` and the + * return value can be either a string or a promise that will be resolved to a string. + * + * Similar to `redirectTo`, if the return value is `undefined` (or a promise that gets + * resolved to `undefined`), no redirection takes place and the route transition occurs as + * though there was no redirection. + * + * If the function throws an error or the returned promise gets rejected, no further + * processing will take place and the + * {@link ngRoute.$route#$routeChangeError $routeChangeError} event will be fired. + * + * `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same + * route definition, will cause the latter to be ignored. + * + * - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes + * (including the path) even if the new URL maps to the same route. + * + * If the option is set to `false` and the URL in the browser changes, but the new URL maps + * to the same route, then a `$routeUpdate` event is broadcasted on the root scope (without + * reloading the route). + * + * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()` * or `$location.hash()` changes. * - * If the option is set to `false` and url in the browser changes, then - * `$routeUpdate` event is broadcasted on the root scope. + * If the option is set to `false` and the URL in the browser changes, then a `$routeUpdate` + * event is broadcasted on the root scope (without reloading the route). + * + *
+ * **Note:** This option has no effect if `reloadOnUrl` is set to `false`. + *
* - * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive + * - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive * * If the option is set to `true`, then the particular route can be matched without being * case sensitive @@ -147,7 +290,10 @@ function $RouteProvider() { */ this.when = function(path, route) { //copy original route object to preserve params inherited from proto chain - var routeCopy = angular.copy(route); + var routeCopy = shallowCopy(route); + if (angular.isUndefined(routeCopy.reloadOnUrl)) { + routeCopy.reloadOnUrl = true; + } if (angular.isUndefined(routeCopy.reloadOnSearch)) { routeCopy.reloadOnSearch = true; } @@ -156,18 +302,19 @@ function $RouteProvider() { } routes[path] = angular.extend( routeCopy, - path && pathRegExp(path, routeCopy) + {originalPath: path}, + path && routeToRegExp(path, routeCopy) ); // create redirection for trailing slashes if (path) { - var redirectPath = (path[path.length - 1] == '/') + var redirectPath = (path[path.length - 1] === '/') ? path.substr(0, path.length - 1) : path + '/'; routes[redirectPath] = angular.extend( - {redirectTo: path}, - pathRegExp(redirectPath, routeCopy) + {originalPath: path, redirectTo: path}, + routeToRegExp(redirectPath, routeCopy) ); } @@ -185,47 +332,6 @@ function $RouteProvider() { */ this.caseInsensitiveMatch = false; - /** - * @param path {string} path - * @param opts {Object} options - * @return {?Object} - * - * @description - * Normalizes the given path, returning a regular expression - * and the original path. - * - * Inspired by pathRexp in visionmedia/express/lib/utils.js. - */ - function pathRegExp(path, opts) { - var insensitive = opts.caseInsensitiveMatch, - ret = { - originalPath: path, - regexp: path - }, - keys = ret.keys = []; - - path = path - .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) { - var optional = (option === '?' || option === '*?') ? '?' : null; - var star = (option === '*' || option === '*?') ? '*' : null; - keys.push({ name: key, optional: !!optional }); - slash = slash || ''; - return '' - + (optional ? '' : slash) - + '(?:' - + (optional ? slash : '') - + (star && '(.+?)' || '([^/]+)') - + (optional || '') - + ')' - + (optional || ''); - }) - .replace(/([\/$\*])/g, '\\$1'); - - ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); - return ret; - } - /** * @ngdoc method * @name $routeProvider#otherwise @@ -246,6 +352,47 @@ function $RouteProvider() { return this; }; + /** + * @ngdoc method + * @name $routeProvider#eagerInstantiationEnabled + * @kind function + * + * @description + * Call this method as a setter to enable/disable eager instantiation of the + * {@link ngRoute.$route $route} service upon application bootstrap. You can also call it as a + * getter (i.e. without any arguments) to get the current value of the + * `eagerInstantiationEnabled` flag. + * + * Instantiating `$route` early is necessary for capturing the initial + * {@link ng.$location#$locationChangeStart $locationChangeStart} event and navigating to the + * appropriate route. Usually, `$route` is instantiated in time by the + * {@link ngRoute.ngView ngView} directive. Yet, in cases where `ngView` is included in an + * asynchronously loaded template (e.g. in another directive's template), the directive factory + * might not be called soon enough for `$route` to be instantiated _before_ the initial + * `$locationChangeSuccess` event is fired. Eager instantiation ensures that `$route` is always + * instantiated in time, regardless of when `ngView` will be loaded. + * + * The default value is true. + * + * **Note**:
+ * You may want to disable the default behavior when unit-testing modules that depend on + * `ngRoute`, in order to avoid an unexpected request for the default route's template. + * + * @param {boolean=} enabled - If provided, update the internal `eagerInstantiationEnabled` flag. + * + * @returns {*} The current value of the `eagerInstantiationEnabled` flag if used as a getter or + * itself (for chaining) if used as a setter. + */ + isEagerInstantiationEnabled = true; + this.eagerInstantiationEnabled = function eagerInstantiationEnabled(enabled) { + if (isDefined(enabled)) { + isEagerInstantiationEnabled = enabled; + return this; + } + + return isEagerInstantiationEnabled; + }; + this.$get = ['$rootScope', '$location', @@ -254,7 +401,8 @@ function $RouteProvider() { '$injector', '$templateRequest', '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { + '$browser', + function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce, $browser) { /** * @ngdoc service @@ -265,7 +413,7 @@ function $RouteProvider() { * @property {Object} current Reference to the current route definition. * The route definition contains: * - * - `controller`: The controller constructor as define in route definition. + * - `controller`: The controller constructor as defined in the route definition. * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for * controller instantiation. The `locals` contain * the resolved values of the `resolve` map. Additionally the `locals` also contain: @@ -273,6 +421,10 @@ function $RouteProvider() { * - `$scope` - The current route scope. * - `$template` - The current route template HTML. * + * The `locals` will be assigned to the route scope's `$resolve` property. You can override + * the property name, using `resolveAs` in the route definition. See + * {@link ngRoute.$routeProvider $routeProvider} for more info. + * * @property {Object} routes Object with all route configuration Objects as its properties. * * @description @@ -335,12 +487,12 @@ function $RouteProvider() { * }) * * .controller('BookController', function($scope, $routeParams) { - * $scope.name = "BookController"; + * $scope.name = 'BookController'; * $scope.params = $routeParams; * }) * * .controller('ChapterController', function($scope, $routeParams) { - * $scope.name = "ChapterController"; + * $scope.name = 'ChapterController'; * $scope.params = $routeParams; * }) * @@ -373,15 +525,15 @@ function $RouteProvider() { * it('should load and compile correct template', function() { * element(by.linkText('Moby: Ch1')).click(); * var content = element(by.css('[ng-view]')).getText(); - * expect(content).toMatch(/controller\: ChapterController/); - * expect(content).toMatch(/Book Id\: Moby/); - * expect(content).toMatch(/Chapter Id\: 1/); + * expect(content).toMatch(/controller: ChapterController/); + * expect(content).toMatch(/Book Id: Moby/); + * expect(content).toMatch(/Chapter Id: 1/); * * element(by.partialLinkText('Scarlet')).click(); * * content = element(by.css('[ng-view]')).getText(); - * expect(content).toMatch(/controller\: BookController/); - * expect(content).toMatch(/Book Id\: Scarlet/); + * expect(content).toMatch(/controller: BookController/); + * expect(content).toMatch(/Book Id: Scarlet/); * }); * * @@ -429,12 +581,14 @@ function $RouteProvider() { * @name $route#$routeChangeError * @eventType broadcast on root scope * @description - * Broadcasted if any of the resolve promises are rejected. + * Broadcasted if a redirection function fails or any redirection or resolve promises are + * rejected. * * @param {Object} angularEvent Synthetic event object * @param {Route} current Current route information. * @param {Route} previous Previous route information. - * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. + * @param {Route} rejection The thrown error or the rejection reason of the promise. Usually + * the rejection reason is the error that caused the promise to get rejected. */ /** @@ -442,8 +596,9 @@ function $RouteProvider() { * @name $route#$routeUpdate * @eventType broadcast on root scope * @description - * The `reloadOnSearch` property has been set to false, and we are reusing the same - * instance of the Controller. + * Broadcasted if the same instance of a route (including template, controller instance, + * resolved dependencies, etc.) is being reused. This can happen if either `reloadOnSearch` or + * `reloadOnUrl` has been set to `false`. * * @param {Object} angularEvent Synthetic event object * @param {Route} current Current/previous route information. @@ -503,7 +658,7 @@ function $RouteProvider() { // interpolate modifies newParams, only query params are left $location.search(newParams); } else { - throw $routeMinErr('norout', 'Tried updating route when with no current route'); + throw $routeMinErr('norout', 'Tried updating route with no current route'); } } }; @@ -551,9 +706,7 @@ function $RouteProvider() { var lastRoute = $route.current; preparedRoute = parseRoute(); - preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route - && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) - && !preparedRoute.reloadOnSearch && !forceReload; + preparedRouteIsUpdateOnly = isNavigationUpdateOnly(preparedRoute, lastRoute); if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { @@ -575,65 +728,145 @@ function $RouteProvider() { } else if (nextRoute || lastRoute) { forceReload = false; $route.current = nextRoute; - if (nextRoute) { - if (nextRoute.redirectTo) { - if (angular.isString(nextRoute.redirectTo)) { - $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) - .replace(); - } else { - $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) - .replace(); + + var nextRoutePromise = $q.resolve(nextRoute); + + $browser.$$incOutstandingRequestCount('$route'); + + nextRoutePromise. + then(getRedirectionData). + then(handlePossibleRedirection). + then(function(keepProcessingRoute) { + return keepProcessingRoute && nextRoutePromise. + then(resolveLocals). + then(function(locals) { + // after route change + if (nextRoute === $route.current) { + if (nextRoute) { + nextRoute.locals = locals; + angular.copy(nextRoute.params, $routeParams); + } + $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); + } + }); + }).catch(function(error) { + if (nextRoute === $route.current) { + $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); } - } - } + }).finally(function() { + // Because `commitRoute()` is called from a `$rootScope.$evalAsync` block (see + // `$locationWatch`), this `$$completeOutstandingRequest()` call will not cause + // `outstandingRequestCount` to hit zero. This is important in case we are redirecting + // to a new route which also requires some asynchronous work. - $q.when(nextRoute). - then(function() { - if (nextRoute) { - var locals = angular.extend({}, nextRoute.resolve), - template, templateUrl; + $browser.$$completeOutstandingRequest(noop, '$route'); + }); + } + } - angular.forEach(locals, function(value, key) { - locals[key] = angular.isString(value) ? - $injector.get(value) : $injector.invoke(value, null, null, key); - }); + function getRedirectionData(route) { + var data = { + route: route, + hasRedirection: false + }; + + if (route) { + if (route.redirectTo) { + if (angular.isString(route.redirectTo)) { + data.path = interpolate(route.redirectTo, route.params); + data.search = route.params; + data.hasRedirection = true; + } else { + var oldPath = $location.path(); + var oldSearch = $location.search(); + var newUrl = route.redirectTo(route.pathParams, oldPath, oldSearch); - if (angular.isDefined(template = nextRoute.template)) { - if (angular.isFunction(template)) { - template = template(nextRoute.params); - } - } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { - if (angular.isFunction(templateUrl)) { - templateUrl = templateUrl(nextRoute.params); - } - if (angular.isDefined(templateUrl)) { - nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl); - template = $templateRequest(templateUrl); - } - } - if (angular.isDefined(template)) { - locals['$template'] = template; - } - return $q.all(locals); + if (angular.isDefined(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; } - }). - then(function(locals) { - // after route change - if (nextRoute == $route.current) { - if (nextRoute) { - nextRoute.locals = locals; - angular.copy(nextRoute.params, $routeParams); + } + } else if (route.resolveRedirectTo) { + return $q. + resolve($injector.invoke(route.resolveRedirectTo)). + then(function(newUrl) { + if (angular.isDefined(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; } - $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); - } - }, function(error) { - if (nextRoute == $route.current) { - $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); - } - }); + + return data; + }); + } + } + + return data; + } + + function handlePossibleRedirection(data) { + var keepProcessingRoute = true; + + if (data.route !== $route.current) { + keepProcessingRoute = false; + } else if (data.hasRedirection) { + var oldUrl = $location.url(); + var newUrl = data.url; + + if (newUrl) { + $location. + url(newUrl). + replace(); + } else { + newUrl = $location. + path(data.path). + search(data.search). + replace(). + url(); + } + + if (newUrl !== oldUrl) { + // Exit out and don't process current next value, + // wait for next location change from redirect + keepProcessingRoute = false; + } + } + + return keepProcessingRoute; + } + + function resolveLocals(route) { + if (route) { + var locals = angular.extend({}, route.resolve); + angular.forEach(locals, function(value, key) { + locals[key] = angular.isString(value) ? + $injector.get(value) : + $injector.invoke(value, null, null, key); + }); + var template = getTemplateFor(route); + if (angular.isDefined(template)) { + locals['$template'] = template; + } + return $q.all(locals); } } + function getTemplateFor(route) { + var template, templateUrl; + if (angular.isDefined(template = route.template)) { + if (angular.isFunction(template)) { + template = template(route.params); + } + } else if (angular.isDefined(templateUrl = route.templateUrl)) { + if (angular.isFunction(templateUrl)) { + templateUrl = templateUrl(route.params); + } + if (angular.isDefined(templateUrl)) { + route.loadedTemplateUrl = $sce.valueOf(templateUrl); + template = $templateRequest(templateUrl); + } + } + return template; + } /** * @returns {Object} the current active route, by matching it against the URL @@ -653,6 +886,29 @@ function $RouteProvider() { return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); } + /** + * @param {Object} newRoute - The new route configuration (as returned by `parseRoute()`). + * @param {Object} oldRoute - The previous route configuration (as returned by `parseRoute()`). + * @returns {boolean} Whether this is an "update-only" navigation, i.e. the URL maps to the same + * route and it can be reused (based on the config and the type of change). + */ + function isNavigationUpdateOnly(newRoute, oldRoute) { + // IF this is not a forced reload + return !forceReload + // AND both `newRoute`/`oldRoute` are defined + && newRoute && oldRoute + // AND they map to the same Route Definition Object + && (newRoute.$$route === oldRoute.$$route) + // AND `reloadOnUrl` is disabled + && (!newRoute.reloadOnUrl + // OR `reloadOnSearch` is disabled + || (!newRoute.reloadOnSearch + // AND both routes have the same path params + && angular.equals(newRoute.pathParams, oldRoute.pathParams) + ) + ); + } + /** * @returns {string} interpolation of the redirect path with the parameters */ @@ -674,6 +930,14 @@ function $RouteProvider() { }]; } +instantiateRoute.$inject = ['$injector']; +function instantiateRoute($injector) { + if (isEagerInstantiationEnabled) { + // Instantiate `$route` + $injector.get('$route'); + } +} + ngRouteModule.provider('$routeParams', $RouteParamsProvider); @@ -681,6 +945,7 @@ ngRouteModule.provider('$routeParams', $RouteParamsProvider); * @ngdoc service * @name $routeParams * @requires $route + * @this * * @description * The `$routeParams` service allows you to retrieve the current set of route parameters. @@ -724,7 +989,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * @restrict ECA * * @description - * # Overview * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by * including the rendered template of the current route into the main layout (`index.html`) file. * Every time the current route changes, the included view changes with it according to the @@ -733,8 +997,10 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * Requires the {@link ngRoute `ngRoute`} module to be installed. * * @animations - * enter - animation is used to bring new content into the browser. - * leave - animation is used to animate existing content away. + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM | + * | {@link ng.$animate#leave leave} | when the old element is removed from to the DOM | * * The enter and leave animation occur concurrently. * @@ -848,17 +1114,17 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); $locationProvider.html5Mode(true); }]) .controller('MainCtrl', ['$route', '$routeParams', '$location', - function($route, $routeParams, $location) { + function MainCtrl($route, $routeParams, $location) { this.$route = $route; this.$location = $location; this.$routeParams = $routeParams; }]) - .controller('BookCtrl', ['$routeParams', function($routeParams) { - this.name = "BookCtrl"; + .controller('BookCtrl', ['$routeParams', function BookCtrl($routeParams) { + this.name = 'BookCtrl'; this.params = $routeParams; }]) - .controller('ChapterCtrl', ['$routeParams', function($routeParams) { - this.name = "ChapterCtrl"; + .controller('ChapterCtrl', ['$routeParams', function ChapterCtrl($routeParams) { + this.name = 'ChapterCtrl'; this.params = $routeParams; }]); @@ -868,15 +1134,15 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); it('should load and compile correct template', function() { element(by.linkText('Moby: Ch1')).click(); var content = element(by.css('[ng-view]')).getText(); - expect(content).toMatch(/controller\: ChapterCtrl/); - expect(content).toMatch(/Book Id\: Moby/); - expect(content).toMatch(/Chapter Id\: 1/); + expect(content).toMatch(/controller: ChapterCtrl/); + expect(content).toMatch(/Book Id: Moby/); + expect(content).toMatch(/Chapter Id: 1/); element(by.partialLinkText('Scarlet')).click(); content = element(by.css('[ng-view]')).getText(); - expect(content).toMatch(/controller\: BookCtrl/); - expect(content).toMatch(/Book Id\: Scarlet/); + expect(content).toMatch(/controller: BookCtrl/); + expect(content).toMatch(/Book Id: Scarlet/); }); @@ -919,8 +1185,8 @@ function ngViewFactory($route, $anchorScroll, $animate) { } if (currentElement) { previousLeaveAnimation = $animate.leave(currentElement); - previousLeaveAnimation.then(function() { - previousLeaveAnimation = null; + previousLeaveAnimation.done(function(response) { + if (response !== false) previousLeaveAnimation = null; }); currentElement = null; } @@ -941,8 +1207,8 @@ function ngViewFactory($route, $anchorScroll, $animate) { // function is called before linking the content, which would apply child // directives to non existing elements. var clone = $transclude(newScope, function(clone) { - $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { - if (angular.isDefined(autoScrollExp) + $animate.enter(clone, null, currentElement || $element).done(function onNgViewEnter(response) { + if (response !== false && angular.isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { $anchorScroll(); } @@ -989,6 +1255,7 @@ function ngViewFillContentFactory($compile, $controller, $route) { $element.data('$ngControllerController', controller); $element.children().data('$ngControllerController', controller); } + scope[current.resolveAs || '$resolve'] = locals; link(scope); } diff --git a/js/vendor/angular-route/angular-route.min.js b/js/vendor/angular-route/angular-route.min.js index 9bc38ed9..754e16c5 100644 --- a/js/vendor/angular-route/angular-route.min.js +++ b/js/vendor/angular-route/angular-route.min.js @@ -1,15 +1,17 @@ /* - AngularJS v1.4.14 - (c) 2010-2015 Google, Inc. http://angularjs.org + AngularJS v1.8.3 + (c) 2010-2020 Google LLC. http://angularjs.org License: MIT */ -(function(q,d,C){'use strict';function w(s,k,h){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,f,b,c,y){function z(){l&&(h.cancel(l),l=null);m&&(m.$destroy(),m=null);n&&(l=h.leave(n),l.then(function(){l=null}),n=null)}function x(){var b=s.current&&s.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),c=s.current;n=y(b,function(b){h.enter(b,null,n||f).then(function(){!d.isDefined(u)||u&&!a.$eval(u)||k()});z()});m=c.scope=b;m.$emit("$viewContentLoaded"); -m.$eval(v)}else z()}var m,n,l,u=b.autoscroll,v=b.onload||"";a.$on("$routeChangeSuccess",x);x()}}}function A(d,k,h){return{restrict:"ECA",priority:-400,link:function(a,f){var b=h.current,c=b.locals;f.html(c.$template);var y=d(f.contents());b.controller&&(c.$scope=a,c=k(b.controller,c),b.controllerAs&&(a[b.controllerAs]=c),f.data("$ngControllerController",c),f.children().data("$ngControllerController",c));y(a)}}}q=d.module("ngRoute",["ng"]).provider("$route",function(){function s(a,f){return d.extend(Object.create(a), -f)}function k(a,d){var b=d.caseInsensitiveMatch,c={originalPath:a,regexp:a},h=c.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g,function(a,d,b,c){a="?"===c||"*?"===c?"?":null;c="*"===c||"*?"===c?"*":null;h.push({name:b,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");c.regexp=new RegExp("^"+a+"$",b?"i":"");return c}var h={};this.when=function(a,f){var b=d.copy(f);d.isUndefined(b.reloadOnSearch)&& -(b.reloadOnSearch=!0);d.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);h[a]=d.extend(b,a&&k(a,b));if(a){var c="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";h[c]=d.extend({redirectTo:a},k(c,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,f,b,c,k,q,x){function m(b){var e= -t.current;(w=(p=l())&&e&&p.$$route===e.$$route&&d.equals(p.pathParams,e.pathParams)&&!p.reloadOnSearch&&!v)||!e&&!p||a.$broadcast("$routeChangeStart",p,e).defaultPrevented&&b&&b.preventDefault()}function n(){var g=t.current,e=p;if(w)g.params=e.params,d.copy(g.params,b),a.$broadcast("$routeUpdate",g);else if(e||g)v=!1,(t.current=e)&&e.redirectTo&&(d.isString(e.redirectTo)?f.path(u(e.redirectTo,e.params)).search(e.params).replace():f.url(e.redirectTo(e.pathParams,f.path(),f.search())).replace()),c.when(e).then(function(){if(e){var a= -d.extend({},e.resolve),b,g;d.forEach(a,function(b,e){a[e]=d.isString(b)?k.get(b):k.invoke(b,null,null,e)});d.isDefined(b=e.template)?d.isFunction(b)&&(b=b(e.params)):d.isDefined(g=e.templateUrl)&&(d.isFunction(g)&&(g=g(e.params)),d.isDefined(g)&&(e.loadedTemplateUrl=x.valueOf(g),b=q(g)));d.isDefined(b)&&(a.$template=b);return c.all(a)}}).then(function(c){e==t.current&&(e&&(e.locals=c,d.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,g))},function(b){e==t.current&&a.$broadcast("$routeChangeError", -e,g,b)})}function l(){var a,b;d.forEach(h,function(c,h){var r;if(r=!b){var k=f.path();r=c.keys;var m={};if(c.regexp)if(k=c.regexp.exec(k)){for(var l=1,n=k.length;l 0; +} + /** * @description * * This object provides a utility for producing rich Error messages within - * Angular. It can be called as follows: + * AngularJS. It can be called as follows: * * var exampleMinErr = minErr('example'); * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); @@ -26,7 +87,7 @@ * Since data will be parsed statically during a build step, some restrictions * are applied with respect to how minErr instances are created and called. * Instances should have names of the form namespaceMinErr for a minErr created - * using minErr('namespace') . Error codes, namespaces and template strings + * using minErr('namespace'). Error codes, namespaces and template strings * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. @@ -37,132 +98,148 @@ function minErr(module, ErrorConstructor) { ErrorConstructor = ErrorConstructor || Error; - return function() { - var SKIP_INDEXES = 2; - var templateArgs = arguments, - code = templateArgs[0], + var url = 'https://errors.angularjs.org/1.8.3/'; + var regex = url.replace('.', '\\.') + '[\\s\\S]*'; + var errRegExp = new RegExp(regex, 'g'); + + return function() { + var code = arguments[0], + template = arguments[1], message = '[' + (module ? module + ':' : '') + code + '] ', - template = templateArgs[1], + templateArgs = sliceArgs(arguments, 2).map(function(arg) { + return toDebugString(arg, minErrConfig.objectMaxDepth); + }), paramPrefix, i; + // A minErr message has two parts: the message itself and the url that contains the + // encoded message. + // The message's parameters can contain other error messages which also include error urls. + // To prevent the messages from getting too long, we strip the error urls from the parameters. + message += template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), - shiftedIndex = index + SKIP_INDEXES; + var index = +match.slice(1, -1); - if (shiftedIndex < templateArgs.length) { - return toDebugString(templateArgs[shiftedIndex]); + if (index < templateArgs.length) { + return templateArgs[index].replace(errRegExp, ''); } return match; }); - message += '\nhttp://errors.angularjs.org/1.4.14/' + - (module ? module + '/' : '') + code; + message += '\n' + url + (module ? module + '/' : '') + code; - for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { - message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + - encodeURIComponent(toDebugString(templateArgs[i])); + if (minErrConfig.urlErrorParamsEnabled) { + for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]); + } } return new ErrorConstructor(message); }; } -/* We need to tell jshint what variables are being exported */ -/* global angular: true, - msie: true, - jqLite: true, - jQuery: true, - slice: true, - splice: true, - push: true, - toString: true, - ngMinErr: true, - angularModule: true, - uid: true, - REGEX_STRING_REGEXP: true, - VALIDITY_STATE_PROPERTY: true, - - lowercase: true, - uppercase: true, - manualLowercase: true, - manualUppercase: true, - nodeName_: true, - isArrayLike: true, - forEach: true, - forEachSorted: true, - reverseParams: true, - nextUid: true, - setHashKey: true, - extend: true, - toInt: true, - inherit: true, - merge: true, - noop: true, - identity: true, - valueFn: true, - isUndefined: true, - isDefined: true, - isObject: true, - isBlankObject: true, - isString: true, - isNumber: true, - isDate: true, - isArray: true, - isFunction: true, - isRegExp: true, - isWindow: true, - isScope: true, - isFile: true, - isFormData: true, - isBlob: true, - isBoolean: true, - isPromiseLike: true, - trim: true, - escapeForRegexp: true, - isElement: true, - makeMap: true, - includes: true, - arrayRemove: true, - copy: true, - shallowCopy: true, - equals: true, - csp: true, - jq: true, - concat: true, - sliceArgs: true, - bind: true, - toJsonReplacer: true, - toJson: true, - fromJson: true, - convertTimezoneToLocal: true, - timezoneToOffset: true, - startingTag: true, - tryDecodeURIComponent: true, - parseKeyValue: true, - toKeyValue: true, - encodeUriSegment: true, - encodeUriQuery: true, - angularInit: true, - bootstrap: true, - getTestability: true, - snake_case: true, - bindJQuery: true, - assertArg: true, - assertArgFn: true, - assertNotHasOwnProperty: true, - getter: true, - getBlockNodes: true, - hasOwnProperty: true, - createMap: true, - - NODE_TYPE_ELEMENT: true, - NODE_TYPE_ATTRIBUTE: true, - NODE_TYPE_TEXT: true, - NODE_TYPE_COMMENT: true, - NODE_TYPE_DOCUMENT: true, - NODE_TYPE_DOCUMENT_FRAGMENT: true, +/* We need to tell ESLint what variables are being exported */ +/* exported + angular, + msie, + jqLite, + jQuery, + slice, + splice, + push, + toString, + minErrConfig, + errorHandlingConfig, + isValidObjectMaxDepth, + ngMinErr, + angularModule, + uid, + REGEX_STRING_REGEXP, + VALIDITY_STATE_PROPERTY, + + lowercase, + uppercase, + nodeName_, + isArrayLike, + forEach, + forEachSorted, + reverseParams, + nextUid, + setHashKey, + extend, + toInt, + inherit, + merge, + noop, + identity, + valueFn, + isUndefined, + isDefined, + isObject, + isBlankObject, + isString, + isNumber, + isNumberNaN, + isDate, + isError, + isArray, + isFunction, + isRegExp, + isWindow, + isScope, + isFile, + isFormData, + isBlob, + isBoolean, + isPromiseLike, + trim, + escapeForRegexp, + isElement, + makeMap, + includes, + arrayRemove, + copy, + simpleCompare, + equals, + csp, + jq, + concat, + sliceArgs, + bind, + toJsonReplacer, + toJson, + fromJson, + convertTimezoneToLocal, + timezoneToOffset, + addDateMinutes, + startingTag, + tryDecodeURIComponent, + parseKeyValue, + toKeyValue, + encodeUriSegment, + encodeUriQuery, + angularInit, + bootstrap, + getTestability, + snake_case, + bindJQuery, + assertArg, + assertArgFn, + assertNotHasOwnProperty, + getter, + getBlockNodes, + hasOwnProperty, + createMap, + stringify, + UNSAFE_restoreLegacyJqLiteXHTMLReplacement, + + NODE_TYPE_ELEMENT, + NODE_TYPE_ATTRIBUTE, + NODE_TYPE_TEXT, + NODE_TYPE_COMMENT, + NODE_TYPE_DOCUMENT, + NODE_TYPE_DOCUMENT_FRAGMENT */ //////////////////////////////////// @@ -171,15 +248,14 @@ function minErr(module, ErrorConstructor) { * @ngdoc module * @name ng * @module ng + * @installation * @description * - * # ng (core module) * The ng module is loaded by default when an AngularJS application is started. The module itself * contains the essential components for an AngularJS application to function. The table below * lists a high level breakdown of each of the services/factories, filters, directives and testing * components available within this core module. * - *
*/ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; @@ -188,24 +264,20 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; // This is used so that it's possible for internal tests to create mock ValidityStates. var VALIDITY_STATE_PROPERTY = 'validity'; + +var hasOwnProperty = Object.prototype.hasOwnProperty; + /** - * @ngdoc function - * @name angular.lowercase - * @module ng - * @kind function + * @private * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; -var hasOwnProperty = Object.prototype.hasOwnProperty; /** - * @ngdoc function - * @name angular.uppercase - * @module ng - * @kind function + * @private * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. @@ -214,29 +286,6 @@ var hasOwnProperty = Object.prototype.hasOwnProperty; var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; -var manualLowercase = function(s) { - /* jshint bitwise: false */ - return isString(s) - ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) - : s; -}; -var manualUppercase = function(s) { - /* jshint bitwise: false */ - return isString(s) - ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) - : s; -}; - - -// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish -// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods -// with correct but slower alternatives. -if ('i' !== 'I'.toLowerCase()) { - lowercase = manualLowercase; - uppercase = manualUppercase; -} - - var msie, // holds major version number for IE, or NaN if UA is not IE. jqLite, // delay binding since jQuery could be loaded after us. @@ -253,11 +302,12 @@ var angularModule, uid = 0; +// Support: IE 9-11 only /** * documentMode is an IE-only property * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ -msie = document.documentMode; +msie = window.document.documentMode; /** @@ -273,18 +323,17 @@ function isArrayLike(obj) { // arrays, strings and jQuery/jqLite objects are array like // * jqLite is either the jQuery or jqLite constructor function - // * we have to check the existance of jqLite first as this method is called + // * we have to check the existence of jqLite first as this method is called // via the forEach method when constructing the jqLite object in the first place if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true; // Support: iOS 8.2 (not reproducible in simulator) // "length" in obj used to prevent JIT error (gh-11508) - var length = "length" in Object(obj) && obj.length; + var length = 'length' in Object(obj) && obj.length; // NodeList objects (with `item` method) and // other objects with suitable length characteristics are array-like - return isNumber(length) && - (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item == 'function'); + return isNumber(length) && (length >= 0 && (length - 1) in obj || typeof obj.item === 'function'); } @@ -328,9 +377,7 @@ function forEach(obj, iterator, context) { if (obj) { if (isFunction(obj)) { for (key in obj) { - // Need to check if hasOwnProperty exists, - // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function - if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { + if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } @@ -435,8 +482,10 @@ function baseExtend(dst, objs, deep) { } else if (isElement(src)) { dst[key] = src.clone(); } else { - if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; - baseExtend(dst[key], [src], true); + if (key !== '__proto__') { + if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; + baseExtend(dst[key], [src], true); + } } } else { dst[key] = src; @@ -485,6 +534,22 @@ function extend(dst) { * Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source * objects, performing a deep copy. * +* @deprecated +* sinceVersion="1.6.5" +* This function is deprecated, but will not be removed in the 1.x lifecycle. +* There are edge cases (see {@link angular.merge#known-issues known issues}) that are not +* supported by this function. We suggest using another, similar library for all-purpose merging, +* such as [lodash's merge()](https://lodash.com/docs/4.17.4#merge). +* +* @knownIssue +* This is a list of (known) object types that are not handled correctly by this function: +* - [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob) +* - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream) +* - [`CanvasGradient`](https://developer.mozilla.org/docs/Web/API/CanvasGradient) +* - AngularJS {@link $rootScope.Scope scopes}; +* +* `angular.merge` also does not support merging objects with circular references. +* * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. @@ -499,6 +564,11 @@ function toInt(str) { return parseInt(str, 10); } +var isNumberNaN = Number.isNaN || function isNumberNaN(num) { + // eslint-disable-next-line no-self-compare + return num !== num; +}; + function inherit(parent, extra) { return extend(Object.create(parent), extra); @@ -556,7 +626,7 @@ function identity($) {return $;} identity.$inject = []; -function valueFn(value) {return function() {return value;};} +function valueFn(value) {return function valueRef() {return value;};} function hasCustomToString(obj) { return isFunction(obj.toString) && obj.toString !== toString; @@ -687,7 +757,27 @@ function isDate(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -var isArray = Array.isArray; +function isArray(arr) { + return Array.isArray(arr) || arr instanceof Array; +} + +/** + * @description + * Determines if a reference is an `Error`. + * Loosely based on https://www.npmjs.com/package/iserror + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Error`. + */ +function isError(value) { + var tag = toString.call(value); + switch (tag) { + case '[object Error]': return true; + case '[object Exception]': return true; + case '[object DOMException]': return true; + default: return value instanceof Error; + } +} /** * @ngdoc function @@ -758,11 +848,15 @@ function isPromiseLike(obj) { } -var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; +var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/; function isTypedArray(value) { return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value)); } +function isArrayBuffer(obj) { + return toString.call(obj) === '[object ArrayBuffer]'; +} + var trim = function(value) { return isString(value) ? value.trim() : value; @@ -772,8 +866,10 @@ var trim = function(value) { // http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 // Prereq: s is a string. var escapeForRegexp = function(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:# * - * @param {*} source The source that will be used to make a copy. - * Can be any type, including primitives, `null`, and `undefined`. - * @param {(Object|Array)=} destination Destination into which the source is copied. If - * provided, must be of the same type as `source`. + *
+ * Only enumerable properties are taken into account. Non-enumerable properties (both on `source` + * and on `destination`) will be ignored. + *
+ * + *
+ * `angular.copy` does not check if destination and source are of the same type. It's the + * developer's responsibility to make sure they are compatible. + *
+ * + * @knownIssue + * This is a non-exhaustive list of object types / features that are not handled correctly by + * `angular.copy`. Note that since this functions is used by the change detection code, this + * means binding or watching objects of these types (or that include these types) might not work + * correctly. + * - [`File`](https://developer.mozilla.org/docs/Web/API/File) + * - [`Map`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) + * - [`ImageData`](https://developer.mozilla.org/docs/Web/API/ImageData) + * - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream) + * - [`Set`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set) + * - [`WeakMap`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) + * - [`getter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)/ + * [`setter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) + * + * @param {*} source The source that will be used to make a copy. Can be any type, including + * primitives, `null`, and `undefined`. + * @param {(Object|Array)=} destination Destination into which the source is copied. If provided, + * must be of the same type as `source`. * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - - -
-
- Name:
- E-mail:
- Gender: male - female
- - -
-
form = {{user | json}}
-
master = {{master | json}}
-
- - -
-
+ $scope.reset(); + }]); + + */ -function copy(source, destination) { +function copy(source, destination, maxDepth) { var stackSource = []; var stackDest = []; + maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN; if (destination) { - if (isTypedArray(destination)) { - throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated."); + if (isTypedArray(destination) || isArrayBuffer(destination)) { + throw ngMinErr('cpta', 'Can\'t copy! TypedArray destination cannot be mutated.'); } if (source === destination) { - throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); + throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.'); } // Empty the destination object @@ -907,35 +1033,39 @@ function copy(source, destination) { stackSource.push(source); stackDest.push(destination); - return copyRecurse(source, destination); + return copyRecurse(source, destination, maxDepth); } - return copyElement(source); + return copyElement(source, maxDepth); - function copyRecurse(source, destination) { + function copyRecurse(source, destination, maxDepth) { + maxDepth--; + if (maxDepth < 0) { + return '...'; + } var h = destination.$$hashKey; - var result, key; + var key; if (isArray(source)) { for (var i = 0, ii = source.length; i < ii; i++) { - destination.push(copyElement(source[i])); + destination.push(copyElement(source[i], maxDepth)); } } else if (isBlankObject(source)) { // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty for (key in source) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } else if (source && typeof source.hasOwnProperty === 'function') { // Slow path, which must rely on hasOwnProperty for (key in source) { if (source.hasOwnProperty(key)) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } } else { // Slowest path --- hasOwnProperty can't be called as a method for (key in source) { if (hasOwnProperty.call(source, key)) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } } @@ -943,7 +1073,7 @@ function copy(source, destination) { return destination; } - function copyElement(source) { + function copyElement(source, maxDepth) { // Simple values if (!isObject(source)) { return source; @@ -957,28 +1087,14 @@ function copy(source, destination) { if (isWindow(source) || isScope(source)) { throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); + 'Can\'t copy! Making copies of Window or Scope instances is not supported.'); } var needsRecurse = false; - var destination; + var destination = copyType(source); - if (isArray(source)) { - destination = []; - needsRecurse = true; - } else if (isTypedArray(source)) { - destination = new source.constructor(source); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); - destination.lastIndex = source.lastIndex; - } else if (isBlob(source)) { - destination = new source.constructor([source], {type: source.type}); - } else if (isFunction(source.cloneNode)) { - destination = source.cloneNode(true); - } else { - destination = Object.create(getPrototypeOf(source)); + if (destination === undefined) { + destination = isArray(source) ? [] : Object.create(getPrototypeOf(source)); needsRecurse = true; } @@ -986,37 +1102,61 @@ function copy(source, destination) { stackDest.push(destination); return needsRecurse - ? copyRecurse(source, destination) + ? copyRecurse(source, destination, maxDepth) : destination; } -} - -/** - * Creates a shallow copy of an object, an array or a primitive. - * - * Assumes that there are no proto properties for objects. - */ -function shallowCopy(src, dst) { - if (isArray(src)) { - dst = dst || []; - - for (var i = 0, ii = src.length; i < ii; i++) { - dst[i] = src[i]; - } - } else if (isObject(src)) { - dst = dst || {}; - for (var key in src) { - if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } + function copyType(source) { + switch (toString.call(source)) { + case '[object Int8Array]': + case '[object Int16Array]': + case '[object Int32Array]': + case '[object Float32Array]': + case '[object Float64Array]': + case '[object Uint8Array]': + case '[object Uint8ClampedArray]': + case '[object Uint16Array]': + case '[object Uint32Array]': + return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length); + + case '[object ArrayBuffer]': + // Support: IE10 + if (!source.slice) { + // If we're in this case we know the environment supports ArrayBuffer + /* eslint-disable no-undef */ + var copied = new ArrayBuffer(source.byteLength); + new Uint8Array(copied).set(new Uint8Array(source)); + /* eslint-enable */ + return copied; + } + return source.slice(0); + + case '[object Boolean]': + case '[object Number]': + case '[object String]': + case '[object Date]': + return new source.constructor(source.valueOf()); + + case '[object RegExp]': + var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]); + re.lastIndex = source.lastIndex; + return re; + + case '[object Blob]': + return new source.constructor([source], {type: source.type}); + } + + if (isFunction(source.cloneNode)) { + return source.cloneNode(true); } } - - return dst || src; } +// eslint-disable-next-line no-self-compare +function simpleCompare(a, b) { return a === b || (a !== a && b !== b); } + + /** * @ngdoc function * @name angular.equals @@ -1045,44 +1185,78 @@ function shallowCopy(src, dst) { * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. + * + * @example + + +
+
+

User 1

+ Name: + Age: + +

User 2

+ Name: + Age: + +
+
+ +
+ User 1:
{{user1 | json}}
+ User 2:
{{user2 | json}}
+ Equal:
{{result}}
+
+
+
+ + angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) { + $scope.user1 = {}; + $scope.user2 = {}; + $scope.compare = function() { + $scope.result = angular.equals($scope.user1, $scope.user2); + }; + }]); + +
*/ function equals(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; + // eslint-disable-next-line no-self-compare if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN var t1 = typeof o1, t2 = typeof o2, length, key, keySet; - if (t1 == t2) { - if (t1 == 'object') { - if (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for (key = 0; key < length; key++) { - if (!equals(o1[key], o2[key])) return false; - } - return true; - } - } else if (isDate(o1)) { - if (!isDate(o2)) return false; - return equals(o1.getTime(), o2.getTime()); - } else if (isRegExp(o1)) { - return isRegExp(o2) ? o1.toString() == o2.toString() : false; - } else { - if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || - isArray(o2) || isDate(o2) || isRegExp(o2)) return false; - keySet = createMap(); - for (key in o1) { - if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (t1 === t2 && t1 === 'object') { + if (isArray(o1)) { + if (!isArray(o2)) return false; + if ((length = o1.length) === o2.length) { + for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; - keySet[key] = true; - } - for (key in o2) { - if (!(key in keySet) && - key.charAt(0) !== '$' && - isDefined(o2[key]) && - !isFunction(o2[key])) return false; } return true; } + } else if (isDate(o1)) { + if (!isDate(o2)) return false; + return simpleCompare(o1.getTime(), o2.getTime()); + } else if (isRegExp(o1)) { + if (!isRegExp(o2)) return false; + return o1.toString() === o2.toString(); + } else { + if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || + isArray(o2) || isDate(o2) || isRegExp(o2)) return false; + keySet = createMap(); + for (key in o1) { + if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (!equals(o1[key], o2[key])) return false; + keySet[key] = true; + } + for (key in o2) { + if (!(key in keySet) && + key.charAt(0) !== '$' && + isDefined(o2[key]) && + !isFunction(o2[key])) return false; + } + return true; } } return false; @@ -1092,8 +1266,8 @@ var csp = function() { if (!isDefined(csp.rules)) { - var ngCspElement = (document.querySelector('[ng-csp]') || - document.querySelector('[data-ng-csp]')); + var ngCspElement = (window.document.querySelector('[ng-csp]') || + window.document.querySelector('[data-ng-csp]')); if (ngCspElement) { var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || @@ -1114,9 +1288,8 @@ var csp = function() { function noUnsafeEval() { try { - /* jshint -W031, -W054 */ + // eslint-disable-next-line no-new, no-new-func new Function(''); - /* jshint +W031, +W054 */ return false; } catch (e) { return true; @@ -1137,7 +1310,7 @@ var csp = function() { * used to force either jqLite by leaving ng-jq blank or setting the name of * the jquery variable under window (eg. jQuery). * - * Since angular looks for this directive when it is loaded (doesn't wait for the + * Since AngularJS looks for this directive when it is loaded (doesn't wait for the * DOMContentLoaded event), it must be placed on an element that comes before the script * which loads angular. Also, only the first instance of `ng-jq` will be used and all * others ignored. @@ -1168,7 +1341,8 @@ var jq = function() { var i, ii = ngAttrPrefixes.length, prefix, name; for (i = 0; i < ii; ++i) { prefix = ngAttrPrefixes[i]; - if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) { + el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]'); + if (el) { name = el.getAttribute(prefix + 'jq'); break; } @@ -1186,7 +1360,6 @@ function sliceArgs(args, startIndex) { } -/* jshint -W101 */ /** * @ngdoc function * @name angular.bind @@ -1204,7 +1377,6 @@ function sliceArgs(args, startIndex) { * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ -/* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { @@ -1220,7 +1392,7 @@ function bind(self, fn) { : fn.call(self); }; } else { - // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) + // In IE, native methods are not functions so they cannot be bound (note: they don't need to be). return fn; } } @@ -1233,7 +1405,7 @@ function toJsonReplacer(key, value) { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; - } else if (value && document === value) { + } else if (value && window.document === value) { val = '$DOCUMENT'; } else if (isScope(value)) { val = '$SCOPE'; @@ -1251,12 +1423,33 @@ function toJsonReplacer(key, value) { * * @description * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be - * stripped since angular uses this notation internally. + * stripped since AngularJS uses this notation internally. * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. + * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON. * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace. * If set to an integer, the JSON output will contain that many spaces per indentation. * @returns {string|undefined} JSON-ified string representing `obj`. + * @knownIssue + * + * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date` + * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the + * `Date.prototype.toJSON` method as follows: + * + * ``` + * var _DatetoJSON = Date.prototype.toJSON; + * Date.prototype.toJSON = function() { + * try { + * return _DatetoJSON.call(this); + * } catch(e) { + * if (e instanceof RangeError) { + * return null; + * } + * throw e; + * } + * }; + * ``` + * + * See https://github.com/angular/angular.js/pull/14221 for more information. */ function toJson(obj, pretty) { if (isUndefined(obj)) return undefined; @@ -1288,10 +1481,11 @@ function fromJson(json) { var ALL_COLONS = /:/g; function timezoneToOffset(timezone, fallback) { + // Support: IE 9-11 only, Edge 13-15+ // IE/Edge do not "understand" colon (`:`) in timezone timezone = timezone.replace(ALL_COLONS, ''); var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; - return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; } @@ -1314,18 +1508,13 @@ function convertTimezoneToLocal(date, timezone, reverse) { * @returns {string} Returns the string representation of the element. */ function startingTag(element) { - element = jqLite(element).clone(); - try { - // turns out IE does not let you set .html() on elements which - // are not allowed to have children. So we just ignore it. - element.empty(); - } catch (e) {} - var elemHtml = jqLite('
').append(element).html(); + element = jqLite(element).clone().empty(); + var elemHtml = jqLite('
').append(element).html(); try { return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); + replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); } catch (e) { return lowercase(elemHtml); } @@ -1347,7 +1536,7 @@ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); } catch (e) { - // Ignore any invalid uri component + // Ignore any invalid uri component. } } @@ -1358,7 +1547,7 @@ function tryDecodeURIComponent(value) { */ function parseKeyValue(/**string*/keyValue) { var obj = {}; - forEach((keyValue || "").split('&'), function(keyValue) { + forEach((keyValue || '').split('&'), function(keyValue) { var splitPoint, key, val; if (keyValue) { key = keyValue = keyValue.replace(/\+/g,'%20'); @@ -1423,7 +1612,7 @@ function encodeUriSegment(val) { * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) + * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG @@ -1453,6 +1642,58 @@ function getNgAttribute(element, ngAttr) { return null; } +function allowAutoBootstrap(document) { + var script = document.currentScript; + + if (!script) { + // Support: IE 9-11 only + // IE does not have `document.currentScript` + return true; + } + + // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack + if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) { + return false; + } + + var attributes = script.attributes; + var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')]; + + return srcs.every(function(src) { + if (!src) { + return true; + } + if (!src.value) { + return false; + } + + var link = document.createElement('a'); + link.href = src.value; + + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for banned URL schemes. + return true; + } + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { + case 'http:': + case 'https:': + case 'ftp:': + case 'blob:': + case 'file:': + case 'data:': + return true; + default: + return false; + } + }); +} + +// Cached as it has to run during loading so that document.currentScript is available. +var isAutoBootstrapAllowed = allowAutoBootstrap(window.document); + /** * @ngdoc directive * @name ngApp @@ -1473,10 +1714,17 @@ function getNgAttribute(element, ngAttr) { * designates the **root element** of the application and is typically placed near the root element * of the page - e.g. on the `` or `` tags. * - * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * There are a few things to keep in mind when using `ngApp`: + * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. + * - AngularJS applications cannot be nested within each other. + * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`. + * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and + * {@link ngRoute.ngView `ngView`}. + * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, + * causing animations to stop working and making the injector inaccessible from outside the app. * * You can specify an **AngularJS module** to be used as the root module for the application. This * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It @@ -1487,9 +1735,13 @@ function getNgAttribute(element, ngAttr) { * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` * would not be resolved to `3`. * + * @example + * + * ### Simple Usage + * * `ngApp` is the easiest, and most common way to bootstrap an application. * - +
I can add: {{a}} + {{b}} = {{ a+b }} @@ -1503,9 +1755,13 @@ function getNgAttribute(element, ngAttr) { * + * @example + * + * ### With `ngStrictDi` + * * Using `ngStrictDi`, you would see something like this: * - +
@@ -1554,7 +1810,7 @@ function getNgAttribute(element, ngAttr) { }]) .controller('GoodController2', GoodController2); function GoodController2($scope) { - $scope.name = "World"; + $scope.name = 'World'; } GoodController2.$inject = ['$scope']; @@ -1585,7 +1841,7 @@ function angularInit(element, bootstrap) { module, config = {}; - // The element `element` has priority over any other element + // The element `element` has priority over any other element. forEach(ngAttrPrefixes, function(prefix) { var name = prefix + 'app'; @@ -1604,7 +1860,12 @@ function angularInit(element, bootstrap) { } }); if (appElement) { - config.strictDi = getNgAttribute(appElement, "strict-di") !== null; + if (!isAutoBootstrapAllowed) { + window.console.error('AngularJS: disabling automatic bootstrap. @@ -7145,11 +8216,11 @@ function $TemplateCacheProvider() { it('should auto compile', function() { var textarea = $('textarea'); var output = $('div[compile]'); - // The initial state reads 'Hello Angular'. - expect(output.getText()).toBe('Hello Angular'); + // The initial state reads 'Hello AngularJS'. + expect(output.getText()).toBe('Hello AngularJS'); textarea.clear(); textarea.sendKeys('{{name}}!'); - expect(output.getText()).toBe('Angular!'); + expect(output.getText()).toBe('AngularJS!'); }); @@ -7196,149 +8267,554 @@ function $TemplateCacheProvider() { * } * ``` * * `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. + * 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. * - * After linking the view is not updated until after a call to $digest which typically is done by - * Angular automatically. + * After linking the view is not updated until after a call to `$digest`, which typically is done by + * AngularJS automatically. * * If you need access to the bound view, there are two ways to do it: * * - If you are not asking the linking function to clone the template, create the DOM element(s) * before you send them to the compiler and keep this reference around. * ```js - * var element = $compile('

{{total}}

')(scope); + * var element = angular.element('

{{total}}

'); + * $compile(element)(scope); * ``` * * - if on the other hand, you need the element to be cloned, the view reference from the original * example would not point to the clone, but rather to the original template that was cloned. In - * this case, you can access the clone via the cloneAttachFn: + * this case, you can access the clone either via the `cloneAttachFn` or the value returned by the + * linking function: * ```js - * var templateElement = angular.element('

{{total}}

'), - * scope = ....; - * + * var templateElement = angular.element('

{{total}}

'); * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { - * //attach the clone to DOM document at the right place + * // Attach the clone to DOM document at the right place. * }); * - * //now we have reference to the cloned DOM via `clonedElement` + * // Now we have reference to the cloned DOM via `clonedElement`. + * // NOTE: The `clonedElement` returned by the linking function is the same as the + * // `clonedElement` passed to `cloneAttachFn`. * ``` * * * For information on how the compiler works, see the - * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. - */ + * {@link guide/compiler AngularJS HTML Compiler} section of the Developer Guide. + * + * @knownIssue + * + * ### Double Compilation + * + Double compilation occurs when an already compiled part of the DOM gets + compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, + and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it + section on double compilation} for an in-depth explanation and ways to avoid it. -var $compileMinErr = minErr('$compile'); + * @knownIssue -/** - * @ngdoc provider - * @name $compileProvider + ### Issues with `replace: true` + * + *
+ * **Note**: {@link $compile#-replace- `replace: true`} is deprecated and not recommended to use, + * mainly due to the issues listed here. It has been completely removed in the new Angular. + *
+ * + * #### Attribute values are not merged + * + * When a `replace` directive encounters the same attribute on the original and the replace node, + * it will simply deduplicate the attribute and join the values with a space or with a `;` in case of + * the `style` attribute. + * ```html + * Original Node: + * Replace Template: + * Result: + * ``` + * + * That means attributes that contain AngularJS expressions will not be merged correctly, e.g. + * {@link ngShow} or {@link ngClass} will cause a {@link $parse} error: + * + * ```html + * Original Node: + * Replace Template: + * Result: + * ``` + * + * See issue [#5695](https://github.com/angular/angular.js/issues/5695). + * + * #### Directives are not deduplicated before compilation + * + * When the original node and the replace template declare the same directive(s), they will be + * {@link guide/compiler#double-compilation-and-how-to-avoid-it compiled twice} because the compiler + * does not deduplicate them. In many cases, this is not noticeable, but e.g. {@link ngModel} will + * attach `$formatters` and `$parsers` twice. + * + * See issue [#2573](https://github.com/angular/angular.js/issues/2573). + * + * #### `transclude: element` in the replace template root can have unexpected effects + * + * When the replace template has a directive at the root node that uses + * {@link $compile#-transclude- `transclude: element`}, e.g. + * {@link ngIf} or {@link ngRepeat}, the DOM structure or scope inheritance can be incorrect. + * See the following issues: + * + * - Incorrect scope on replaced element: + * [#9837](https://github.com/angular/angular.js/issues/9837) + * - Different DOM between `template` and `templateUrl`: + * [#10612](https://github.com/angular/angular.js/issues/14326) * - * @description */ -$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; -function $CompileProvider($provide, $$sanitizeUriProvider) { - var hasDirectives = {}, - Suffix = 'Directive', - 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 - // 'on' and be composed of only English letters. - var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; - var bindingCache = createMap(); - - function parseIsolateBindings(scope, directiveName, isController) { - var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; - - var bindings = createMap(); - - forEach(scope, function(definition, scopeName) { - if (definition in bindingCache) { - bindings[scopeName] = bindingCache[definition]; - return; - } - var match = definition.match(LOCAL_REGEXP); - - if (!match) { - throw $compileMinErr('iscp', - "Invalid {3} for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", - directiveName, scopeName, definition, - (isController ? "controller bindings definition" : - "isolate scope definition")); - } - - bindings[scopeName] = { - mode: match[1][0], - collection: match[2] === '*', - optional: match[3] === '?', - attrName: match[4] || scopeName - }; - if (match[4]) { - bindingCache[definition] = bindings[scopeName]; - } - }); - - return bindings; - } - - function parseDirectiveBindings(directive, directiveName) { - var bindings = { - isolateScope: null, - bindToController: null - }; - if (isObject(directive.scope)) { - if (directive.bindToController === true) { - bindings.bindToController = parseIsolateBindings(directive.scope, - directiveName, true); - bindings.isolateScope = {}; - } else { - bindings.isolateScope = parseIsolateBindings(directive.scope, - directiveName, false); - } - } - if (isObject(directive.bindToController)) { - bindings.bindToController = - parseIsolateBindings(directive.bindToController, directiveName, true); - } - if (isObject(bindings.bindToController)) { - var controller = directive.controller; - var controllerAs = directive.controllerAs; - if (!controller) { - // There is no controller, there may or may not be a controllerAs property - throw $compileMinErr('noctrl', - "Cannot bind to controller without directive '{0}'s controller.", - directiveName); - } else if (!identifierForController(controller, controllerAs)) { - // There is a controller, but no identifier or controllerAs property - throw $compileMinErr('noident', - "Cannot bind to controller without identifier for directive '{0}'.", - directiveName); - } - } - return bindings; - } - function assertValidDirectiveName(name) { - var letter = name.charAt(0); +/** + * @ngdoc directive + * @name ngProp + * @restrict A + * @element ANY + * + * @usage + * + * ```html + * + * + * ``` + * + * or with uppercase letters in property (e.g. "propName"): + * + * + * ```html + * + * + * ``` + * + * + * @description + * The `ngProp` directive binds an expression to a DOM element property. + * `ngProp` allows writing to arbitrary properties by including + * the property name in the attribute, e.g. `ng-prop-value="'my value'"` binds 'my value' to + * the `value` property. + * + * Usually, it's not necessary to write to properties in AngularJS, as the built-in directives + * handle the most common use cases (instead of the above example, you would use {@link ngValue}). + * + * However, [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements) + * often use custom properties to hold data, and `ngProp` can be used to provide input to these + * custom elements. + * + * ## Binding to camelCase properties + * + * Since HTML attributes are case-insensitive, camelCase properties like `innerHTML` must be escaped. + * AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so + * `innerHTML` must be written as `ng-prop-inner_h_t_m_l="expression"` (Note that this is just an + * example, and for binding HTML {@link ngBindHtml} should be used. + * + * ## Security + * + * Binding expressions to arbitrary properties poses a security risk, as properties like `innerHTML` + * can insert potentially dangerous HTML into the application, e.g. script tags that execute + * malicious code. + * For this reason, `ngProp` applies Strict Contextual Escaping with the {@link ng.$sce $sce service}. + * This means vulnerable properties require their content to be "trusted", based on the + * context of the property. For example, the `innerHTML` is in the `HTML` context, and the + * `iframe.src` property is in the `RESOURCE_URL` context, which requires that values written to + * this property are trusted as a `RESOURCE_URL`. + * + * This can be set explicitly by calling $sce.trustAs(type, value) on the value that is + * trusted before passing it to the `ng-prop-*` directive. There are exist shorthand methods for + * each context type in the form of {@link ng.$sce#trustAsResourceUrl $sce.trustAsResourceUrl()} et al. + * + * In some cases you can also rely upon automatic sanitization of untrusted values - see below. + * + * Based on the context, other options may exist to mark a value as trusted / configure the behavior + * of {@link ng.$sce}. For example, to restrict the `RESOURCE_URL` context to specific origins, use + * the {@link $sceDelegateProvider#trustedResourceUrlList trustedResourceUrlList()} + * and {@link $sceDelegateProvider#bannedResourceUrlList bannedResourceUrlList()}. + * + * {@link ng.$sce#what-trusted-context-types-are-supported- Find out more about the different context types}. + * + * ### HTML Sanitization + * + * By default, `$sce` will throw an error if it detects untrusted HTML content, and will not bind the + * content. + * However, if you include the {@link ngSanitize ngSanitize module}, it will try to sanitize the + * potentially dangerous HTML, e.g. strip non-trusted tags and attributes when binding to + * `innerHTML`. + * + * @example + * ### Binding to different contexts + * + * + * + * angular.module('exampleNgProp', []) + * .component('main', { + * templateUrl: 'main.html', + * controller: function($sce) { + * this.safeContent = 'Safe content'; + * this.unsafeContent = ''; + * this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent); + * } + * }); + * + * + *
+ *
+ * Binding to a property without security context: + *
+ * innerText (safeContent) + *
+ * + *
+ * "Safe" content that requires a security context will throw because the contents could potentially be dangerous ... + *
+ * innerHTML (safeContent) + *
+ * + *
+ * ... so that actually dangerous content cannot be executed: + *
+ * innerHTML (unsafeContent) + *
+ * + *
+ * ... but unsafe Content that has been trusted explicitly works - only do this if you are 100% sure! + *
+ * innerHTML (trustedUnsafeContent) + *
+ *
+ *
+ * + *
+ *
+ * + * .prop-unit { + * margin-bottom: 10px; + * } + * + * .prop-binding { + * min-height: 30px; + * border: 1px solid blue; + * } + * + * .prop-note { + * font-family: Monospace; + * } + * + *
+ * + * + * @example + * ### Binding to innerHTML with ngSanitize + * + * + * + * angular.module('exampleNgProp', ['ngSanitize']) + * .component('main', { + * templateUrl: 'main.html', + * controller: function($sce) { + * this.safeContent = 'Safe content'; + * this.unsafeContent = ''; + * this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent); + * } + * }); + * + * + *
+ *
+ * "Safe" content will be sanitized ... + *
+ * innerHTML (safeContent) + *
+ * + *
+ * ... as will dangerous content: + *
+ * innerHTML (unsafeContent) + *
+ * + *
+ * ... and content that has been trusted explicitly works the same as without ngSanitize: + *
+ * innerHTML (trustedUnsafeContent) + *
+ *
+ *
+ * + *
+ *
+ * + * .prop-unit { + * margin-bottom: 10px; + * } + * + * .prop-binding { + * min-height: 30px; + * border: 1px solid blue; + * } + * + * .prop-note { + * font-family: Monospace; + * } + * + *
+ * + */ + +/** @ngdoc directive + * @name ngOn + * @restrict A + * @element ANY + * + * @usage + * + * ```html + * + * + * ``` + * + * or with uppercase letters in property (e.g. "eventName"): + * + * + * ```html + * + * + * ``` + * + * @description + * The `ngOn` directive adds an event listener to a DOM element via + * {@link angular.element angular.element().on()}, and evaluates an expression when the event is + * fired. + * `ngOn` allows adding listeners for arbitrary events by including + * the event name in the attribute, e.g. `ng-on-drop="onDrop()"` executes the 'onDrop()' expression + * when the `drop` event is fired. + * + * AngularJS provides specific directives for many events, such as {@link ngClick}, so in most + * cases it is not necessary to use `ngOn`. However, AngularJS does not support all events + * (e.g. the `drop` event in the example above), and new events might be introduced in later DOM + * standards. + * + * Another use-case for `ngOn` is listening to + * [custom events](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events) + * fired by + * [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements). + * + * ## Binding to camelCase properties + * + * Since HTML attributes are case-insensitive, camelCase properties like `myEvent` must be escaped. + * AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so + * `myEvent` must be written as `ng-on-my_event="expression"`. + * + * @example + * ### Bind to built-in DOM events + * + * + * + * angular.module('exampleNgOn', []) + * .component('main', { + * templateUrl: 'main.html', + * controller: function() { + * this.clickCount = 0; + * this.mouseoverCount = 0; + * + * this.loadingState = 0; + * } + * }); + * + * + *
+ * This is equivalent to `ngClick` and `ngMouseover`:
+ *
+ * clickCount: {{$ctrl.clickCount}}
+ * mouseover: {{$ctrl.mouseoverCount}} + * + *
+ * + * For the `error` and `load` event on images no built-in AngularJS directives exist:
+ *
+ *
+ * Image is loading + * Image load error + * Image loaded successfully + *
+ *
+ *
+ * + *
+ *
+ *
+ * + * + * @example + * ### Bind to custom DOM events + * + * + * + * angular.module('exampleNgOn', []) + * .component('main', { + * templateUrl: 'main.html', + * controller: function() { + * this.eventLog = ''; + * + * this.listener = function($event) { + * this.eventLog = 'Event with type "' + $event.type + '" fired at ' + $event.detail; + * }; + * } + * }) + * .component('childComponent', { + * templateUrl: 'child.html', + * controller: function($element) { + * this.fireEvent = function() { + * var event = new CustomEvent('customtype', { detail: new Date()}); + * + * $element[0].dispatchEvent(event); + * }; + * } + * }); + * + * + *
+ * Event log: {{$ctrl.eventLog}} + *
+ * + + * + * + *
+ *
+ *
+ */ + +var $compileMinErr = minErr('$compile'); + +function UNINITIALIZED_VALUE() {} +var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE(); + +/** + * @ngdoc provider + * @name $compileProvider + * + * @description + */ +$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; +/** @this */ +function $CompileProvider($provide, $$sanitizeUriProvider) { + var hasDirectives = {}, + Suffix = 'Directive', + 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 + // 'on' and be composed of only English letters. + var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; + var bindingCache = createMap(); + + function parseIsolateBindings(scope, directiveName, isController) { + var LOCAL_REGEXP = /^([@&]|[=<](\*?))(\??)\s*([\w$]*)$/; + + var bindings = createMap(); + + forEach(scope, function(definition, scopeName) { + definition = definition.trim(); + + if (definition in bindingCache) { + bindings[scopeName] = bindingCache[definition]; + return; + } + var match = definition.match(LOCAL_REGEXP); + + if (!match) { + throw $compileMinErr('iscp', + 'Invalid {3} for directive \'{0}\'.' + + ' Definition: {... {1}: \'{2}\' ...}', + directiveName, scopeName, definition, + (isController ? 'controller bindings definition' : + 'isolate scope definition')); + } + + bindings[scopeName] = { + mode: match[1][0], + collection: match[2] === '*', + optional: match[3] === '?', + attrName: match[4] || scopeName + }; + if (match[4]) { + bindingCache[definition] = bindings[scopeName]; + } + }); + + return bindings; + } + + function parseDirectiveBindings(directive, directiveName) { + var bindings = { + isolateScope: null, + bindToController: null + }; + if (isObject(directive.scope)) { + if (directive.bindToController === true) { + bindings.bindToController = parseIsolateBindings(directive.scope, + directiveName, true); + bindings.isolateScope = {}; + } else { + bindings.isolateScope = parseIsolateBindings(directive.scope, + directiveName, false); + } + } + if (isObject(directive.bindToController)) { + bindings.bindToController = + parseIsolateBindings(directive.bindToController, directiveName, true); + } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); + } + return bindings; + } + + function assertValidDirectiveName(name) { + var letter = name.charAt(0); if (!letter || letter !== lowercase(letter)) { - throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name); + throw $compileMinErr('baddir', 'Directive/Component name \'{0}\' is invalid. The first character must be a lowercase letter', name); } if (name !== name.trim()) { throw $compileMinErr('baddir', - "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces", + 'Directive/Component name \'{0}\' is invalid. The name should not contain leading or trailing whitespaces', name); } } + function getDirectiveRequire(directive) { + var require = directive.require || (directive.controller && directive.name); + + if (!isArray(require) && isObject(require)) { + forEach(require, function(value, key) { + var match = value.match(REQUIRE_PREFIX_REGEXP); + var name = value.substring(match[0].length); + if (!name) require[key] = match[0] + key; + }); + } + + return require; + } + + function getDirectiveRestrict(restrict, name) { + if (restrict && !(isString(restrict) && /[EACM]/.test(restrict))) { + throw $compileMinErr('badrestrict', + 'Restrict property \'{0}\' of directive \'{1}\' is invalid', + restrict, + name); + } + + return restrict || 'EA'; + } + /** * @ngdoc method * @name $compileProvider#directive @@ -7347,14 +8823,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @description * Register a new directive with the compiler. * - * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which - * will match as ng-bind), or an object map of directives where the keys are the - * names and the values are the factories. - * @param {Function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. + * @param {string|Object} name Name of the directive in camel-case (i.e. `ngBind` which will match + * as `ng-bind`), or an object map of directives where the keys are the names and the values + * are the factories. + * @param {Function|Array} directiveFactory An injectable directive factory function. See the + * {@link guide/directive directive guide} and the {@link $compile compile API} for more info. * @returns {ng.$compileProvider} Self for chaining. */ - this.directive = function registerDirective(name, directiveFactory) { + this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertValidDirectiveName(name); @@ -7375,8 +8852,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.priority = directive.priority || 0; directive.index = index; directive.name = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'EA'; + directive.require = getDirectiveRequire(directive); + directive.restrict = getDirectiveRestrict(directive.restrict, name); directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { @@ -7393,66 +8870,255 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return this; }; + /** + * @ngdoc method + * @name $compileProvider#component + * @module ng + * @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match ``), + * or an object map of components where the keys are the names and the values are the component definition objects. + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}), + * with the following properties (all optional): + * + * - `controller` – `{(string|function()=}` – controller constructor function that should be + * associated with newly created scope or the name of a {@link ng.$compile#-controller- + * registered controller} if passed as a string. An empty `noop` function by default. + * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope. + * If present, the controller will be published to scope under the `controllerAs` name. + * If not present, this will default to be `$ctrl`. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used as the contents of this component. + * Empty string by default. + * + * If `template` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used as the contents of this component. + * + * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties. + * Component properties are always bound to the component controller and not to the scope. + * See {@link ng.$compile#-bindtocontroller- `bindToController`}. + * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled. + * Disabled by default. + * - `require` - `{Object=}` - requires the controllers of other directives and binds them to + * this component's controller. The object keys specify the property names under which the required + * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}. + * - `$...` – additional properties to attach to the directive factory function and the controller + * constructor function. (This is used by the component router to annotate) + * + * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls. + * @description + * Register a **component definition** with the compiler. This is a shorthand for registering a special + * type of directive, which represents a self-contained UI component in your application. Such components + * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`). + * + * Component definitions are very simple and do not require as much configuration as defining general + * directives. Component definitions usually consist only of a template and a controller backing it. + * + * In order to make the definition easier, components enforce best practices like use of `controllerAs`, + * `bindToController`. They always have **isolate scope** and are restricted to elements. + * + * Here are a few examples of how you would usually define components: + * + * ```js + * var myMod = angular.module(...); + * myMod.component('myComp', { + * template: '
My name is {{$ctrl.name}}
', + * controller: function() { + * this.name = 'shahar'; + * } + * }); + * + * myMod.component('myComp', { + * template: '
My name is {{$ctrl.name}}
', + * bindings: {name: '@'} + * }); + * + * myMod.component('myComp', { + * templateUrl: 'views/my-comp.html', + * controller: 'MyCtrl', + * controllerAs: 'ctrl', + * bindings: {name: '@'} + * }); + * + * ``` + * For more examples, and an in-depth guide, see the {@link guide/component component guide}. + * + *
+ * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + this.component = function registerComponent(name, options) { + if (!isString(name)) { + forEach(name, reverseParams(bind(this, registerComponent))); + return this; + } + + var controller = options.controller || function() {}; + + function factory($injector) { + function makeInjectable(fn) { + if (isFunction(fn) || isArray(fn)) { + return /** @this */ function(tElement, tAttrs) { + return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); + }; + } else { + return fn; + } + } + + var template = (!options.template && !options.templateUrl ? '' : options.template); + var ddo = { + controller: controller, + controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', + template: makeInjectable(template), + templateUrl: makeInjectable(options.templateUrl), + transclude: options.transclude, + scope: {}, + bindToController: options.bindings || {}, + restrict: 'E', + require: options.require + }; + + // Copy annotations (starting with $) over to the DDO + forEach(options, function(val, key) { + if (key.charAt(0) === '$') ddo[key] = val; + }); + + return ddo; + } + + // TODO(pete) remove the following `forEach` before we release 1.6.0 + // The component-router@0.2.0 looks for the annotations on the controller constructor + // Nothing in AngularJS looks for annotations on the factory function but we can't remove + // it from 1.5.x yet. + + // Copy any annotation properties (starting with $) over to the factory and controller constructor functions + // These could be used by libraries such as the new component router + forEach(options, function(val, key) { + if (key.charAt(0) === '$') { + factory[key] = val; + // Don't try to copy over annotations to named controller + if (isFunction(controller)) controller[key] = val; + } + }); + + factory.$inject = ['$injector']; + + return this.directive(name, factory); + }; + /** * @ngdoc method - * @name $compileProvider#aHrefSanitizationWhitelist + * @name $compileProvider#aHrefSanitizationTrustedUrlList * @kind function * * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted safe * urls during a[href] sanitization. * * The sanitization is a security measure aimed at preventing XSS attacks via html links. * * Any url about to be assigned to a[href] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` + * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationTrustedUrlList` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.aHrefSanitizationWhitelist = function(regexp) { + this.aHrefSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); + $$sanitizeUriProvider.aHrefSanitizationTrustedUrlList(regexp); return this; } else { - return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); + return $$sanitizeUriProvider.aHrefSanitizationTrustedUrlList(); } }; /** * @ngdoc method - * @name $compileProvider#imgSrcSanitizationWhitelist + * @name $compileProvider#aHrefSanitizationWhitelist + * @kind function + * + * @deprecated + * sinceVersion="1.8.1" + * + * This method is deprecated. Use {@link $compileProvider#aHrefSanitizationTrustedUrlList + * aHrefSanitizationTrustedUrlList} instead. + */ + Object.defineProperty(this, 'aHrefSanitizationWhitelist', { + get: function() { + return this.aHrefSanitizationTrustedUrlList; + }, + set: function(value) { + this.aHrefSanitizationTrustedUrlList = value; + } + }); + + + /** + * @ngdoc method + * @name $compileProvider#imgSrcSanitizationTrustedUrlList * @kind function * * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted safe * urls during img[src] sanitization. * * The sanitization is a security measure aimed at prevent XSS attacks via html links. * * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationTrustedUrlList` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.imgSrcSanitizationWhitelist = function(regexp) { + this.imgSrcSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); + $$sanitizeUriProvider.imgSrcSanitizationTrustedUrlList(regexp); return this; } else { - return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); + return $$sanitizeUriProvider.imgSrcSanitizationTrustedUrlList(); } }; + + /** + * @ngdoc method + * @name $compileProvider#imgSrcSanitizationWhitelist + * @kind function + * + * @deprecated + * sinceVersion="1.8.1" + * + * This method is deprecated. Use {@link $compileProvider#imgSrcSanitizationTrustedUrlList + * imgSrcSanitizationTrustedUrlList} instead. + */ + Object.defineProperty(this, 'imgSrcSanitizationWhitelist', { + get: function() { + return this.imgSrcSanitizationTrustedUrlList; + }, + set: function(value) { + this.imgSrcSanitizationTrustedUrlList = value; + } + }); + /** * @ngdoc method * @name $compileProvider#debugInfoEnabled @@ -7468,7 +9134,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * binding information and a reference to the current scope on to DOM elements. * If enabled, the compiler will add the following to DOM elements that have been bound to the scope * * `ng-binding` CSS class + * * `ng-scope` and `ng-isolated-scope` CSS classes * * `$binding` data property containing an array of the binding expressions + * * Data properties used by the {@link angular.element#methods `scope()`/`isolateScope()` methods} to return + * the element's scope. + * * Placeholder comments will contain information about what directive and binding caused the placeholder. + * E.g. ``. * * You may want to disable this in production for a significant performance boost. See * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. @@ -7481,16 +9152,306 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { debugInfoEnabled = enabled; return this; } - return debugInfoEnabled; - }; + return debugInfoEnabled; + }; + + /** + * @ngdoc method + * @name $compileProvider#strictComponentBindingsEnabled + * + * @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, + * otherwise return the current strictComponentBindingsEnabled state. + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable / disable the strict component bindings check. If enabled, the + * compiler will enforce that all scope / controller bindings of a + * {@link $compileProvider#directive directive} / {@link $compileProvider#component component} + * that are not set as optional with `?`, must be provided when the directive is instantiated. + * If not provided, the compiler will throw the + * {@link error/$compile/missingattr $compile:missingattr error}. + * + * The default value is false. + */ + var strictComponentBindingsEnabled = false; + this.strictComponentBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + strictComponentBindingsEnabled = enabled; + return this; + } + return strictComponentBindingsEnabled; + }; + + var TTL = 10; + /** + * @ngdoc method + * @name $compileProvider#onChangesTtl + * @description + * + * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result + * in several iterations of calls to these hooks. However if an application needs more than the default 10 + * iterations to stabilize then you should investigate what is causing the model to continuously change during + * the `$onChanges` hook execution. + * + * Increasing the TTL could have performance implications, so you should not change it without proper justification. + * + * @param {number} limit The number of `$onChanges` hook iterations. + * @returns {number|object} the current limit (or `this` if called as a setter for chaining) + */ + this.onChangesTtl = function(value) { + if (arguments.length) { + TTL = value; + return this; + } + return TTL; + }; + + var commentDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#commentDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on comments should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on comments for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check comments when looking for directives. + * This should however only be used if you are sure that no comment directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on comments + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.commentDirectivesEnabled = function(value) { + if (arguments.length) { + commentDirectivesEnabledConfig = value; + return this; + } + return commentDirectivesEnabledConfig; + }; + + + var cssClassDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#cssClassDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on element classes should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on element classes for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check element classes when looking for directives. + * This should however only be used if you are sure that no class directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on element classes + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.cssClassDirectivesEnabled = function(value) { + if (arguments.length) { + cssClassDirectivesEnabledConfig = value; + return this; + } + return cssClassDirectivesEnabledConfig; + }; + + + /** + * The security context of DOM Properties. + * @private + */ + var PROP_CONTEXTS = createMap(); + + /** + * @ngdoc method + * @name $compileProvider#addPropertySecurityContext + * @description + * + * Defines the security context for DOM properties bound by ng-prop-*. + * + * @param {string} elementName The element name or '*' to match any element. + * @param {string} propertyName The DOM property name. + * @param {string} ctx The {@link $sce} security context in which this value is safe for use, e.g. `$sce.URL` + * @returns {object} `this` for chaining + */ + this.addPropertySecurityContext = function(elementName, propertyName, ctx) { + var key = (elementName.toLowerCase() + '|' + propertyName.toLowerCase()); + + if (key in PROP_CONTEXTS && PROP_CONTEXTS[key] !== ctx) { + throw $compileMinErr('ctxoverride', 'Property context \'{0}.{1}\' already set to \'{2}\', cannot override to \'{3}\'.', elementName, propertyName, PROP_CONTEXTS[key], ctx); + } + + PROP_CONTEXTS[key] = ctx; + return this; + }; + + /* Default property contexts. + * + * Copy of https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58 + * Changing: + * - SecurityContext.* => SCE_CONTEXTS/$sce.* + * - STYLE => CSS + * - various URL => MEDIA_URL + * - *|formAction, form|action URL => RESOURCE_URL (like the attribute) + */ + (function registerNativePropertyContexts() { + function registerContext(ctx, values) { + forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; }); + } + + registerContext(SCE_CONTEXTS.HTML, [ + 'iframe|srcdoc', + '*|innerHTML', + '*|outerHTML' + ]); + registerContext(SCE_CONTEXTS.CSS, ['*|style']); + registerContext(SCE_CONTEXTS.URL, [ + 'area|href', 'area|ping', + 'a|href', 'a|ping', + 'blockquote|cite', + 'body|background', + 'del|cite', + 'input|src', + 'ins|cite', + 'q|cite' + ]); + registerContext(SCE_CONTEXTS.MEDIA_URL, [ + 'audio|src', + 'img|src', 'img|srcset', + 'source|src', 'source|srcset', + 'track|src', + 'video|src', 'video|poster' + ]); + registerContext(SCE_CONTEXTS.RESOURCE_URL, [ + '*|formAction', + 'applet|code', 'applet|codebase', + 'base|href', + 'embed|src', + 'frame|src', + 'form|action', + 'head|profile', + 'html|manifest', + 'iframe|src', + 'link|href', + 'media|src', + 'object|codebase', 'object|data', + 'script|src' + ]); + })(); + + + this.$get = [ + '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', + '$controller', '$rootScope', '$sce', '$animate', + function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, + $controller, $rootScope, $sce, $animate) { + + var SIMPLE_ATTR_NAME = /^\w/; + var specialAttrHolder = window.document.createElement('div'); + + + var commentDirectivesEnabled = commentDirectivesEnabledConfig; + var cssClassDirectivesEnabled = cssClassDirectivesEnabledConfig; + + + var onChangesTtl = TTL; + // The onChanges hooks should all be run together in a single digest + // When changes occur, the call to trigger their hooks will be added to this queue + var onChangesQueue; + + // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest + function flushOnChangesQueue() { + try { + if (!(--onChangesTtl)) { + // We have hit the TTL limit so reset everything + onChangesQueue = undefined; + throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL); + } + // We must run this hook in an apply since the $$postDigest runs outside apply + $rootScope.$apply(function() { + for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) { + try { + onChangesQueue[i](); + } catch (e) { + $exceptionHandler(e); + } + } + // Reset the queue to trigger a new schedule next time there is a change + onChangesQueue = undefined; + }); + } finally { + onChangesTtl++; + } + } - this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', - '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, - $controller, $rootScope, $sce, $animate, $$sanitizeUri) { - var Attributes = function(element, attributesToCopy) { + function sanitizeSrcset(value, invokeType) { + if (!value) { + return value; + } + if (!isString(value)) { + throw $compileMinErr('srcset', 'Can\'t pass trusted values to `{0}`: "{1}"', invokeType, value.toString()); + } + + // Such values are a bit too complex to handle automatically inside $sce. + // Instead, we sanitize each of the URIs individually, which works, even dynamically. + + // It's not possible to work around this using `$sce.trustAsMediaUrl`. + // If you want to programmatically set explicitly trusted unsafe URLs, you should use + // `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the + // `ng-bind-html` directive. + + var result = ''; + + // first check if there are spaces because it's not the same pattern + var trimmedSrcset = trim(value); + // ( 999x ,| 999w ,| ,|, ) + var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; + var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; + + // split srcset into tuple of uri and descriptor except for the last item + var rawUris = trimmedSrcset.split(pattern); + + // for each tuples + var nbrUrisWith2parts = Math.floor(rawUris.length / 2); + for (var i = 0; i < nbrUrisWith2parts; i++) { + var innerIdx = i * 2; + // sanitize the uri + result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx])); + // add the descriptor + result += ' ' + trim(rawUris[innerIdx + 1]); + } + + // split the last item into uri and descriptor + var lastTuple = trim(rawUris[i * 2]).split(/\s/); + + // sanitize the last uri + result += $sce.getTrustedMediaUrl(trim(lastTuple[0])); + + // and add the last descriptor if any + if (lastTuple.length === 2) { + result += (' ' + trim(lastTuple[1])); + } + return result; + } + + + function Attributes(element, attributesToCopy) { if (attributesToCopy) { var keys = Object.keys(attributesToCopy); var i, l, key; @@ -7504,7 +9465,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } this.$$element = element; - }; + } Attributes.prototype = { /** @@ -7594,8 +9555,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ $set: function(key, value, writeAttr, attrName) { // TODO: decide whether or not to throw an error if "class" - //is set through this function since it may cause $updateClass to - //become unstable. + // is set through this function since it may cause $updateClass to + // become unstable. var node = this.$$element[0], booleanKey = getBooleanAttrName(node, key), @@ -7625,63 +9586,43 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); - 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' && isDefined(value)) { - // 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; + // Sanitize img[srcset] values. + if (nodeName === 'img' && key === 'srcset') { + this[key] = value = sanitizeSrcset(value, '$set(\'srcset\', value)'); } if (writeAttr !== false) { if (value === null || isUndefined(value)) { this.$$element.removeAttr(attrName); } else { - this.$$element.attr(attrName, value); + if (SIMPLE_ATTR_NAME.test(attrName)) { + // jQuery skips special boolean attrs treatment in XML nodes for + // historical reasons and hence AngularJS cannot freely call + // `.attr(attrName, false) with such attributes. To avoid issues + // in XHTML, call `removeAttr` in such cases instead. + // See https://github.com/jquery/jquery/issues/4249 + if (booleanKey && value === false) { + this.$$element.removeAttr(attrName); + } else { + this.$$element.attr(attrName, value); + } + } else { + setSpecialAttr(this.$$element[0], attrName, value); + } } } // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[observer], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); + if ($$observers) { + forEach($$observers[observer], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + } }, @@ -7723,6 +9664,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + function setSpecialAttr(element, attrName, value) { + // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute` + // so we have to jump through some hoops to get such an attribute + // https://github.com/angular/angular.js/pull/13318 + specialAttrHolder.innerHTML = ''; + var attributes = specialAttrHolder.firstChild.attributes; + var attribute = attributes[0]; + // We have to remove the attribute from its container element before we can add it to the destination element + attributes.removeNamedItem(attribute.name); + attribute.value = value; + element.attributes.setNamedItem(attribute); + } function safeAddClass($element, className) { try { @@ -7736,12 +9689,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' && endSymbol == '}}') + denormalizeTemplate = (startSymbol === '{{' && endSymbol === '}}') ? identity : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); }, - NG_ATTR_BINDING = /^ngAttr[A-Z]/; + NG_PREFIX_BINDING = /^ng(Attr|Prop|On)([A-Z].*)$/; var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/; compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { @@ -7769,6 +9722,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); } : noop; + compile.$$createComment = function(directiveName, comment) { + var content = ''; + if (debugInfoEnabled) { + content = ' ' + (directiveName || '') + ': '; + if (comment) content += comment + ' '; + } + return window.document.createComment(content); + }; + return compile; //================================ @@ -7780,25 +9742,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // modify it. $compileNodes = jqLite($compileNodes); } - - var NOT_EMPTY = /\S+/; - - // 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 - for (var i = 0, len = $compileNodes.length; i < len; i++) { - var domNode = $compileNodes[i]; - - if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) { - jqLiteWrapNode(domNode, $compileNodes[i] = document.createElement('span')); - } - } - var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); compile.$$addScopeClass($compileNodes); var namespace = null; return function publicLinkFn(scope, cloneConnectFn, options) { + if (!$compileNodes) { + throw $compileMinErr('multilink', 'This element has already been linked.'); + } assertArg(scope, 'scope'); if (previousCompileContext && previousCompileContext.needsNewScope) { @@ -7833,7 +9785,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // for call to the link function. // Note: This will already clone the nodes... $linkNode = jqLite( - wrapTemplate(namespace, jqLite('
').append($compileNodes).html()) + 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 @@ -7853,6 +9805,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } @@ -7863,7 +9819,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!node) { return 'html'; } else { - return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; + return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html'; } } @@ -7885,12 +9841,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], + // `nodeList` can be either an element's `.childNodes` (live NodeList) + // or a jqLite/jQuery collection or an array + notLiveList = isArray(nodeList) || (nodeList instanceof jqLite), attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; + for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + // Support: IE 11 only + // Workaround for #11781 and #14924 + if (msie === 11) { + mergeConsecutiveTextNodes(nodeList, i, notLiveList); + } + + // We must always refer to `nodeList[i]` hereafter, + // since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); @@ -7937,7 +9904,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { stableNodeList = new Array(nodeListLength); // create a sparse array by only copying the elements which have a linkFn - for (i = 0; i < linkFns.length; i+=3) { + for (i = 0; i < linkFns.length; i += 3) { idx = linkFns[i]; stableNodeList[idx] = nodeList[idx]; } @@ -7981,9 +9948,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) { + var node = nodeList[idx]; + var parent = node.parentNode; + var sibling; + + if (node.nodeType !== NODE_TYPE_TEXT) { + return; + } + + while (true) { + sibling = parent ? node.nextSibling : nodeList[idx + 1]; + if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) { + break; + } + + node.nodeValue = node.nodeValue + sibling.nodeValue; - var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { + if (sibling.parentNode) { + sibling.parentNode.removeChild(sibling); + } + if (notLiveList && sibling === nodeList[idx + 1]) { + nodeList.splice(idx + 1, 1); + } + } + } + + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { transcludedScope = scope.$new(false, containingScope); @@ -7995,7 +9987,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { transcludeControllers: controllers, futureParentElement: futureParentElement }); - }; + } + + // We need to attach the transclusion slots onto the `boundTranscludeFn` + // so that they are available inside the `controllersBoundTransclude` function + var boundSlots = boundTranscludeFn.$$slots = createMap(); + for (var slotName in transcludeFn.$$slots) { + if (transcludeFn.$$slots[slotName]) { + boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn); + } else { + boundSlots[slotName] = null; + } + } return boundTranscludeFn; } @@ -8027,42 +10030,66 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, + for (var attr, name, nName, value, ngPrefixMatch, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; + var isNgAttr = false, isNgProp = false, isNgEvent = false; + var multiElementMatch; + attr = nAttrs[j]; name = attr.name; - value = trim(attr.value); + value = attr.value; + + nName = directiveNormalize(name.toLowerCase()); + + // Support ng-attr-*, ng-prop-* and ng-on-* + if ((ngPrefixMatch = nName.match(NG_PREFIX_BINDING))) { + isNgAttr = ngPrefixMatch[1] === 'Attr'; + isNgProp = ngPrefixMatch[1] === 'Prop'; + isNgEvent = ngPrefixMatch[1] === 'On'; - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + // Normalize the non-prefixed name name = name.replace(PREFIX_REGEXP, '') - .substr(8).replace(/_(.)/g, function(match, letter) { + .toLowerCase() + .substr(4 + ngPrefixMatch[1].length).replace(/_(.)/g, function(match, letter) { return letter.toUpperCase(); }); - } - var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE); - if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) { + // Support *-start / *-end multi element directives + } else if ((multiElementMatch = nName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) { attrStartName = name; attrEndName = name.substr(0, name.length - 5) + 'end'; name = name.substr(0, name.length - 6); } - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - if (isNgAttr || !attrs.hasOwnProperty(nName)) { + if (isNgProp || isNgEvent) { + attrs[nName] = value; + attrsMap[nName] = attr.name; + + if (isNgProp) { + addPropertyDirective(node, directives, nName, name); + } else { + addEventDirective(directives, nName, name); + } + } else { + // Update nName for cases where a prefix was removed + // NOTE: the .toLowerCase() is unnecessary and causes https://github.com/angular/angular.js/issues/16624 for ng-attr-* + nName = directiveNormalize(name.toLowerCase()); + attrsMap[nName] = name; + + if (isNgAttr || !attrs.hasOwnProperty(nName)) { attrs[nName] = value; if (getBooleanAttrName(node, nName)) { attrs[nName] = true; // presence means true } + } + + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); } - addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); } if (nodeName === 'input' && node.getAttribute('type') === 'hidden') { @@ -8072,13 +10099,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } // use class as directive + if (!cssClassDirectivesEnabled) break; className = node.className; if (isObject(className)) { // Maybe SVGAnimatedString className = className.animVal; } if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + while ((match = CLASS_DIRECTIVE_REGEXP.exec(className))) { nName = directiveNormalize(match[2]); if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); @@ -8088,29 +10116,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } break; case NODE_TYPE_TEXT: /* Text Node */ - if (msie === 11) { - // Workaround for #11781 - while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) { - node.nodeValue = node.nodeValue + node.nextSibling.nodeValue; - node.parentNode.removeChild(node.nextSibling); - } - } addTextInterpolateDirective(directives, node.nodeValue); break; case NODE_TYPE_COMMENT: /* Comment */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read - // comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } + if (!commentDirectivesEnabled) break; + collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective); break; } @@ -8118,8 +10128,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return directives; } + function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) { + // function created because of performance, try/catch disables + // the optimization of the whole function #14848 + try { + var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); + if (match) { + var nName = directiveNormalize(match[1]); + if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[2]); + } + } + } catch (e) { + // turns out that under some circumstances IE9 throws errors when one attempts to read + // comment's node value. + // Just ignore it and continue. (Can't seem to reproduce in test case.) + } + } + /** - * Given a node with an directive-start it collects all of the siblings until it finds + * Given a node with a directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart @@ -8133,10 +10161,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { do { if (!node) { throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", + 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.', attrStart, attrEnd); } - if (node.nodeType == NODE_TYPE_ELEMENT) { + if (node.nodeType === NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -8159,12 +10187,41 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {Function} */ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function(scope, element, attrs, controllers, transcludeFn) { + return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) { element = groupScan(element[0], attrStart, attrEnd); return linkFn(scope, element, attrs, controllers, transcludeFn); }; } + /** + * A function generator that is used to support both eager and lazy compilation + * linking function. + * @param eager + * @param $compileNodes + * @param transcludeFn + * @param maxPriority + * @param ignoreDirective + * @param previousCompileContext + * @returns {Function} + */ + function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { + var compiled; + + if (eager) { + return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + } + return /** @this */ function lazyCompilation() { + if (!compiled) { + compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + + // Null out all of these references in order to make them eligible for garbage collection + // since this is a potentially long lived closure + $compileNodes = transcludeFn = previousCompileContext = null; + } + return compiled.apply(this, arguments); + }; + } + /** * Once the directives have been collected, their compile functions are executed. This method * is responsible for inlining directive templates as well as terminating the application @@ -8209,6 +10266,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceDirective = originalReplaceDirective, childTranscludeFn = transcludeFn, linkFn, + didScanForMultipleTransclusion = false, + mightHaveMultipleTransclusionError = false, directiveValue; // executes all directives on the current element @@ -8227,7 +10286,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { break; // prevent further processing of directives } - if (directiveValue = directive.scope) { + directiveValue = directive.scope; + + if (directiveValue) { // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives @@ -8251,15 +10312,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directiveName = directive.name; + // If we encounter a condition that can result in transclusion on the directive, + // then scan ahead in the remaining directives for others that may cause a multiple + // transclusion error to be thrown during the compilation process. If a matching directive + // is found, then we know that when we encounter a transcluded directive, we need to eagerly + // compile the `transclude` function rather than doing it lazily in order to throw + // exceptions at the correct time + if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template)) + || (directive.transclude && !directive.$$tlb))) { + var candidateDirective; + + for (var scanningIndex = i + 1; (candidateDirective = directives[scanningIndex++]);) { + if ((candidateDirective.transclude && !candidateDirective.$$tlb) + || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) { + mightHaveMultipleTransclusionError = true; + break; + } + } + + didScanForMultipleTransclusion = true; + } + if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; controllerDirectives = controllerDirectives || createMap(); - assertNoDuplicate("'" + directiveName + "' controller", + assertNoDuplicate('\'' + directiveName + '\' controller', controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; } - if (directiveValue = directive.transclude) { + directiveValue = directive.transclude; + + if (directiveValue) { hasTranscludeDirective = true; // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. @@ -8270,17 +10353,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nonTlbTranscludeDirective = directive; } - if (directiveValue == 'element') { + if (directiveValue === 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; $template = $compileNode; $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + - templateAttrs[directiveName] + ' ')); + jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName])); compileNode = $compileNode[0]; replaceWith(jqCollection, sliceArgs($template), compileNode); - childTranscludeFn = compile($template, transcludeFn, terminalPriority, + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: // - controllerDirectives - otherwise we'll create duplicates controllers @@ -8292,10 +10374,72 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nonTlbTranscludeDirective: nonTlbTranscludeDirective }); } else { - $template = jqLite(jqLiteClone(compileNode)).contents(); + + var slots = createMap(); + + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { + + // We have transclusion slots, + // collect them up, compile them and store their transclusion functions + $template = window.document.createDocumentFragment(); + + var slotMap = createMap(); + var filledSlots = createMap(); + + // Parse the element selectors + forEach(directiveValue, function(elementSelector, slotName) { + // If an element selector starts with a ? then it is optional + var optional = (elementSelector.charAt(0) === '?'); + elementSelector = optional ? elementSelector.substring(1) : elementSelector; + + slotMap[elementSelector] = slotName; + + // We explicitly assign `null` since this implies that a slot was defined but not filled. + // Later when calling boundTransclusion functions with a slot name we only error if the + // slot is `undefined` + slots[slotName] = null; + + // filledSlots contains `true` for all slots that are either optional or have been + // filled. This is used to check that we have not missed any required slots + filledSlots[slotName] = optional; + }); + + // Add the matching elements into their slot + forEach($compileNode.contents(), function(node) { + var slotName = slotMap[directiveNormalize(nodeName_(node))]; + if (slotName) { + filledSlots[slotName] = true; + slots[slotName] = slots[slotName] || window.document.createDocumentFragment(); + slots[slotName].appendChild(node); + } else { + $template.appendChild(node); + } + }); + + // Check for required slots that were not filled + forEach(filledSlots, function(filled, slotName) { + if (!filled) { + throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName); + } + }); + + for (var slotName in slots) { + if (slots[slotName]) { + // Only define a transclusion function if the slot was filled + var slotCompileNodes = jqLite(slots[slotName].childNodes); + slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slotCompileNodes, transcludeFn); + } + } + + $template = jqLite($template.childNodes); + } + $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn, undefined, + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined, undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope}); + childTranscludeFn.$$slots = slots; } } @@ -8319,9 +10463,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', directiveName, ''); } @@ -8361,6 +10505,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceDirective = directive; } + // eslint-disable-next-line no-func-assign nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, @@ -8373,10 +10518,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } else if (directive.compile) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); + var context = directive.$$originalDirective || directive; if (isFunction(linkFn)) { - addLinkFns(null, linkFn, attrStart, attrEnd); + addLinkFns(null, bind(context, linkFn), attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); + addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -8423,77 +10569,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - - function getControllers(directiveName, require, $element, elementControllers) { - var value; - - if (isString(require)) { - var match = require.match(REQUIRE_PREFIX_REGEXP); - var name = require.substring(match[0].length); - var inheritType = match[1] || match[3]; - var optional = match[2] === '?'; - - //If only parents then start at the parent element - if (inheritType === '^^') { - $element = $element.parent(); - //Otherwise attempt getting the controller from elementControllers in case - //the element is transcluded (and has no data) and to avoid .data if possible - } else { - value = elementControllers && elementControllers[name]; - value = value && value.instance; - } - - if (!value) { - var dataName = '$' + name + 'Controller'; - value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); - } - - if (!value && !optional) { - throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", - name, directiveName); - } - } else if (isArray(require)) { - value = []; - for (var i = 0, ii = require.length; i < ii; i++) { - value[i] = getControllers(directiveName, require[i], $element, elementControllers); - } - } - - return value || null; - } - - function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) { - var elementControllers = createMap(); - for (var controllerKey in controllerDirectives) { - var directive = controllerDirectives[controllerKey]; - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }; - - var controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - var controllerInstance = $controller(controller, locals, true, directive.controllerAs); - - // For directives with element transclusion the element is a comment. - // In this case .data will not attach any data. - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - $element.data('$' + directive.name + 'Controller', controllerInstance.instance); - } - return elementControllers; - } - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, - attrs, removeScopeBindingWatches, removeControllerBindingWatches; + var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, + attrs, scopeBindingInfo; if (compileNode === linkNode) { attrs = templateAttrs; @@ -8515,10 +10593,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` transcludeFn = controllersBoundTransclude; transcludeFn.$$boundTransclude = boundTranscludeFn; + // expose the slots on the `$transclude` function + transcludeFn.isSlotFilled = function(slotName) { + return !!boundTranscludeFn.$$slots[slotName]; + }; } if (controllerDirectives) { - elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope); + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective); } if (newIsolateScopeDirective) { @@ -8528,11 +10610,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile.$$addScopeClass($element, true); isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings; - removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope, + scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope, isolateScope.$$isolateBindings, newIsolateScopeDirective); - if (removeScopeBindingWatches) { - isolateScope.$on('$destroy', removeScopeBindingWatches); + if (scopeBindingInfo.removeWatches) { + isolateScope.$on('$destroy', scopeBindingInfo.removeWatches); } } @@ -8542,22 +10624,47 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (controller.identifier && bindings) { - removeControllerBindingWatches = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } - var controllerResult = controller(); - if (controllerResult !== controller.instance) { - // If the controller constructor has a return value, overwrite the instance - // from setupControllers - controller.instance = controllerResult; - $element.data('$' + controllerDirective.name + 'Controller', controllerResult); - removeControllerBindingWatches && removeControllerBindingWatches(); - removeControllerBindingWatches = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy + forEach(controllerDirectives, function(controllerDirective, name) { + var require = controllerDirective.require; + if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { + extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); } - } + }); + + // Handle the init and destroy lifecycle hooks on all controllers that have them + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$onChanges)) { + try { + controllerInstance.$onChanges(controller.bindingInfo.initialChanges); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$onInit)) { + try { + controllerInstance.$onInit(); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$doCheck)) { + controllerScope.$watch(function() { controllerInstance.$doCheck(); }); + controllerInstance.$doCheck(); + } + if (isFunction(controllerInstance.$onDestroy)) { + controllerScope.$on('$destroy', function callOnDestroyHook() { + controllerInstance.$onDestroy(); + }); + } + }); // PRELINKING for (i = 0, ii = preLinkFns.length; i < ii; i++) { @@ -8578,7 +10685,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { scopeToChild = isolateScope; } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + if (childLinkFn) { + childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + } // POSTLINKING for (i = postLinkFns.length - 1; i >= 0; i--) { @@ -8592,13 +10701,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { ); } + // Trigger $postLink lifecycle hooks + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$postLink)) { + controllerInstance.$postLink(); + } + }); + // This is the function that is injected as `$transclude`. // Note: all arguments are optional! - function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) { var transcludeControllers; - // No scope passed in: if (!isScope(scope)) { + slotName = futureParentElement; futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; @@ -8610,9 +10727,104 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!futureParentElement) { futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; } - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + if (slotName) { + // slotTranscludeFn can be one of three things: + // * a transclude function - a filled slot + // * `null` - an optional slot that was not filled + // * `undefined` - a slot that was not declared (i.e. invalid) + var slotTranscludeFn = boundTranscludeFn.$$slots[slotName]; + if (slotTranscludeFn) { + return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } else if (isUndefined(slotTranscludeFn)) { + throw $compileMinErr('noslot', + 'No parent directive that requires a transclusion with slot name "{0}". ' + + 'Element: {1}', + slotName, startingTag($element)); + } + } else { + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } + } + } + } + + function getControllers(directiveName, require, $element, elementControllers) { + var value; + + if (isString(require)) { + var match = require.match(REQUIRE_PREFIX_REGEXP); + var name = require.substring(match[0].length); + var inheritType = match[1] || match[3]; + var optional = match[2] === '?'; + + //If only parents then start at the parent element + if (inheritType === '^^') { + $element = $element.parent(); + //Otherwise attempt getting the controller from elementControllers in case + //the element is transcluded (and has no data) and to avoid .data if possible + } else { + value = elementControllers && elementControllers[name]; + value = value && value.instance; + } + + if (!value) { + var dataName = '$' + name + 'Controller'; + + if (inheritType === '^^' && $element[0] && $element[0].nodeType === NODE_TYPE_DOCUMENT) { + // inheritedData() uses the documentElement when it finds the document, so we would + // require from the element itself. + value = null; + } else { + value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); + } + } + + if (!value && !optional) { + throw $compileMinErr('ctreq', + 'Controller \'{0}\', required by directive \'{1}\', can\'t be found!', + name, directiveName); + } + } else if (isArray(require)) { + value = []; + for (var i = 0, ii = require.length; i < ii; i++) { + value[i] = getControllers(directiveName, require[i], $element, elementControllers); + } + } else if (isObject(require)) { + value = {}; + forEach(require, function(controller, property) { + value[property] = getControllers(directiveName, controller, $element, elementControllers); + }); + } + + return value || null; + } + + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) { + var elementControllers = createMap(); + for (var controllerKey in controllerDirectives) { + var directive = controllerDirectives[controllerKey]; + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }; + + var controller = directive.controller; + if (controller === '@') { + controller = attrs[directive.name]; } + + var controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment. + // In this case .data will not attach any data. + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); } + return elementControllers; } // Depending upon the context in which a directive finds itself it might need to have a new isolated @@ -8648,24 +10860,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasDirectives.hasOwnProperty(name)) { for (var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i < ii; i++) { - try { - directive = directives[i]; - if ((isUndefined(maxPriority) || maxPriority > directive.priority) && - directive.restrict.indexOf(location) != -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); - } - if (!directive.$$bindings) { - var bindings = directive.$$bindings = - parseDirectiveBindings(directive, directive.name); - if (isObject(bindings.isolateScope)) { - directive.$$isolateBindings = bindings.isolateScope; - } + directive = directives[i]; + if ((isUndefined(maxPriority) || maxPriority > directive.priority) && + directive.restrict.indexOf(location) !== -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + } + if (!directive.$$bindings) { + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; } - tDirectives.push(directive); - match = directive; } - } catch (e) { $exceptionHandler(e); } + tDirectives.push(directive); + match = directive; + } } } return match; @@ -8703,14 +10913,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ function mergeTemplateAttributes(dst, src) { var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; + dstAttr = dst.$attr; // reapply the old attributes to the new element forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { + if (key.charAt(0) !== '$') { if (src[key] && src[key] !== value) { - value += (key === 'style' ? ';' : ' ') + src[key]; + if (value.length) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } else { + value = src[key]; + } } dst.$set(key, value, true, srcAttr[key]); } @@ -8718,18 +10931,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // copy the new attributes on the old attrs object forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass($element, value); - dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; - } else if (key == 'style') { - $element.attr('style', $element.attr('style') + ';' + value); - dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; - // `dst` will never contain hasOwnProperty as DOM parser won't let it. - // You will get an "InvalidCharacterError: DOM Exception 5" error if you - // have an attribute like "has-own-property" or "data-has-own-property", etc. - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { + // Check if we already set this attribute in the loop above. + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. + if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') { dst[key] = value; - dstAttr[key] = srcAttr[key]; + + if (key !== 'class' && key !== 'style') { + dstAttr[key] = srcAttr[key]; + } } }); } @@ -8766,9 +10977,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', origAsyncDirective.name, templateUrl); } @@ -8794,7 +11005,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); forEach($rootElement, function(node, i) { - if (node == compileNode) { + if (node === compileNode) { $rootElement[i] = $compileNode[0]; } }); @@ -8831,6 +11042,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; + }).catch(function(error) { + if (isError(error)) { + $exceptionHandler(error); + } }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { @@ -8909,7 +11124,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { switch (type) { case 'svg': case 'math': - var wrapper = document.createElement('div'); + var wrapper = window.document.createElement('div'); wrapper.innerHTML = '<' + type + '>' + template + ''; return wrapper.childNodes[0].childNodes; default: @@ -8918,37 +11133,113 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { - return $sce.HTML; - } - var tag = nodeName_(node); - // maction[xlink:href] can source SVG. It's not limited to . - if (attrNormalizedName == "xlinkHref" || - (tag == "form" && attrNormalizedName == "action") || - (tag != "img" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { - return $sce.RESOURCE_URL; - } + function getTrustedAttrContext(nodeName, attrNormalizedName) { + if (attrNormalizedName === 'srcdoc') { + return $sce.HTML; + } + // All nodes with src attributes require a RESOURCE_URL value, except for + // img and various html5 media nodes, which require the MEDIA_URL context. + if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') { + if (['img', 'video', 'audio', 'source', 'track'].indexOf(nodeName) === -1) { + return $sce.RESOURCE_URL; + } + return $sce.MEDIA_URL; + } else if (attrNormalizedName === 'xlinkHref') { + // Some xlink:href are okay, most aren't + if (nodeName === 'image') return $sce.MEDIA_URL; + if (nodeName === 'a') return $sce.URL; + return $sce.RESOURCE_URL; + } else if ( + // Formaction + (nodeName === 'form' && attrNormalizedName === 'action') || + // If relative URLs can go where they are not expected to, then + // all sorts of trust issues can arise. + (nodeName === 'base' && attrNormalizedName === 'href') || + // links can be stylesheets or imports, which can run script in the current origin + (nodeName === 'link' && attrNormalizedName === 'href') + ) { + return $sce.RESOURCE_URL; + } else if (nodeName === 'a' && (attrNormalizedName === 'href' || + attrNormalizedName === 'ngHref')) { + return $sce.URL; + } + } + + function getTrustedPropContext(nodeName, propNormalizedName) { + var prop = propNormalizedName.toLowerCase(); + return PROP_CONTEXTS[nodeName + '|' + prop] || PROP_CONTEXTS['*|' + prop]; + } + + function sanitizeSrcsetPropertyValue(value) { + return sanitizeSrcset($sce.valueOf(value), 'ng-prop-srcset'); + } + function addPropertyDirective(node, directives, attrName, propName) { + if (EVENT_HANDLER_ATTR_REGEXP.test(propName)) { + throw $compileMinErr('nodomevents', 'Property bindings for HTML DOM event properties are disallowed'); + } + + var nodeName = nodeName_(node); + var trustedContext = getTrustedPropContext(nodeName, propName); + + var sanitizer = identity; + // Sanitize img[srcset] + source[srcset] values. + if (propName === 'srcset' && (nodeName === 'img' || nodeName === 'source')) { + sanitizer = sanitizeSrcsetPropertyValue; + } else if (trustedContext) { + sanitizer = $sce.getTrusted.bind($sce, trustedContext); + } + + directives.push({ + priority: 100, + compile: function ngPropCompileFn(_, attr) { + var ngPropGetter = $parse(attr[attrName]); + var ngPropWatch = $parse(attr[attrName], function sceValueOf(val) { + // Unwrap the value to compare the actual inner safe value, not the wrapper object. + return $sce.valueOf(val); + }); + + return { + pre: function ngPropPreLinkFn(scope, $element) { + function applyPropValue() { + var propValue = ngPropGetter(scope); + $element[0][propName] = sanitizer(propValue); + } + + applyPropValue(); + scope.$watch(ngPropWatch, applyPropValue); + } + }; + } + }); } + function addEventDirective(directives, attrName, eventName) { + directives.push( + createEventDirective($parse, $rootScope, $exceptionHandler, attrName, eventName, /*forceAsync=*/false) + ); + } - function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { - var trustedContext = getTrustedContext(node, name); - allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { + var nodeName = nodeName_(node); + var trustedContext = getTrustedAttrContext(nodeName, name); + var mustHaveExpression = !isNgAttr; + var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr; - var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); + var interpolateFn = $interpolate(value, mustHaveExpression, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; - - if (name === "multiple" && nodeName_(node) === "select") { - throw $compileMinErr("selmulti", - "Binding to the 'multiple' attribute is not supported. Element: {0}", + if (name === 'multiple' && nodeName === 'select') { + throw $compileMinErr('selmulti', + 'Binding to the \'multiple\' attribute is not supported. Element: {0}', startingTag(node)); } + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', 'Interpolations for HTML DOM event attributes are disallowed'); + } + directives.push({ priority: 100, compile: function() { @@ -8956,12 +11247,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { pre: function attrInterpolatePreLinkFn(scope, element, attr) { var $$observers = (attr.$$observers || (attr.$$observers = createMap())); - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - "Interpolations for HTML DOM event attributes are disallowed. Please use the " + - "ng- versions (such as ng-click instead of onclick) instead."); - } - // If the attribute has changed since last $interpolate()ed var newValue = attr[name]; if (newValue !== value) { @@ -8990,7 +11275,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); @@ -9021,7 +11306,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if ($rootElement) { for (i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { + if ($rootElement[i] === firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, jj = $rootElement.length; @@ -9049,41 +11334,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { parent.replaceChild(newNode, firstElementToRemove); } - // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? - var fragment = document.createDocumentFragment(); - fragment.appendChild(firstElementToRemove); + // Append all the `elementsToRemove` to a fragment. This will... + // - remove them from the DOM + // - allow them to still be traversed with .nextSibling + // - allow a single fragment.qSA to fetch all elements being removed + var fragment = window.document.createDocumentFragment(); + for (i = 0; i < removeCount; i++) { + fragment.appendChild(elementsToRemove[i]); + } if (jqLite.hasData(firstElementToRemove)) { - // Copy over user data (that includes Angular's $scope etc.). Don't copy private + // Copy over user data (that includes AngularJS's $scope etc.). Don't copy private // data here because there's no public interface in jQuery to do that and copying over // event listeners (which is the main use of private data) wouldn't work anyway. jqLite.data(newNode, jqLite.data(firstElementToRemove)); - // Remove data of the replaced element. We cannot just call .remove() - // on the element it since that would deallocate scope that is needed - // for the new node. Instead, remove the data "manually". - if (!jQuery) { - delete jqLite.cache[firstElementToRemove[jqLite.expando]]; - } else { - // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after - // the replaced element. The cleanData version monkey-patched by Angular would cause - // the scope to be trashed and we do need the very same scope to work with the new - // element. However, we cannot just cache the non-patched version and use it here as - // that would break if another library patches the method after Angular does (one - // example is jQuery UI). Instead, set a flag indicating scope destroying should be - // skipped this one time. - skipDestroyOnNextJQueryCleanData = true; - jQuery.cleanData([firstElementToRemove]); - } + // Remove $destroy event listeners from `firstElementToRemove` + jqLite(firstElementToRemove).off('$destroy'); } - for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { - var element = elementsToRemove[k]; - jqLite(element).remove(); // must do this way to clean up expando - fragment.appendChild(element); - delete elementsToRemove[k]; - } + // Cleanup any data/listeners on the elements and children. + // This includes invoking the $destroy event on any elements with listeners. + jqLite.cleanData(fragment.querySelectorAll('*')); + // Update the jqLite collection to only contain the `newNode` + for (i = 1; i < removeCount; i++) { + delete elementsToRemove[i]; + } elementsToRemove[0] = newNode; elementsToRemove.length = 1; } @@ -9102,26 +11379,39 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } + function strictBindingsCheck(attrName, directiveName) { + if (strictComponentBindingsEnabled) { + throw $compileMinErr('missingattr', + 'Attribute \'{0}\' of \'{1}\' is non-optional and must be set!', + attrName, directiveName); + } + } - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. + // Set up $watches for isolate scope and controller bindings. function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { var removeWatchCollection = []; - forEach(bindings, function(definition, scopeName) { + var initialChanges = {}; + var changes; + + forEach(bindings, function initializeBinding(definition, scopeName) { var attrName = definition.attrName, optional = definition.optional, - mode = definition.mode, // @, =, or & + mode = definition.mode, // @, =, <, or & lastValue, - parentGet, parentSet, compare; + parentGet, parentSet, compare, removeWatch; switch (mode) { case '@': if (!optional && !hasOwnProperty.call(attrs, attrName)) { - destination[scopeName] = attrs[attrName] = void 0; + strictBindingsCheck(attrName, directive.name); + destination[scopeName] = attrs[attrName] = undefined; + } - attrs.$observe(attrName, function(value) { - if (isString(value)) { + removeWatch = attrs.$observe(attrName, function(value) { + if (isString(value) || isBoolean(value)) { + var oldValue = destination[scopeName]; + recordChanges(scopeName, value, oldValue); destination[scopeName] = value; } }); @@ -9132,16 +11422,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // the value is there for use in the link fn destination[scopeName] = $interpolate(lastValue)(scope); } else if (isBoolean(lastValue)) { - // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted + // If the attributes is one of the BOOLEAN_ATTR then AngularJS will have converted // the value to boolean rather than a string, so we special case this situation destination[scopeName] = lastValue; } + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); break; case '=': if (!hasOwnProperty.call(attrs, attrName)) { if (optional) break; - attrs[attrName] = void 0; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; } if (optional && !attrs[attrName]) break; @@ -9149,13 +11442,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - compare = function(a, b) { return a === b || (a !== a && b !== b); }; + compare = simpleCompare; } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest lastValue = destination[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', - "Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!", + 'Expression \'{0}\' in attribute \'{1}\' used with directive \'{2}\' is non-assignable!', attrs[attrName], attrName, directive.name); }; lastValue = destination[scopeName] = parentGet(scope); @@ -9170,10 +11463,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { parentSet(scope, parentValue = destination[scopeName]); } } - return lastValue = parentValue; + lastValue = parentValue; + return lastValue; }; parentValueWatch.$stateful = true; - var removeWatch; if (definition.collection) { removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); } else { @@ -9182,7 +11475,38 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { removeWatchCollection.push(removeWatch); break; + case '<': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + var isLiteral = parentGet.literal; + + var initialValue = destination[scopeName] = parentGet(scope); + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + + removeWatch = scope[definition.collection ? '$watchCollection' : '$watch'](parentGet, function parentValueWatchAction(newValue, oldValue) { + if (oldValue === newValue) { + if (oldValue === initialValue || (isLiteral && equals(oldValue, initialValue))) { + return; + } + oldValue = initialValue; + } + recordChanges(scopeName, newValue, oldValue); + destination[scopeName] = newValue; + }); + + removeWatchCollection.push(removeWatch); + break; + case '&': + if (!optional && !hasOwnProperty.call(attrs, attrName)) { + strictBindingsCheck(attrName, directive.name); + } // Don't assign Object.prototype method to scope parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; @@ -9196,22 +11520,65 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); - return removeWatchCollection.length && function removeWatches() { - for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { - removeWatchCollection[i](); + function recordChanges(key, currentValue, previousValue) { + if (isFunction(destination.$onChanges) && !simpleCompare(currentValue, previousValue)) { + // If we have not already scheduled the top level onChangesQueue handler then do so now + if (!onChangesQueue) { + scope.$$postDigest(flushOnChangesQueue); + onChangesQueue = []; + } + // If we have not already queued a trigger of onChanges for this controller then do so now + if (!changes) { + changes = {}; + onChangesQueue.push(triggerOnChangesHook); + } + // If the has been a change on this property already then we need to reuse the previous value + if (changes[key]) { + previousValue = changes[key].previousValue; + } + // Store this change + changes[key] = new SimpleChange(previousValue, currentValue); + } + } + + function triggerOnChangesHook() { + destination.$onChanges(changes); + // Now clear the changes so that we schedule onChanges when more changes arrive + changes = undefined; + } + + return { + initialChanges: initialChanges, + removeWatches: removeWatchCollection.length && function removeWatches() { + for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { + removeWatchCollection[i](); + } } }; } }]; } -var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; +function SimpleChange(previous, current) { + this.previousValue = previous; + this.currentValue = current; +} +SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; }; + + +var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; +var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, function(_, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); } /** @@ -9221,7 +11588,7 @@ function directiveNormalize(name) { * @description * A shared object between directive compile / linking functions which contains normalized DOM * element attributes. The values reflect current binding state `{{ }}`. The normalization is - * needed since all of these are treated as equivalent in Angular: + * needed since all of these are treated as equivalent in AngularJS: * * ``` * @@ -9283,7 +11650,7 @@ function tokenDifference(str1, str2) { 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; + if (token === tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } @@ -9300,8 +11667,9 @@ function removeComments(jqNodes) { while (i--) { var node = jqNodes[i]; - if (node.nodeType === NODE_TYPE_COMMENT) { - splice.call(jqNodes, i, 1); + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); } } return jqNodes; @@ -9323,16 +11691,26 @@ function identifierForController(controller, ident) { /** * @ngdoc provider * @name $controllerProvider + * @this + * * @description - * The {@link ng.$controller $controller service} is used by Angular to create new + * The {@link ng.$controller $controller service} is used by AngularJS to create new * controllers. * * This provider allows controller registration via the * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { - var controllers = {}, - globals = false; + var controllers = {}; + + /** + * @ngdoc method + * @name $controllerProvider#has + * @param {string} name Controller name to check. + */ + this.has = function(name) { + return controllers.hasOwnProperty(name); + }; /** * @ngdoc method @@ -9351,17 +11729,7 @@ function $ControllerProvider() { } }; - /** - * @ngdoc method - * @name $controllerProvider#allowGlobals - * @description If called, allows `$controller` to find controller constructors on `window` - */ - this.allowGlobals = function() { - globals = true; - }; - - - this.$get = ['$injector', '$window', function($injector, $window) { + this.$get = ['$injector', function($injector) { /** * @ngdoc service @@ -9374,8 +11742,6 @@ function $ControllerProvider() { * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * 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 @@ -9390,7 +11756,7 @@ function $ControllerProvider() { * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(expression, locals, later, ident) { + return function $controller(expression, locals, later, ident) { // PRIVATE API: // param `later` --- indicates that the controller's constructor is invoked at a later time. // If true, $controller will allocate the object with the correct @@ -9408,15 +11774,19 @@ function $ControllerProvider() { match = expression.match(CNTRL_REG); if (!match) { throw $controllerMinErr('ctrlfmt', - "Badly formed controller string '{0}'. " + - "Must match `__name__ as __id__` or `__name__`.", expression); + 'Badly formed controller string \'{0}\'. ' + + 'Must match `__name__ as __id__` or `__name__`.', expression); } - constructor = match[1], + constructor = match[1]; identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || - (globals ? getter($window, constructor, true) : undefined); + : getter(locals.$scope, constructor, true); + + if (!expression) { + throw $controllerMinErr('ctrlreg', + 'The controller with the name \'{0}\' is not registered.', constructor); + } assertArgFn(expression, constructor, true); } @@ -9440,8 +11810,7 @@ function $ControllerProvider() { addIdentifier(locals, identifier, instance, constructor || expression.name); } - var instantiate; - return instantiate = extend(function() { + return extend(function $controllerInit() { var result = $injector.invoke(expression, instance, locals, constructor); if (result !== instance && (isObject(result) || isFunction(result))) { instance = result; @@ -9469,7 +11838,7 @@ function $ControllerProvider() { function addIdentifier(locals, identifier, instance, name) { if (!(locals && isObject(locals.$scope))) { throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", + 'Cannot export controller \'{0}\' as \'{1}\'! No $scope object provided via `locals`.', name, identifier); } @@ -9482,12 +11851,13 @@ function $ControllerProvider() { * @ngdoc service * @name $document * @requires $window + * @this * * @description * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - +

$document title:

@@ -9509,13 +11879,41 @@ function $DocumentProvider() { }]; } + +/** + * @private + * @this + * Listens for document visibility change and makes the current status accessible. + */ +function $$IsDocumentHiddenProvider() { + this.$get = ['$document', '$rootScope', function($document, $rootScope) { + var doc = $document[0]; + var hidden = doc && doc.hidden; + + $document.on('visibilitychange', changeListener); + + $rootScope.$on('$destroy', function() { + $document.off('visibilitychange', changeListener); + }); + + function changeListener() { + hidden = doc.hidden; + } + + return function() { + return hidden; + }; + }]; +} + /** * @ngdoc service * @name $exceptionHandler * @requires ng.$log + * @this * * @description - * Any uncaught exception in angular expressions is delegated to this service. + * Any uncaught exception in AngularJS expressions is delegated to this service. * The default implementation simply delegates to `$log.error` which logs it into * the browser console. * @@ -9524,18 +11922,21 @@ function $DocumentProvider() { * * ## Example: * + * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught + * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead + * of `$log.error()`. + * * ```js - * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() { - * return function(exception, cause) { - * exception.message += ' (caused by "' + cause + '")'; - * throw exception; - * }; - * }); + * angular. + * module('exceptionOverwrite', []). + * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) { + * return function myExceptionHandler(exception, cause) { + * logErrorsToBackend(exception, cause); + * $log.warn(exception, cause); + * }; + * }]); * ``` * - * 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} @@ -9545,7 +11946,7 @@ function $DocumentProvider() { * `try { ... } catch(e) { $exceptionHandler(e); }` * * @param {Error} exception Exception associated with the error. - * @param {string=} cause optional information about the context in which + * @param {string=} cause Optional information about the context in which * the error was thrown. * */ @@ -9557,7 +11958,7 @@ function $ExceptionHandlerProvider() { }]; } -var $$ForceReflowProvider = function() { +var $$ForceReflowProvider = /** @this */ function() { this.$get = ['$document', function($document) { return function(domNode) { //the line below will force the browser to perform a repaint so @@ -9587,13 +11988,8 @@ var JSON_ENDS = { '[': /]$/, '{': /}$/ }; -var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; +var JSON_PROTECTION_PREFIX = /^\)]\}',?\n/; var $httpMinErr = minErr('$http'); -var $httpMinErrLegacyFn = function(method) { - return function() { - throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method); - }; -}; function serializeValue(v) { if (isObject(v)) { @@ -9603,6 +11999,7 @@ function serializeValue(v) { } +/** @this */ function $HttpParamSerializerProvider() { /** * @ngdoc service @@ -9615,19 +12012,19 @@ function $HttpParamSerializerProvider() { * * `{'foo': 'bar'}` results in `foo=bar` * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) - * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object) + * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object) * * Note that serializer will sort the request parameters alphabetically. - * */ + */ this.$get = function() { return function ngParamSerializer(params) { if (!params) return ''; var parts = []; forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; + if (value === null || isUndefined(value) || isFunction(value)) return; if (isArray(value)) { - forEach(value, function(v, k) { + forEach(value, function(v) { parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); }); } else { @@ -9640,10 +12037,12 @@ function $HttpParamSerializerProvider() { }; } +/** @this */ function $HttpParamSerializerJQLikeProvider() { /** * @ngdoc service * @name $httpParamSerializerJQLike + * * @description * * Alternative {@link $http `$http`} params serializer that follows @@ -9683,7 +12082,7 @@ function $HttpParamSerializerJQLikeProvider() { * }); * ``` * - * */ + */ this.$get = function() { return function jQueryLikeParamSerializer(params) { if (!params) return ''; @@ -9692,7 +12091,6 @@ function $HttpParamSerializerJQLikeProvider() { return parts.join('&'); function serialize(toSerialize, prefix, topLevel) { - if (toSerialize === null || isUndefined(toSerialize)) return; if (isArray(toSerialize)) { forEach(toSerialize, function(value, index) { serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']'); @@ -9705,7 +12103,11 @@ function $HttpParamSerializerJQLikeProvider() { (topLevel ? '' : ']')); }); } else { - parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); + if (isFunction(toSerialize)) { + toSerialize = toSerialize(); + } + parts.push(encodeUriQuery(prefix) + '=' + + (toSerialize == null ? '' : encodeUriQuery(serializeValue(toSerialize)))); } } }; @@ -9719,8 +12121,18 @@ function defaultHttpResponseTransform(data, headers) { if (tempData) { var contentType = headers('Content-Type'); - if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { - data = fromJson(tempData); + var hasJsonContentType = contentType && (contentType.indexOf(APPLICATION_JSON) === 0); + + if (hasJsonContentType || isJsonLike(tempData)) { + try { + data = fromJson(tempData); + } catch (e) { + if (!hasJsonContentType) { + return data; + } + throw $httpMinErr('baddata', 'Data must be a valid JSON object. Received: "{0}". ' + + 'Parse error: "{1}"', data, e); + } } } } @@ -9772,7 +12184,7 @@ function parseHeaders(headers) { * @param {(string|Object)} headers Headers to provide access to. * @returns {function(string=)} Returns a getter function which if called with: * - * - if called with single an argument returns a single header value or null + * - if called with an argument returns a single header value or null * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { @@ -9783,7 +12195,7 @@ function headersGetter(headers) { if (name) { var value = headersObj[lowercase(name)]; - if (value === void 0) { + if (value === undefined) { value = null; } return value; @@ -9826,9 +12238,11 @@ function isSuccess(status) { /** * @ngdoc provider * @name $httpProvider + * @this + * * @description * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. - * */ + */ function $HttpProvider() { /** * @ngdoc property @@ -9841,12 +12255,6 @@ function $HttpProvider() { * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses * by default. See {@link $http#caching $http Caching} for more information. * - * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. - * Defaults value is `'XSRF-TOKEN'`. - * - * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the - * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. - * * - **`defaults.headers`** - {Object} - Default headers for all $http requests. * Refer to {@link ng.$http#setting-http-headers $http} for documentation on * setting default headers. @@ -9855,13 +12263,40 @@ function $HttpProvider() { * - **`defaults.headers.put`** * - **`defaults.headers.patch`** * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. * * - **`defaults.paramSerializer`** - `{string|function(Object):string}` - A function * used to the prepare string representation of request parameters (specified as an object). * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * - **/ + * - **`defaults.transformRequest`** - + * `{Array|function(data, headersGetter)}` - + * An array of functions (or a single function) which are applied to the request data. + * By default, this is an array with one request transformation function: + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * - **`defaults.transformResponse`** - + * `{Array|function(data, headersGetter, status)}` - + * An array of functions (or a single function) which are applied to the response data. By default, + * this is an array which applies one response transformation function that does two things: + * + * - If XSRF prefix is detected, strip it + * (see {@link ng.$http#security-considerations Security Considerations in the $http docs}). + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. + * + * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. + * + */ var defaults = this.defaults = { // transform incoming response data transformResponse: [defaultHttpResponseTransform], @@ -9884,7 +12319,9 @@ function $HttpProvider() { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', - paramSerializer: '$httpParamSerializer' + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' }; var useApplyAsync = false; @@ -9906,7 +12343,7 @@ function $HttpProvider() { * * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. * otherwise, returns the current configured value. - **/ + */ this.useApplyAsync = function(value) { if (isDefined(value)) { useApplyAsync = !!value; @@ -9915,30 +12352,6 @@ function $HttpProvider() { return useApplyAsync; }; - var useLegacyPromise = true; - /** - * @ngdoc method - * @name $httpProvider#useLegacyPromiseExtensions - * @description - * - * Configure `$http` service to return promises without the shorthand methods `success` and `error`. - * This should be used to make sure that applications work without these methods. - * - * Defaults to true. If no value is specified, returns the current configured value. - * - * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods. - * - * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. - * otherwise, returns the current configured value. - **/ - this.useLegacyPromiseExtensions = function(value) { - if (isDefined(value)) { - useLegacyPromise = !!value; - return this; - } - return useLegacyPromise; - }; - /** * @ngdoc property * @name $httpProvider#interceptors @@ -9951,11 +12364,73 @@ function $HttpProvider() { * array, on request, but reverse order, on response. * * {@link ng.$http#interceptors Interceptors detailed info} - **/ + */ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { + /** + * @ngdoc property + * @name $httpProvider#xsrfTrustedOrigins + * @description + * + * Array containing URLs whose origins are trusted to receive the XSRF token. See the + * {@link ng.$http#security-considerations Security Considerations} sections for more details on + * XSRF. + * + * **Note:** An "origin" consists of the [URI scheme](https://en.wikipedia.org/wiki/URI_scheme), + * the [hostname](https://en.wikipedia.org/wiki/Hostname) and the + * [port number](https://en.wikipedia.org/wiki/Port_(computer_networking). For `http:` and + * `https:`, the port number can be omitted if using th default ports (80 and 443 respectively). + * Examples: `http://example.com`, `https://api.example.com:9876` + * + *
+ * It is not possible to trust specific URLs/paths. The `path`, `query` and `fragment` parts + * of a URL will be ignored. For example, `https://foo.com/path/bar?query=baz#fragment` will be + * treated as `https://foo.com`, meaning that **all** requests to URLs starting with + * `https://foo.com/` will include the XSRF token. + *
+ * + * @example + * + * ```js + * // App served from `https://example.com/`. + * angular. + * module('xsrfTrustedOriginsExample', []). + * config(['$httpProvider', function($httpProvider) { + * $httpProvider.xsrfTrustedOrigins.push('https://api.example.com'); + * }]). + * run(['$http', function($http) { + * // The XSRF token will be sent. + * $http.get('https://api.example.com/preferences').then(...); + * + * // The XSRF token will NOT be sent. + * $http.get('https://stats.example.com/activity').then(...); + * }]); + * ``` + */ + var xsrfTrustedOrigins = this.xsrfTrustedOrigins = []; + + /** + * @ngdoc property + * @name $httpProvider#xsrfWhitelistedOrigins + * @description + * + * @deprecated + * sinceVersion="1.8.1" + * + * This property is deprecated. Use {@link $httpProvider#xsrfTrustedOrigins xsrfTrustedOrigins} + * instead. + */ + Object.defineProperty(this, 'xsrfWhitelistedOrigins', { + get: function() { + return this.xsrfTrustedOrigins; + }, + set: function(origins) { + this.xsrfTrustedOrigins = origins; + } + }); + + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { var defaultCache = $cacheFactory('$http'); @@ -9977,6 +12452,11 @@ function $HttpProvider() { ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); + /** + * A function to check request URLs against a list of allowed origins. + */ + var urlIsAllowedOrigin = urlIsAllowedOriginFactory(xsrfTrustedOrigins); + /** * @ngdoc service * @kind function @@ -9988,7 +12468,7 @@ function $HttpProvider() { * @requires $injector * * @description - * The `$http` service is a core Angular service that facilitates communication with the remote + * The `$http` service is a core AngularJS service that facilitates communication with the remote * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). * @@ -10005,7 +12485,9 @@ function $HttpProvider() { * * ## General usage * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} — - * that is used to generate an HTTP request and returns a {@link ng.$q promise}. + * that is used to generate an HTTP request and returns a {@link ng.$q promise} that is + * resolved (request success) or rejected (request failure) with a + * {@link ng.$http#$http-returns response} object. * * ```js * // Simple GET request example: @@ -10021,20 +12503,6 @@ function $HttpProvider() { * }); * ``` * - * The response object has these properties: - * - * - **data** – `{string|Object}` – The response body transformed with the transform - * functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. - * - **statusText** – `{string}` – HTTP status text of the response. - * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. - * * * ## Shortcut methods * @@ -10069,14 +12537,6 @@ function $HttpProvider() { * $httpBackend.flush(); * ``` * - * ## Deprecation Notice - *
- * The `$http` legacy promise methods `success` and `error` have been deprecated. - * Use the standard `then` method instead. - * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to - * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error. - *
- * * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults @@ -10084,7 +12544,7 @@ function $HttpProvider() { * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` + * - Accept: application/json, text/plain, \*/\* * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -10100,7 +12560,7 @@ function $HttpProvider() { * * ``` * module.run(function($http) { - * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' + * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'; * }); * ``` * @@ -10131,7 +12591,7 @@ function $HttpProvider() { * which allows you to `push` or `unshift` a new transformation function into the transformation chain. * *
- * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. + * **Note:** AngularJS does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference). * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest * function will be reflected on the scope and in any templates where the object is data-bound. @@ -10148,22 +12608,25 @@ function $HttpProvider() { * You can augment or replace the default transformations by modifying these properties by adding to or * replacing the array. * - * Angular provides the following default transformations: + * AngularJS provides the following default transformations: * - * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`) is + * an array with one function that does the following: * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`) is + * an array with one function that does the following: * * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. * * * ### Overriding the Default Transformations Per Request * - * If you wish override the request/response transformations only for a single request then provide + * If you wish to override the request/response transformations only for a single request then provide * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * @@ -10206,7 +12669,7 @@ function $HttpProvider() { * * cache a specific response - set config.cache value to TRUE or to a cache object * * If caching is enabled, but neither the default cache nor config.cache are set to a cache object, - * then the default `$cacheFactory($http)` object is used. + * then the default `$cacheFactory("$http")` object is used. * * The default cache value can be set by updating the * {@link ng.$http#defaults `$http.defaults.cache`} property or the @@ -10318,7 +12781,7 @@ function $HttpProvider() { * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) * - * Both server and the client must cooperate in order to eliminate these threats. Angular comes + * Both server and the client must cooperate in order to eliminate these threats. AngularJS comes * pre-configured with strategies that address these issues, but for this to work backend server * cooperation is required. * @@ -10328,7 +12791,7 @@ function $HttpProvider() { * allows third party website to turn your JSON resource URL into * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To * counter this your server can prefix all JSON requests with following string `")]}',\n"`. - * Angular will automatically strip the prefix before processing it as JSON. + * AngularJS will automatically strip the prefix before processing it as JSON. * * For example if your server needs to return: * ```js @@ -10341,46 +12804,70 @@ function $HttpProvider() { * ['one','two'] * ``` * - * Angular will strip the prefix, before processing the JSON. + * AngularJS will strip the prefix, before processing the JSON. * * * ### Cross Site Request Forgery (XSRF) Protection * * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by * which the attacker can trick an authenticated user into unknowingly executing actions on your - * website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the + * website. AngularJS provides a mechanism to counter XSRF. When performing XHR requests, the * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP - * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the - * cookie, your server can be assured that the XHR came from JavaScript running on your domain. - * The header will not be set for cross-domain requests. + * header (by default `X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read + * the cookie, your server can be assured that the XHR came from JavaScript running on your + * domain. * * To take advantage of this, your server needs to set a token in a JavaScript readable session * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the - * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure - * that only JavaScript running on your domain could have sent the request. The token must be - * unique for each user and must be verifiable by the server (to prevent the JavaScript from + * server can verify that the cookie matches the `X-XSRF-TOKEN` HTTP header, and therefore be + * sure that only JavaScript running on your domain could have sent the request. The token must + * be unique for each user and must be verifiable by the server (to prevent the JavaScript from * making up its own tokens). We recommend that the token is a digest of your site's * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) * for added security. * - * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName - * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, - * or the per-request config object. + * The header will — by default — **not** be set for cross-domain requests. This + * prevents unauthorized servers (e.g. malicious or compromised 3rd-party APIs) from gaining + * access to your users' XSRF tokens and exposing them to Cross Site Request Forgery. If you + * want to, you can trust additional origins to also receive the XSRF token, by adding them + * to {@link ng.$httpProvider#xsrfTrustedOrigins xsrfTrustedOrigins}. This might be + * useful, for example, if your application, served from `example.com`, needs to access your API + * at `api.example.com`. + * See {@link ng.$httpProvider#xsrfTrustedOrigins $httpProvider.xsrfTrustedOrigins} for + * more details. + * + *
+ * **Warning**
+ * Only trusted origins that you have control over and make sure you understand the + * implications of doing so. + *
+ * + * The name of the cookie and the header can be specified using the `xsrfCookieName` and + * `xsrfHeaderName` properties of either `$httpProvider.defaults` at config-time, + * `$http.defaults` at run-time, or the per-request config object. + * + * In order to prevent collisions in environments where multiple AngularJS apps share the + * same domain or subdomain, we recommend that each application uses a unique cookie name. * - * In order to prevent collisions in environments where multiple Angular apps share the - * same domain or subdomain, we recommend that each application uses unique cookie name. * * @param {object} config Object describing the request to be made and how it should be * processed. The object has following properties: * * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * - **params** – `{Object.}` – Map of strings or objects which will be serialized * with the `paramSerializer` and appended as GET parameters. * - **data** – `{string|Object}` – Data to be sent as the request message data. * - **headers** – `{Object}` – Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function is null, the * header will not be sent. Functions accept a config object as an argument. + * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object. + * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`. + * The handler will be called in the context of a `$apply` block. + * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload + * object. To bind events to the XMLHttpRequest object, use `eventHandlers`. + * The handler will be called in the context of a `$apply` block. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – @@ -10407,14 +12894,44 @@ function $HttpProvider() { * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. + * + * A numerical timeout or a promise returned from {@link ng.$timeout $timeout}, will set + * the `xhrStatus` in the {@link $http#$http-returns response} to "timeout", and any other + * resolved promise will set it to "abort", following standard XMLHttpRequest behavior. + * * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) * for more information. * - **responseType** - `{string}` - see * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype). * - * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object - * when the request succeeds or fails. + * @returns {HttpPromise} A {@link ng.$q `Promise}` that will be resolved (request success) + * or rejected (request failure) with a response object. + * + * The response object has these properties: + * + * - **data** – `{string|Object}` – The response body transformed with + * the transform functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used + * to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. + * - **xhrStatus** – `{string}` – Status of the XMLHttpRequest + * (`complete`, `error`, `timeout` or `abort`). + * + * + * A response status code between 200 and 299 is considered a success status + * and will result in the success callback being called. Any response status + * code outside of that range is considered an error status and will result + * in the error callback being called. + * Also, status codes less than -1 are normalized to zero. -1 usually means + * the request was aborted, e.g. using a `config.timeout`. More information + * about the status might be available in the `xhrStatus` property. + * + * Note that if the response is a redirect, XMLHttpRequest will transparently + * follow it, meaning that the outcome (success or error) will be determined + * by the final response status code. * * * @property {Array.} pendingRequests Array of config objects for currently pending @@ -10422,7 +12939,7 @@ function $HttpProvider() { * * * @example - +

+ * Current time is: + *
+ * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
+ * + * + *
+ *
+ */ + var interval = $$intervalFactory(setIntervalFn, clearIntervalFn); + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {Promise=} promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ + interval.cancel = function(promise) { + if (!promise) return false; - /** - * @ngdoc service - * @name $interval - * - * @description - * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` - * milliseconds. - * - * The return value of registering an interval function is a promise. This promise will be - * notified upon each tick of the interval, and will be resolved after `count` iterations, or - * run indefinitely if `count` is not defined. The value of the notification will be the - * number of iterations that have run. - * To cancel an interval, call `$interval.cancel(promise)`. - * - * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to - * move forward by `millis` milliseconds and trigger any functions scheduled to run in that - * time. - * - *
- * **Note**: Intervals created by this service must be explicitly destroyed when you are finished - * with them. In particular they are not automatically destroyed when a controller's scope or a - * directive's element are destroyed. - * You should take this into consideration and make sure to always cancel the interval at the - * appropriate moment. See the example below for more details on how and when to do this. - *
- * - * @param {function()} fn A function that should be called repeatedly. - * @param {number} delay Number of milliseconds between each function call. - * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat - * indefinitely. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {promise} A promise which will be notified on each iteration. - * - * @example - * - * - * - * - *
- *
- *
- * Current time is: - *
- * Blood 1 : {{blood_1}} - * Blood 2 : {{blood_2}} - * - * - * - *
- *
- * - *
- *
- */ - function interval(fn, delay, count, invokeApply) { - var hasParams = arguments.length > 4, - args = hasParams ? sliceArgs(arguments, 4) : [], - setInterval = $window.setInterval, - clearInterval = $window.clearInterval, - iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply), - deferred = (skipApply ? $$q : $q).defer(), - promise = deferred.promise; + if (!promise.hasOwnProperty('$$intervalId')) { + throw $intervalMinErr('badprom', + '`$interval.cancel()` called with a promise that was not generated by `$interval()`.'); + } - count = isDefined(count) ? count : 0; + if (!intervals.hasOwnProperty(promise.$$intervalId)) return false; - promise.then(null, null, (!hasParams) ? fn : function() { - fn.apply(null, args); - }); + var id = promise.$$intervalId; + var deferred = intervals[id]; - promise.$$intervalId = setInterval(function tick() { - deferred.notify(iteration++); + // Interval cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + clearIntervalFn(id); - if (count > 0 && iteration >= count) { - deferred.resolve(iteration); - clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - } + return true; + }; - if (!skipApply) $rootScope.$apply(); + return interval; + }]; +} + +/** @this */ +function $$IntervalFactoryProvider() { + this.$get = ['$browser', '$q', '$$q', '$rootScope', + function($browser, $q, $$q, $rootScope) { + return function intervalFactory(setIntervalFn, clearIntervalFn) { + return function intervalFn(fn, delay, count, invokeApply) { + var hasParams = arguments.length > 4, + args = hasParams ? sliceArgs(arguments, 4) : [], + iteration = 0, + skipApply = isDefined(invokeApply) && !invokeApply, + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; + + count = isDefined(count) ? count : 0; + + function callback() { + if (!hasParams) { + fn(iteration); + } else { + fn.apply(null, args); + } + } - }, delay); + function tick() { + if (skipApply) { + $browser.defer(callback); + } else { + $rootScope.$evalAsync(callback); + } + deferred.notify(iteration++); - intervals[promise.$$intervalId] = deferred; + if (count > 0 && iteration >= count) { + deferred.resolve(iteration); + clearIntervalFn(promise.$$intervalId); + } - return promise; - } + if (!skipApply) $rootScope.$apply(); + } + promise.$$intervalId = setIntervalFn(tick, delay, deferred, skipApply); - /** - * @ngdoc method - * @name $interval#cancel - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {Promise=} promise returned by the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully canceled. - */ - interval.cancel = function(promise) { - if (promise && promise.$$intervalId in intervals) { - intervals[promise.$$intervalId].reject('canceled'); - $window.clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - return true; - } - return false; + return promise; + }; }; - - return interval; }]; } +/** + * @ngdoc service + * @name $jsonpCallbacks + * @requires $window + * @description + * This service handles the lifecycle of callbacks to handle JSONP requests. + * Override this service if you wish to customise where the callbacks are stored and + * how they vary compared to the requested url. + */ +var $jsonpCallbacksProvider = /** @this */ function() { + this.$get = function() { + var callbacks = angular.callbacks; + var callbackMap = {}; + + function createCallback(callbackId) { + var callback = function(data) { + callback.data = data; + callback.called = true; + }; + callback.id = callbackId; + return callback; + } + + return { + /** + * @ngdoc method + * @name $jsonpCallbacks#createCallback + * @param {string} url the url of the JSONP request + * @returns {string} the callback path to send to the server as part of the JSONP request + * @description + * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback + * to pass to the server, which will be used to call the callback with its payload in the JSONP response. + */ + createCallback: function(url) { + var callbackId = '_' + (callbacks.$$counter++).toString(36); + var callbackPath = 'angular.callbacks.' + callbackId; + var callback = createCallback(callbackId); + callbackMap[callbackPath] = callbacks[callbackId] = callback; + return callbackPath; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#wasCalled + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {boolean} whether the callback has been called, as a result of the JSONP response + * @description + * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the + * callback that was passed in the request. + */ + wasCalled: function(callbackPath) { + return callbackMap[callbackPath].called; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#getResponse + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {*} the data received from the response via the registered callback + * @description + * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback + * in the JSONP response. + */ + getResponse: function(callbackPath) { + return callbackMap[callbackPath].data; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#removeCallback + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @description + * {@link $httpBackend} calls this method to remove the callback after the JSONP request has + * completed or timed-out. + */ + removeCallback: function(callbackPath) { + var callback = callbackMap[callbackPath]; + delete callbacks[callback.id]; + delete callbackMap[callbackPath]; + } + }; + }; +}; + /** * @ngdoc service * @name $locale * * @description - * $locale service provides localization rules for various Angular components. As of right now the + * $locale service provides localization rules for various AngularJS components. As of right now the * only public api is: * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +/* global stripHash: true */ + +var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -11681,10 +14515,34 @@ function encodePath(path) { i = segments.length; while (i--) { - segments[i] = encodeUriSegment(segments[i]); + // decode forward slashes to prevent them from being double encoded + segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/')); + } + + return segments.join('/'); +} + +function decodePath(path, html5Mode) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = decodeURIComponent(segments[i]); + if (html5Mode) { + // encode forward slashes to prevent them from being mistaken for path separators + segments[i] = segments[i].replace(/\//g, '%2F'); + } } - return segments.join('/'); + return segments.join('/'); +} + +function normalizePath(pathValue, searchValue, hashValue) { + var search = toKeyValue(searchValue), + hash = hashValue ? '#' + encodeUriSegment(hashValue) : '', + path = encodePath(pathValue); + + return path + (search ? '?' + search : '') + hash; } function parseAbsoluteUrl(absoluteUrl, locationObj) { @@ -11695,49 +14553,51 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) { locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } +var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; +function parseAppUrl(url, locationObj, html5Mode) { + + if (DOUBLE_SLASH_REGEX.test(url)) { + throw $locationMinErr('badpath', 'Invalid url "{0}".', url); + } -function parseAppUrl(relativeUrl, locationObj) { - var prefixed = (relativeUrl.charAt(0) !== '/'); + var prefixed = (url.charAt(0) !== '/'); if (prefixed) { - relativeUrl = '/' + relativeUrl; + url = '/' + url; } - var match = urlResolve(relativeUrl); - locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? - match.pathname.substring(1) : match.pathname); + var match = urlResolve(url); + var path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname; + locationObj.$$path = decodePath(path, html5Mode); locationObj.$$search = parseKeyValue(match.search); locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; - if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') { locationObj.$$path = '/' + locationObj.$$path; } } +function startsWith(str, search) { + return str.slice(0, search.length) === search; +} /** * - * @param {string} begin - * @param {string} whole - * @returns {string} returns text from whole after begin or undefined if it does not begin with - * expected string. + * @param {string} base + * @param {string} url + * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with + * the expected string. */ -function beginsWith(begin, whole) { - if (whole.indexOf(begin) === 0) { - return whole.substr(begin.length); +function stripBaseUrl(base, url) { + if (startsWith(url, base)) { + return url.substr(base.length); } } - function stripHash(url) { var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); -} - -function trimEmptyHash(url) { - return url.replace(/(#.+)|#$/, '$1'); + return index === -1 ? url : url.substr(0, index); } - function stripFile(url) { return url.substr(0, stripHash(url).lastIndexOf('/') + 1); } @@ -11749,13 +14609,13 @@ function serverBase(url) { /** - * LocationHtml5Url represents an url + * LocationHtml5Url represents a URL * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} basePrefix url path prefix + * @param {string} basePrefix URL path prefix */ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; @@ -11764,18 +14624,18 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * Parse given html5 (regular) url string into properties - * @param {string} url HTML5 url + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL * @private */ this.$$parse = function(url) { - var pathUrl = beginsWith(appBaseNoFile, url); + var pathUrl = stripBaseUrl(appBaseNoFile, url); if (!isString(pathUrl)) { throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); } - parseAppUrl(pathUrl, this); + parseAppUrl(pathUrl, this, true); if (!this.$$path) { this.$$path = '/'; @@ -11784,16 +14644,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$compose(); }; - /** - * Compose url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + this.$$normalizeUrl = function(url) { + return appBaseNoFile + url.substr(1); // first char is always '/' }; this.$$parseLinkUrl = function(url, relHref) { @@ -11806,16 +14658,17 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { var appUrl, prevAppUrl; var rewrittenUrl; - if (isDefined(appUrl = beginsWith(appBase, url))) { + + if (isDefined(appUrl = stripBaseUrl(appBase, url))) { prevAppUrl = appUrl; - if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) { - rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) { + rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl); } else { rewrittenUrl = appBase + prevAppUrl; } - } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) { + } else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) { rewrittenUrl = appBaseNoFile + appUrl; - } else if (appBaseNoFile == url + '/') { + } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; } if (rewrittenUrl) { @@ -11827,7 +14680,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * @@ -11842,19 +14695,19 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL * @private */ this.$$parse = function(url) { - var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url); var withoutHashUrl; if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { - // The rest of the url starts with a hash so we have + // The rest of the URL starts with a hash so we have // got either a hashbang path or a plain hash fragment - withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl); + withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl); if (isUndefined(withoutHashUrl)) { // There was no hashbang prefix so we just have a hash fragment withoutHashUrl = withoutBaseUrl; @@ -11870,12 +14723,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { withoutHashUrl = ''; if (isUndefined(withoutBaseUrl)) { appBase = url; - this.replace(); + /** @type {?} */ (this).replace(); } } } - parseAppUrl(withoutHashUrl, this); + parseAppUrl(withoutHashUrl, this, false); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); @@ -11889,7 +14742,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { * * a.setAttribute('href', '/foo') * * a.pathname === '/C:/foo' //true * - * Inside of Angular, we're always using pathnames that + * Inside of AngularJS, we're always using pathnames that * do not include drive names for routing. */ function removeWindowsDriveName(path, url, base) { @@ -11902,7 +14755,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { var firstPathSegmentMatch; //Get the relative path from the input URL. - if (url.indexOf(base) === 0) { + if (startsWith(url, base)) { url = url.replace(base, ''); } @@ -11916,20 +14769,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { } }; - /** - * Compose hashbang url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + this.$$normalizeUrl = function(url) { + return appBase + (url ? hashPrefix + url : ''); }; this.$$parseLinkUrl = function(url, relHref) { - if (stripHash(appBase) == stripHash(url)) { + if (stripHash(appBase) === stripHash(url)) { this.$$parse(url); return true; } @@ -11939,7 +14784,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * @@ -11963,9 +14808,9 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var rewrittenUrl; var appUrl; - if (appBase == stripHash(url)) { + if (appBase === stripHash(url)) { rewrittenUrl = url; - } else if ((appUrl = beginsWith(appBaseNoFile, url))) { + } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) { rewrittenUrl = appBase + hashPrefix + appUrl; } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; @@ -11976,20 +14821,21 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { return !!rewrittenUrl; }; - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$normalizeUrl = function(url) { // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' - this.$$absUrl = appBase + hashPrefix + this.$$url; + return appBase + hashPrefix + url; }; - } var locationPrototype = { + /** + * Ensure absolute URL is initialized. + * @private + */ + $$absUrl:'', + /** * Are we in html5 mode? * @private @@ -12002,6 +14848,16 @@ var locationPrototype = { */ $$replace: false, + /** + * Compose url and update `url` and `absUrl` property + * @private + */ + $$compose: function() { + this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash); + this.$$absUrl = this.$$normalizeUrl(this.$$url); + this.$$urlUpdatedByLocation = true; + }, + /** * @ngdoc method * @name $location#absUrl @@ -12009,17 +14865,17 @@ var locationPrototype = { * @description * This method is getter only. * - * Return full url representation with all segments encoded according to rules specified in + * 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 + * // 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 + * @return {string} full URL */ absUrl: locationGetter('$$absUrl'), @@ -12030,18 +14886,18 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * Return URL (e.g. `/path?a=b#hash`) when called without any parameter. * * Change path, search and hash, when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // 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`) + * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`) * @return {string} url */ url: function(url) { @@ -12064,16 +14920,16 @@ var locationPrototype = { * @description * This method is getter only. * - * Return protocol of current url. + * Return protocol of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var protocol = $location.protocol(); * // => "http" * ``` * - * @return {string} protocol of current url + * @return {string} protocol of current URL */ protocol: locationGetter('$$protocol'), @@ -12084,24 +14940,24 @@ var locationPrototype = { * @description * This method is getter only. * - * Return host of current url. + * Return host of current URL. * - * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. + * Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var host = $location.host(); * // => "example.com" * - * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo + * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo * host = $location.host(); * // => "example.com" * host = location.host; * // => "example.com:8080" * ``` * - * @return {string} host of current url. + * @return {string} host of current URL. */ host: locationGetter('$$host'), @@ -12112,11 +14968,11 @@ var locationPrototype = { * @description * This method is getter only. * - * Return port of current url. + * Return port of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var port = $location.port(); * // => 80 * ``` @@ -12132,7 +14988,7 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return path of current url when called without any parameter. + * Return path of current URL when called without any parameter. * * Change path when called with parameter and return `$location`. * @@ -12141,7 +14997,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var path = $location.path(); * // => "/some/path" * ``` @@ -12151,7 +15007,7 @@ var locationPrototype = { */ path: locationGetterSetter('$$path', function(path) { path = path !== null ? path.toString() : ''; - return path.charAt(0) == '/' ? path : '/' + path; + return path.charAt(0) === '/' ? path : '/' + path; }), /** @@ -12161,13 +15017,13 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return search part (as object) of current url when called without any parameter. + * Return search part (as object) of current URL when called without any parameter. * * Change search part when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var searchObject = $location.search(); * // => {foo: 'bar', baz: 'xoxo'} * @@ -12183,7 +15039,7 @@ var locationPrototype = { * of `$location` to the specified value. * * If the argument is a hash object containing an array of values, these values will be encoded - * as duplicate search parameters in the url. + * as duplicate search parameters in the URL. * * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` * will override only a single search property. @@ -12245,7 +15101,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue * var hash = $location.hash(); * // => "hashValue" * ``` @@ -12306,6 +15162,7 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun // 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; + this.$$urlUpdatedByLocation = true; return this; }; @@ -12313,14 +15170,14 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun function locationGetter(property) { - return function() { + return /** @this */ function() { return this[property]; }; } function locationGetterSetter(property, preprocess) { - return function(value) { + return /** @this */ function(value) { if (isUndefined(value)) { return this[property]; } @@ -12362,11 +15219,13 @@ function locationGetterSetter(property, preprocess) { /** * @ngdoc provider * @name $locationProvider + * @this + * * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ function $LocationProvider() { - var hashPrefix = '', + var hashPrefix = '!', html5Mode = { enabled: false, requireBase: true, @@ -12377,6 +15236,7 @@ function $LocationProvider() { * @ngdoc method * @name $locationProvider#hashPrefix * @description + * The default value for the prefix is `'!'`. * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -12403,8 +15263,12 @@ function $LocationProvider() { * 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. + * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled, + * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will + * only happen on links with an attribute that matches the given string. For example, if set + * to `'internal-link'`, then the URL will only be rewritten for `` links. + * Note that [attribute name normalization](guide/directive#normalization) does not apply + * here, so `'internalLink'` will **not** match `'internal-link'`. * * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ @@ -12422,7 +15286,7 @@ function $LocationProvider() { html5Mode.requireBase = mode.requireBase; } - if (isBoolean(mode.rewriteLinks)) { + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { html5Mode.rewriteLinks = mode.rewriteLinks; } @@ -12482,7 +15346,7 @@ function $LocationProvider() { if (html5Mode.enabled) { if (!baseHref && html5Mode.requireBase) { throw $locationMinErr('nobase', - "$location in HTML5 mode requires a tag to be present!"); + '$location in HTML5 mode requires a tag to be present!'); } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; @@ -12499,6 +15363,13 @@ function $LocationProvider() { var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; + // Determine if two URLs are equal despite potentially having different encoding/normalizing + // such as $location.absUrl() vs $browser.url() + // See https://github.com/angular/angular.js/issues/16592 + function urlsEqual(a, b) { + return a === b || urlResolve(a).href === urlResolve(b).href; + } + function setBrowserUrlWithFallback(url, replace, state) { var oldUrl = $location.url(); var oldState = $location.$$state; @@ -12519,10 +15390,11 @@ function $LocationProvider() { } $rootElement.on('click', function(event) { + var rewriteLinks = html5Mode.rewriteLinks; // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return; + if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; var elm = jqLite(event.target); @@ -12532,6 +15404,8 @@ function $LocationProvider() { if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } + if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return; + var absHref = elm.prop('href'); // get the actual href attribute - see // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx @@ -12548,15 +15422,13 @@ 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, + // We do a preventDefault for all urls that are part of the AngularJS 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()) { + if ($location.absUrl() !== $browser.url()) { $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - $window.angular['ff-684208-preventDefault'] = true; } } } @@ -12564,7 +15436,7 @@ function $LocationProvider() { // rewrite hashbang url <> html5 url - if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) { + if ($location.absUrl() !== initialUrl) { $browser.url($location.absUrl(), true); } @@ -12573,7 +15445,7 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { - if (isUndefined(beginsWith(appBaseNoFile, newUrl))) { + if (!startsWith(newUrl, appBaseNoFile)) { // If we are navigating outside of the app then force a reload $window.location.href = newUrl; return; @@ -12583,7 +15455,6 @@ function $LocationProvider() { var oldUrl = $location.absUrl(); var oldState = $location.$$state; var defaultPrevented; - newUrl = trimEmptyHash(newUrl); $location.$$parse(newUrl); $location.$$state = newState; @@ -12608,36 +15479,40 @@ function $LocationProvider() { // update browser $rootScope.$watch(function $locationWatch() { - var oldUrl = trimEmptyHash($browser.url()); - var newUrl = trimEmptyHash($location.absUrl()); - var oldState = $browser.state(); - var currentReplace = $location.$$replace; - var urlOrStateChanged = oldUrl !== newUrl || - ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); + if (initializing || $location.$$urlUpdatedByLocation) { + $location.$$urlUpdatedByLocation = false; - if (initializing || urlOrStateChanged) { - initializing = false; + var oldUrl = $browser.url(); + var newUrl = $location.absUrl(); + var oldState = $browser.state(); + var currentReplace = $location.$$replace; + var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); - $rootScope.$evalAsync(function() { - var newUrl = $location.absUrl(); - var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, - $location.$$state, oldState).defaultPrevented; + if (initializing || urlOrStateChanged) { + initializing = false; - // if the location was changed by a `$locationChangeStart` handler then stop - // processing this location change - if ($location.absUrl() !== newUrl) return; + $rootScope.$evalAsync(function() { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; - if (defaultPrevented) { - $location.$$parse(oldUrl); - $location.$$state = oldState; - } else { - if (urlOrStateChanged) { - setBrowserUrlWithFallback(newUrl, currentReplace, - oldState === $location.$$state ? null : $location.$$state); + // 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 { + if (urlOrStateChanged) { + setBrowserUrlWithFallback(newUrl, currentReplace, + oldState === $location.$$state ? null : $location.$$state); + } + afterLocationChange(oldUrl, oldState); } - afterLocationChange(oldUrl, oldState); - } - }); + }); + } } $location.$$replace = false; @@ -12666,11 +15541,19 @@ function $LocationProvider() { * * The main purpose of this service is to simplify debugging and troubleshooting. * + * To reveal the location of the calls to `$log` in the JavaScript console, + * you can "blackbox" the AngularJS source in your browser: + * + * [Mozilla description of blackboxing](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Black_box_a_source). + * [Chrome description of blackboxing](https://developer.chrome.com/devtools/docs/blackboxing). + * + * Note: Not all browsers support blackboxing. + * * The default is to log `debug` messages. You can use * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - + angular.module('logExample', []) .controller('LogController', ['$scope', '$log', function($scope, $log) { @@ -12696,6 +15579,8 @@ function $LocationProvider() { /** * @ngdoc provider * @name $logProvider + * @this + * * @description * Use the `$logProvider` to configure how the application logs messages */ @@ -12713,13 +15598,22 @@ function $LogProvider() { this.debugEnabled = function(flag) { if (isDefined(flag)) { debug = flag; - return this; + return this; } else { return debug; } }; this.$get = ['$window', function($window) { + // Support: IE 9-11, Edge 12-14+ + // IE/Edge display errors in such a way that it requires the user to click in 4 places + // to see the stack trace. There is no way to feature-detect it so there's a chance + // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't + // break apps. Other browsers display errors in a sensible way and some of them map stack + // traces along source maps if available so it makes sense to let browsers display it + // as they want. + var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent); + return { /** * @ngdoc method @@ -12772,12 +15666,12 @@ function $LogProvider() { fn.apply(self, arguments); } }; - }()) + })() }; function formatError(arg) { - if (arg instanceof Error) { - if (arg.stack) { + if (isError(arg)) { + if (arg.stack && formatStackTrace) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; @@ -12790,29 +15684,17 @@ function $LogProvider() { function consoleLog(type) { var console = $window.console || {}, - logFn = console[type] || console.log || noop, - hasApply = false; - - // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. - // The reason behind this is that console.log has type "object" in IE8... - try { - hasApply = !!logFn.apply; - } catch (e) {} - - if (hasApply) { - return function() { - var args = []; - forEach(arguments, function(arg) { - args.push(formatError(arg)); - }); - return logFn.apply(console, args); - }; - } + logFn = console[type] || console.log || noop; - // we are IE which either doesn't have window.console => this is noop and we do nothing, - // or we are IE where console.log doesn't have apply so we log at least first 2 args - return function(arg1, arg2) { - logFn(arg1, arg2 == null ? '' : arg2); + return function() { + var args = []; + forEach(arguments, function(arg) { + args.push(formatError(arg)); + }); + // Support: IE 9 only + // console methods don't inherit from Function.prototype in IE 9 so we can't + // call `logFn.apply(console, args)` directly. + return Function.prototype.apply.call(logFn, console, args); }; } }]; @@ -12831,118 +15713,45 @@ function $LogProvider() { var $parseMinErr = minErr('$parse'); -// Sandboxing Angular Expressions +var objectValueOf = {}.constructor.prototype.valueOf; + +// Sandboxing AngularJS 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 -// obtaining a reference to native JS functions such as the Function constructor. +// AngularJS expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // -// As an example, consider the following Angular expression: +// As an example, consider the following AngularJS expression: // // {}.toString.constructor('alert("evil JS code")') // -// 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 -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// 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. +// It is important to realize that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // // See https://docs.angularjs.org/guide/security -function ensureSafeMemberName(name, fullExpression) { - if (name === "__defineGetter__" || name === "__defineSetter__" - || name === "__lookupGetter__" || name === "__lookupSetter__" - || name === "__proto__") { - throw $parseMinErr('isecfld', - 'Attempting to access a disallowed field in Angular expressions! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - -function getStringValue(name, fullExpression) { - // From the JavaScript docs: +function getStringValue(name) { // Property names must be strings. This means that non-string objects cannot be used // as keys in an object. Any non-string object, including a number, is typecasted // into a string via the toString method. + // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names // - // So, to ensure that we are checking the same `name` that JavaScript would use, - // we cast it to a string, if possible. - // Doing `name + ''` can cause a repl error if the result to `toString` is not a string, - // this is, this will handle objects that misbehave. - name = name + ''; - if (!isString(name)) { - throw $parseMinErr('iseccst', - 'Cannot convert object to primitive value! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.window === obj) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// block Object so that we can't get hold of dangerous Object.* methods - obj === Object) { - throw $parseMinErr('isecobj', - 'Referencing Object in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; -} - -var CALL = Function.prototype.call; -var APPLY = Function.prototype.apply; -var BIND = Function.prototype.bind; - -function ensureSafeFunction(obj, fullExpression) { - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (obj === CALL || obj === APPLY || obj === BIND) { - throw $parseMinErr('isecff', - 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } + // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it + // to a string. It's not always possible. If `name` is an object and its `toString` method is + // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown: + // + // TypeError: Cannot convert object to primitive value + // + // For performance reasons, we don't catch this error here and allow it to propagate up the call + // stack. Note that you'll get the same error in JavaScript if you try to access a property using + // such a 'broken' object as a key. + return name + ''; } -function ensureSafeAssignContext(obj, fullExpression) { - if (obj) { - if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor || - obj === {}.constructor || obj === [].constructor || obj === Function.constructor) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression); - } - } -} var OPERATORS = createMap(); forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); -var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; +var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'}; ///////////////////////////////////////// @@ -12951,7 +15760,7 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"' /** * @constructor */ -var Lexer = function(options) { +var Lexer = function Lexer(options) { this.options = options; }; @@ -12965,11 +15774,11 @@ Lexer.prototype = { while (this.index < this.text.length) { var ch = this.text.charAt(this.index); - if (ch === '"' || ch === "'") { + if (ch === '"' || ch === '\'') { this.readString(ch); } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); - } else if (this.isIdent(ch)) { + } else if (this.isIdentifierStart(this.peekMultichar())) { this.readIdent(); } else if (this.is(ch, '(){}[].,;:?')) { this.tokens.push({index: this.index, text: ch}); @@ -13004,7 +15813,7 @@ Lexer.prototype = { }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9') && typeof ch === "string"; + return ('0' <= ch && ch <= '9') && typeof ch === 'string'; }, isWhitespace: function(ch) { @@ -13013,12 +15822,48 @@ Lexer.prototype = { ch === '\n' || ch === '\v' || ch === '\u00A0'); }, - isIdent: function(ch) { + isIdentifierStart: function(ch) { + return this.options.isIdentifierStart ? + this.options.isIdentifierStart(ch, this.codePointAt(ch)) : + this.isValidIdentifierStart(ch); + }, + + isValidIdentifierStart: function(ch) { return ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '_' === ch || ch === '$'); }, + isIdentifierContinue: function(ch) { + return this.options.isIdentifierContinue ? + this.options.isIdentifierContinue(ch, this.codePointAt(ch)) : + this.isValidIdentifierContinue(ch); + }, + + isValidIdentifierContinue: function(ch, cp) { + return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch); + }, + + codePointAt: function(ch) { + if (ch.length === 1) return ch.charCodeAt(0); + // eslint-disable-next-line no-bitwise + return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00; + }, + + peekMultichar: function() { + var ch = this.text.charAt(this.index); + var peek = this.peek(); + if (!peek) { + return ch; + } + var cp1 = ch.charCodeAt(0); + var cp2 = peek.charCodeAt(0); + if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) { + return ch + peek; + } + return ch; + }, + isExpOperator: function(ch) { return (ch === '-' || ch === '+' || this.isNumber(ch)); }, @@ -13037,19 +15882,19 @@ Lexer.prototype = { var start = this.index; while (this.index < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { + if (ch === '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { + if (ch === 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { number += ch; } else if (this.isExpOperator(ch) && (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { this.throwError('Invalid exponent'); } else { break; @@ -13067,12 +15912,13 @@ Lexer.prototype = { readIdent: function() { var start = this.index; + this.index += this.peekMultichar().length; while (this.index < this.text.length) { - var ch = this.text.charAt(this.index); - if (!(this.isIdent(ch) || this.isNumber(ch))) { + var ch = this.peekMultichar(); + if (!this.isIdentifierContinue(ch)) { break; } - this.index++; + this.index += ch.length; } this.tokens.push({ index: start, @@ -13123,7 +15969,7 @@ Lexer.prototype = { } }; -var AST = function(lexer, options) { +var AST = function AST(lexer, options) { this.lexer = lexer; this.options = options; }; @@ -13143,6 +15989,7 @@ AST.ArrayExpression = 'ArrayExpression'; AST.Property = 'Property'; AST.ObjectExpression = 'ObjectExpression'; AST.ThisExpression = 'ThisExpression'; +AST.LocalsExpression = 'LocalsExpression'; // Internal use only AST.NGValueParameter = 'NGValueParameter'; @@ -13178,8 +16025,7 @@ AST.prototype = { filterChain: function() { var left = this.expression(); - var token; - while ((token = this.expect('|'))) { + while (this.expect('|')) { left = this.filter(left); } return left; @@ -13192,6 +16038,10 @@ AST.prototype = { assignment: function() { var result = this.ternary(); if (this.expect('=')) { + if (!isAssignable(result)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + } + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; } return result; @@ -13281,8 +16131,10 @@ AST.prototype = { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); - } else if (this.constants.hasOwnProperty(this.peek().text)) { - primary = copy(this.constants[this.consume().text]); + } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { + primary = copy(this.selfReferential[this.consume().text]); + } else if (this.options.literals.hasOwnProperty(this.peek().text)) { + primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { @@ -13323,7 +16175,7 @@ AST.prototype = { var args = []; if (this.peekToken().text !== ')') { do { - args.push(this.expression()); + args.push(this.filterChain()); } while (this.expect(',')); } return args; @@ -13369,13 +16221,28 @@ AST.prototype = { property = {type: AST.Property, kind: 'init'}; if (this.peek().constant) { property.key = this.constant(); + property.computed = false; + this.consume(':'); + property.value = this.expression(); } else if (this.peek().identifier) { property.key = this.identifier(); + property.computed = false; + if (this.peek(':')) { + this.consume(':'); + property.value = this.expression(); + } else { + property.value = property.key; + } + } else if (this.peek('[')) { + this.consume('['); + property.key = this.expression(); + this.consume(']'); + property.computed = true; + this.consume(':'); + property.value = this.expression(); } else { - this.throwError("invalid key", this.peek()); + this.throwError('invalid key', this.peek()); } - this.consume(':'); - property.value = this.expression(); properties.push(property); } while (this.expect(',')); } @@ -13434,16 +16301,9 @@ AST.prototype = { return false; }, - - /* `undefined` is not a constant, it is an identifier, - * but using it as an identifier is not supported - */ - constants: { - 'true': { type: AST.Literal, value: true }, - 'false': { type: AST.Literal, value: false }, - 'null': { type: AST.Literal, value: null }, - 'undefined': {type: AST.Literal, value: undefined }, - 'this': {type: AST.ThisExpression } + selfReferential: { + 'this': {type: AST.ThisExpression }, + '$locals': {type: AST.LocalsExpression } } }; @@ -13462,14 +16322,47 @@ function isStateless($filter, filterName) { return !fn.$stateful; } -function findConstantAndWatchExpressions(ast, $filter) { +var PURITY_ABSOLUTE = 1; +var PURITY_RELATIVE = 2; + +// Detect nodes which could depend on non-shallow state of objects +function isPure(node, parentIsPure) { + switch (node.type) { + // Computed members might invoke a stateful toString() + case AST.MemberExpression: + if (node.computed) { + return false; + } + break; + + // Unary always convert to primative + case AST.UnaryExpression: + return PURITY_ABSOLUTE; + + // The binary + operator can invoke a stateful toString(). + case AST.BinaryExpression: + return node.operator !== '+' ? PURITY_ABSOLUTE : false; + + // Functions / filters probably read state from within objects + case AST.CallExpression: + return false; + } + + return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure; +} + +function findConstantAndWatchExpressions(ast, $filter, parentIsPure) { var allConstants; var argsToWatch; + var isStatelessFilter; + + var astIsPure = ast.isPure = isPure(ast, parentIsPure); + switch (ast.type) { case AST.Program: allConstants = true; forEach(ast.body, function(expr) { - findConstantAndWatchExpressions(expr.expression, $filter); + findConstantAndWatchExpressions(expr.expression, $filter, astIsPure); allConstants = allConstants && expr.expression.constant; }); ast.constant = allConstants; @@ -13479,26 +16372,26 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.toWatch = []; break; case AST.UnaryExpression: - findConstantAndWatchExpressions(ast.argument, $filter); + findConstantAndWatchExpressions(ast.argument, $filter, astIsPure); ast.constant = ast.argument.constant; ast.toWatch = ast.argument.toWatch; break; case AST.BinaryExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); ast.constant = ast.left.constant && ast.right.constant; ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); break; case AST.LogicalExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); ast.constant = ast.left.constant && ast.right.constant; ast.toWatch = ast.constant ? [] : [ast]; break; case AST.ConditionalExpression: - findConstantAndWatchExpressions(ast.test, $filter); - findConstantAndWatchExpressions(ast.alternate, $filter); - findConstantAndWatchExpressions(ast.consequent, $filter); + findConstantAndWatchExpressions(ast.test, $filter, astIsPure); + findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure); + findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure); ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant; ast.toWatch = ast.constant ? [] : [ast]; break; @@ -13507,29 +16400,28 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.toWatch = [ast]; break; case AST.MemberExpression: - findConstantAndWatchExpressions(ast.object, $filter); + findConstantAndWatchExpressions(ast.object, $filter, astIsPure); if (ast.computed) { - findConstantAndWatchExpressions(ast.property, $filter); + findConstantAndWatchExpressions(ast.property, $filter, astIsPure); } ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); - ast.toWatch = [ast]; + ast.toWatch = ast.constant ? [] : [ast]; break; case AST.CallExpression: - allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false; + isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false; + allConstants = isStatelessFilter; argsToWatch = []; forEach(ast.arguments, function(expr) { - findConstantAndWatchExpressions(expr, $filter); + findConstantAndWatchExpressions(expr, $filter, astIsPure); allConstants = allConstants && expr.constant; - if (!expr.constant) { - argsToWatch.push.apply(argsToWatch, expr.toWatch); - } + argsToWatch.push.apply(argsToWatch, expr.toWatch); }); ast.constant = allConstants; - ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; + ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; break; case AST.AssignmentExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); ast.constant = ast.left.constant && ast.right.constant; ast.toWatch = [ast]; break; @@ -13537,11 +16429,9 @@ function findConstantAndWatchExpressions(ast, $filter) { allConstants = true; argsToWatch = []; forEach(ast.elements, function(expr) { - findConstantAndWatchExpressions(expr, $filter); + findConstantAndWatchExpressions(expr, $filter, astIsPure); allConstants = allConstants && expr.constant; - if (!expr.constant) { - argsToWatch.push.apply(argsToWatch, expr.toWatch); - } + argsToWatch.push.apply(argsToWatch, expr.toWatch); }); ast.constant = allConstants; ast.toWatch = argsToWatch; @@ -13550,10 +16440,14 @@ function findConstantAndWatchExpressions(ast, $filter) { allConstants = true; argsToWatch = []; forEach(ast.properties, function(property) { - findConstantAndWatchExpressions(property.value, $filter); + findConstantAndWatchExpressions(property.value, $filter, astIsPure); allConstants = allConstants && property.value.constant; - if (!property.value.constant) { - argsToWatch.push.apply(argsToWatch, property.value.toWatch); + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + if (property.computed) { + //`{[key]: value}` implicitly does `key.toString()` which may be non-pure + findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false); + allConstants = allConstants && property.key.constant; + argsToWatch.push.apply(argsToWatch, property.key.toWatch); } }); ast.constant = allConstants; @@ -13563,11 +16457,15 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.constant = false; ast.toWatch = []; break; + case AST.LocalsExpression: + ast.constant = false; + ast.toWatch = []; + break; } } function getInputs(body) { - if (body.length != 1) return; + if (body.length !== 1) return; var lastExpression = body[0].expression; var candidate = lastExpression.toWatch; if (candidate.length !== 1) return candidate; @@ -13596,19 +16494,16 @@ function isConstant(ast) { return ast.constant; } -function ASTCompiler(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTCompiler($filter) { this.$filter = $filter; } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(ast) { var self = this; - var ast = this.astBuilder.ast(expression); this.state = { nextId: 0, filters: {}, - expensiveChecks: expensiveChecks, fn: {vars: [], body: [], own: {}}, assign: {vars: [], body: [], own: {}}, inputs: [] @@ -13633,7 +16528,7 @@ ASTCompiler.prototype = { var intoId = self.nextId(); self.recurse(watch, intoId); self.return_(intoId); - self.state.inputs.push(fnKey); + self.state.inputs.push({name: fnKey, isPure: watch.isPure}); watch.watchId = key; }); this.state.computing = 'fn'; @@ -13649,30 +16544,17 @@ ASTCompiler.prototype = { this.watchFns() + 'return fn;'; - /* jshint -W054 */ + // eslint-disable-next-line no-new-func var fn = (new Function('$filter', - 'ensureSafeMemberName', - 'ensureSafeObject', - 'ensureSafeFunction', 'getStringValue', - 'ensureSafeAssignContext', 'ifDefined', 'plus', - 'text', fnString))( this.$filter, - ensureSafeMemberName, - ensureSafeObject, - ensureSafeFunction, getStringValue, - ensureSafeAssignContext, ifDefined, - plusFn, - expression); - /* jshint +W054 */ + plusFn); this.state = this.stage = undefined; - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); return fn; }, @@ -13682,13 +16564,16 @@ ASTCompiler.prototype = { watchFns: function() { var result = []; - var fns = this.state.inputs; + var inputs = this.state.inputs; var self = this; - forEach(fns, function(name) { - result.push('var ' + name + '=' + self.generateFunction(name, 's')); + forEach(inputs, function(input) { + result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's')); + if (input.isPure) { + result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';'); + } }); - if (fns.length) { - result.push('fn.inputs=[' + fns.join(',') + '];'); + if (inputs.length) { + result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];'); } return result.join(''); }, @@ -13719,7 +16604,7 @@ ASTCompiler.prototype = { }, recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { - var left, right, self = this, args, expression; + var left, right, self = this, args, expression, computed; recursionFn = recursionFn || noop; if (!skipWatchIdCheck && isDefined(ast.watchId)) { intoId = intoId || this.nextId(); @@ -13743,7 +16628,7 @@ ASTCompiler.prototype = { case AST.Literal: expression = this.escape(ast.value); this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.UnaryExpression: this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); @@ -13783,22 +16668,18 @@ ASTCompiler.prototype = { nameId.computed = false; nameId.name = ast.name; } - ensureSafeMemberName(ast.name); self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), function() { self.if_(self.stage === 'inputs' || 's', function() { if (create && create !== 1) { self.if_( - self.not(self.nonComputedMember('s', ast.name)), + self.isNull(self.nonComputedMember('s', ast.name)), self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); } self.assign(intoId, self.nonComputedMember('s', ast.name)); }); }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) ); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - self.addEnsureSafeObject(intoId); - } recursionFn(intoId); break; case AST.MemberExpression: @@ -13806,32 +16687,24 @@ ASTCompiler.prototype = { intoId = intoId || this.nextId(); self.recurse(ast.object, left, undefined, function() { self.if_(self.notNull(left), function() { - if (create && create !== 1) { - self.addEnsureSafeAssignContext(left); - } if (ast.computed) { right = self.nextId(); self.recurse(ast.property, right); self.getStringValue(right); - self.addEnsureSafeMemberName(right); if (create && create !== 1) { self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); } - expression = self.ensureSafeObject(self.computedMember(left, right)); + expression = self.computedMember(left, right); self.assign(intoId, expression); if (nameId) { nameId.computed = true; nameId.name = right; } } else { - ensureSafeMemberName(ast.property.name); if (create && create !== 1) { - self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); } expression = self.nonComputedMember(left, ast.property.name); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { - expression = self.ensureSafeObject(expression); - } self.assign(intoId, expression); if (nameId) { nameId.computed = false; @@ -13863,21 +16736,16 @@ ASTCompiler.prototype = { args = []; self.recurse(ast.callee, right, left, function() { self.if_(self.notNull(right), function() { - self.addEnsureSafeFunction(right); forEach(ast.arguments, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { - args.push(self.ensureSafeObject(argument)); + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(argument); }); }); if (left.name) { - if (!self.state.expensiveChecks) { - self.addEnsureSafeObject(left.context); - } expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; } else { expression = right + '(' + args.join(',') + ')'; } - expression = self.ensureSafeObject(expression); self.assign(intoId, expression); }, function() { self.assign(intoId, 'undefined'); @@ -13889,14 +16757,9 @@ ASTCompiler.prototype = { case AST.AssignmentExpression: right = this.nextId(); left = {}; - if (!isAssignable(ast.left)) { - throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); - } this.recurse(ast.left, undefined, left, function() { self.if_(self.notNull(left.context), function() { self.recurse(ast.right, right); - self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); - self.addEnsureSafeAssignContext(left.context); expression = self.member(left.context, left.name, left.computed) + ast.operator + right; self.assign(intoId, expression); recursionFn(intoId || expression); @@ -13906,35 +16769,63 @@ ASTCompiler.prototype = { case AST.ArrayExpression: args = []; forEach(ast.elements, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { args.push(argument); }); }); expression = '[' + args.join(',') + ']'; this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.ObjectExpression: args = []; + computed = false; forEach(ast.properties, function(property) { - self.recurse(property.value, self.nextId(), undefined, function(expr) { - args.push(self.escape( - property.key.type === AST.Identifier ? property.key.name : - ('' + property.key.value)) + - ':' + expr); - }); + if (property.computed) { + computed = true; + } }); - expression = '{' + args.join(',') + '}'; - this.assign(intoId, expression); - recursionFn(expression); + if (computed) { + intoId = intoId || this.nextId(); + this.assign(intoId, '{}'); + forEach(ast.properties, function(property) { + if (property.computed) { + left = self.nextId(); + self.recurse(property.key, left); + } else { + left = property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value); + } + right = self.nextId(); + self.recurse(property.value, right); + self.assign(self.member(intoId, left, property.computed), right); + }); + } else { + forEach(ast.properties, function(property) { + self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) { + args.push(self.escape( + property.key.type === AST.Identifier ? property.key.name : + ('' + property.key.value)) + + ':' + expr); + }); + }); + expression = '{' + args.join(',') + '}'; + this.assign(intoId, expression); + } + recursionFn(intoId || expression); break; case AST.ThisExpression: this.assign(intoId, 's'); - recursionFn('s'); + recursionFn(intoId || 's'); + break; + case AST.LocalsExpression: + this.assign(intoId, 'l'); + recursionFn(intoId || 'l'); break; case AST.NGValueParameter: this.assign(intoId, 'v'); - recursionFn('v'); + recursionFn(intoId || 'v'); break; } }, @@ -13993,12 +16884,22 @@ ASTCompiler.prototype = { return '!(' + expression + ')'; }, + isNull: function(expression) { + return expression + '==null'; + }, + notNull: function(expression) { return expression + '!=null'; }, nonComputedMember: function(left, right) { - return left + '.' + right; + var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/; + var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g; + if (SAFE_IDENTIFIER.test(right)) { + return left + '.' + right; + } else { + return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]'; + } }, computedMember: function(left, right) { @@ -14010,40 +16911,8 @@ ASTCompiler.prototype = { return this.nonComputedMember(left, right); }, - addEnsureSafeObject: function(item) { - this.current().body.push(this.ensureSafeObject(item), ';'); - }, - - addEnsureSafeMemberName: function(item) { - this.current().body.push(this.ensureSafeMemberName(item), ';'); - }, - - addEnsureSafeFunction: function(item) { - this.current().body.push(this.ensureSafeFunction(item), ';'); - }, - - addEnsureSafeAssignContext: function(item) { - this.current().body.push(this.ensureSafeAssignContext(item), ';'); - }, - - ensureSafeObject: function(item) { - return 'ensureSafeObject(' + item + ',text)'; - }, - - ensureSafeMemberName: function(item) { - return 'ensureSafeMemberName(' + item + ',text)'; - }, - - ensureSafeFunction: function(item) { - return 'ensureSafeFunction(' + item + ',text)'; - }, - getStringValue: function(item) { - this.assign(item, 'getStringValue(' + item + ',text)'); - }, - - ensureSafeAssignContext: function(item) { - return 'ensureSafeAssignContext(' + item + ',text)'; + this.assign(item, 'getStringValue(' + item + ')'); }, lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { @@ -14067,7 +16936,7 @@ ASTCompiler.prototype = { }, escape: function(value) { - if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'"; + if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\''; if (isNumber(value)) return value.toString(); if (value === true) return 'true'; if (value === false) return 'false'; @@ -14091,17 +16960,13 @@ ASTCompiler.prototype = { }; -function ASTInterpreter(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTInterpreter($filter) { this.$filter = $filter; } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(ast) { var self = this; - var ast = this.astBuilder.ast(expression); - this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -14114,6 +16979,7 @@ ASTInterpreter.prototype = { inputs = []; forEach(toWatch, function(watch, key) { var input = self.recurse(watch); + input.isPure = watch.isPure; watch.input = input; inputs.push(input); watch.watchId = key; @@ -14123,7 +16989,7 @@ ASTInterpreter.prototype = { forEach(ast.body, function(expression) { expressions.push(self.recurse(expression.expression)); }); - var fn = ast.body.length === 0 ? function() {} : + var fn = ast.body.length === 0 ? noop : ast.body.length === 1 ? expressions[0] : function(scope, locals) { var lastValue; @@ -14140,13 +17006,11 @@ ASTInterpreter.prototype = { if (inputs) { fn.inputs = inputs; } - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); return fn; }, recurse: function(ast, context, create) { - var left, right, self = this, args, expression; + var left, right, self = this, args; if (ast.input) { return this.inputs(ast.input, ast.watchId); } @@ -14172,20 +17036,16 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); - return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), - context, create, self.expression); + return self.identifier(ast.name, context, create); case AST.MemberExpression: left = this.recurse(ast.object, false, !!create); if (!ast.computed) { - ensureSafeMemberName(ast.property.name, self.expression); right = ast.property.name; } if (ast.computed) right = this.recurse(ast.property); return ast.computed ? - this.computedMember(left, right, context, create, self.expression) : - this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + this.computedMember(left, right, context, create) : + this.nonComputedMember(left, right, context, create); case AST.CallExpression: args = []; forEach(ast.arguments, function(expr) { @@ -14206,13 +17066,11 @@ ASTInterpreter.prototype = { var rhs = right(scope, locals, assign, inputs); var value; if (rhs.value != null) { - ensureSafeObject(rhs.context, self.expression); - ensureSafeFunction(rhs.value, self.expression); var values = []; for (var i = 0; i < args.length; ++i) { - values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + values.push(args[i](scope, locals, assign, inputs)); } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + value = rhs.value.apply(rhs.context, values); } return context ? {value: value} : value; }; @@ -14222,8 +17080,6 @@ ASTInterpreter.prototype = { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs = right(scope, locals, assign, inputs); - ensureSafeObject(lhs.value, self.expression); - ensureSafeAssignContext(lhs.context); lhs.context[lhs.name] = rhs; return context ? {value: rhs} : rhs; }; @@ -14242,16 +17098,28 @@ ASTInterpreter.prototype = { case AST.ObjectExpression: args = []; forEach(ast.properties, function(property) { - args.push({key: property.key.type === AST.Identifier ? - property.key.name : - ('' + property.key.value), - value: self.recurse(property.value) - }); + if (property.computed) { + args.push({key: self.recurse(property.key), + computed: true, + value: self.recurse(property.value) + }); + } else { + args.push({key: property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value), + computed: false, + value: self.recurse(property.value) + }); + } }); return function(scope, locals, assign, inputs) { var value = {}; for (var i = 0; i < args.length; ++i) { - value[args[i].key] = args[i].value(scope, locals, assign, inputs); + if (args[i].computed) { + value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs); + } else { + value[args[i].key] = args[i].value(scope, locals, assign, inputs); + } } return context ? {value: value} : value; }; @@ -14259,8 +17127,12 @@ ASTInterpreter.prototype = { return function(scope) { return context ? {value: scope} : scope; }; + case AST.LocalsExpression: + return function(scope, locals) { + return context ? {value: locals} : locals; + }; case AST.NGValueParameter: - return function(scope, locals, assign, inputs) { + return function(scope, locals, assign) { return context ? {value: assign} : assign; }; } @@ -14283,7 +17155,7 @@ ASTInterpreter.prototype = { if (isDefined(arg)) { arg = -arg; } else { - arg = 0; + arg = -0; } return context ? {value: arg} : arg; }; @@ -14342,12 +17214,14 @@ ASTInterpreter.prototype = { }, 'binary==': function(left, right, context) { return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs); return context ? {value: arg} : arg; }; }, 'binary!=': function(left, right, context) { return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); return context ? {value: arg} : arg; }; @@ -14397,16 +17271,13 @@ ASTInterpreter.prototype = { value: function(value, context) { return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; }, - identifier: function(name, expensiveChecks, context, create, expression) { + identifier: function(name, context, create) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; - if (create && create !== 1 && base && !(base[name])) { + if (create && create !== 1 && base && base[name] == null) { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } if (context) { return {context: base, name: name, value: value}; } else { @@ -14414,7 +17285,7 @@ ASTInterpreter.prototype = { } }; }, - computedMember: function(left, right, context, create, expression) { + computedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs; @@ -14422,15 +17293,12 @@ ASTInterpreter.prototype = { if (lhs != null) { rhs = right(scope, locals, assign, inputs); rhs = getStringValue(rhs); - ensureSafeMemberName(rhs, expression); if (create && create !== 1) { - ensureSafeAssignContext(lhs); if (lhs && !(lhs[rhs])) { lhs[rhs] = {}; } } value = lhs[rhs]; - ensureSafeObject(value, expression); } if (context) { return {context: lhs, name: rhs, value: value}; @@ -14439,19 +17307,15 @@ ASTInterpreter.prototype = { } }; }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + nonComputedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); if (create && create !== 1) { - ensureSafeAssignContext(lhs); - if (lhs && !(lhs[right])) { + if (lhs && lhs[right] == null) { lhs[right] = {}; } } var value = lhs != null ? lhs[right] : undefined; - if (expensiveChecks || isPossiblyDangerousMemberName(right)) { - ensureSafeObject(value, expression); - } if (context) { return {context: lhs, name: right, value: value}; } else { @@ -14470,28 +17334,38 @@ ASTInterpreter.prototype = { /** * @constructor */ -var Parser = function(lexer, $filter, options) { - this.lexer = lexer; - this.$filter = $filter; - this.options = options; - this.ast = new AST(this.lexer); - this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : - new ASTCompiler(this.ast, $filter); -}; +function Parser(lexer, $filter, options) { + this.ast = new AST(lexer, options); + this.astCompiler = options.csp ? new ASTInterpreter($filter) : + new ASTCompiler($filter); +} Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); - } -}; + var ast = this.getAst(text); + var fn = this.astCompiler.compile(ast.ast); + fn.literal = isLiteral(ast.ast); + fn.constant = isConstant(ast.ast); + fn.oneTime = ast.oneTime; + return fn; + }, -function isPossiblyDangerousMemberName(name) { - return name == 'constructor'; -} + getAst: function(exp) { + var oneTime = false; + exp = exp.trim(); -var objectValueOf = Object.prototype.valueOf; + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } + return { + ast: this.ast.ast(exp), + oneTime: oneTime + }; + } +}; function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); @@ -14506,15 +17380,15 @@ function getValueOf(value) { * * @description * - * Converts Angular {@link guide/expression expression} into a function. + * Converts AngularJS {@link guide/expression expression} into a function. * * ```js * var getter = $parse('user.name'); * var setter = getter.assign; - * var context = {user:{name:'angular'}}; + * var context = {user:{name:'AngularJS'}}; * var locals = {user:{name:'local'}}; * - * expect(getter(context)).toEqual('angular'); + * expect(getter(context)).toEqual('AngularJS'); * setter(context, 'newValue'); * expect(context.user.name).toEqual('newValue'); * expect(getter(context, locals)).toEqual('local'); @@ -14543,67 +17417,94 @@ function getValueOf(value) { /** * @ngdoc provider * @name $parseProvider + * @this * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); + var cache = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined + }; + var identStart, identContinue; + + /** + * @ngdoc method + * @name $parseProvider#addLiteral + * @description + * + * Configure $parse service to add literal values that will be present as literal at expressions. + * + * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. + * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. + * + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; + }; + + /** + * @ngdoc method + * @name $parseProvider#setIdentifierFns + * + * @description + * + * Allows defining the set of characters that are allowed in AngularJS expressions. The function + * `identifierStart` will get called to know if a given character is a valid character to be the + * first character for an identifier. The function `identifierContinue` will get called to know if + * a given character is a valid character to be a follow-up identifier character. The functions + * `identifierStart` and `identifierContinue` will receive as arguments the single character to be + * identifier and the character code point. These arguments will be `string` and `numeric`. Keep in + * mind that the `string` parameter can be two characters long depending on the character + * representation. It is expected for the function to return `true` or `false`, whether that + * character is allowed or not. + * + * Since this function will be called extensively, keep the implementation of these functions fast, + * as the performance of these functions have a direct impact on the expressions parsing speed. + * + * @param {function=} identifierStart The function that will decide whether the given character is + * a valid identifier start character. + * @param {function=} identifierContinue The function that will decide whether the given character is + * a valid identifier continue character. + */ + this.setIdentifierFns = function(identifierStart, identifierContinue) { + identStart = identifierStart; + identContinue = identifierContinue; + return this; + }; this.$get = ['$filter', function($filter) { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true + literals: copy(literals), + isIdentifierStart: isFunction(identStart) && identStart, + isIdentifierContinue: isFunction(identContinue) && identContinue }; - var runningChecksEnabled = false; - - $parse.$$runningExpensiveChecks = function() { - return runningChecksEnabled; - }; - + $parse.$$getAst = $$getAst; return $parse; - function $parse(exp, interceptorFn, expensiveChecks) { - var parsedExpression, oneTime, cacheKey; - - expensiveChecks = expensiveChecks || runningChecksEnabled; + function $parse(exp, interceptorFn) { + var parsedExpression, cacheKey; switch (typeof exp) { case 'string': exp = exp.trim(); cacheKey = exp; - var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { - if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { - oneTime = true; - exp = exp.substring(2); - } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; - var lexer = new Lexer(parseOptions); - var parser = new Parser(lexer, $filter, parseOptions); + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); parsedExpression = parser.parse(exp); - if (parsedExpression.constant) { - parsedExpression.$$watchDelegate = constantWatchDelegate; - } else if (oneTime) { - parsedExpression.$$watchDelegate = parsedExpression.literal ? - oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; - } else if (parsedExpression.inputs) { - parsedExpression.$$watchDelegate = inputsWatchDelegate; - } - if (expensiveChecks) { - parsedExpression = expensiveChecksInterceptor(parsedExpression); - } - cache[cacheKey] = parsedExpression; + + cache[cacheKey] = addWatchDelegate(parsedExpression); } return addInterceptor(parsedExpression, interceptorFn); @@ -14615,31 +17516,13 @@ function $ParseProvider() { } } - function expensiveChecksInterceptor(fn) { - if (!fn) return fn; - expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate; - expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign); - expensiveCheckFn.constant = fn.constant; - expensiveCheckFn.literal = fn.literal; - for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) { - fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]); - } - expensiveCheckFn.inputs = fn.inputs; - - return expensiveCheckFn; - - function expensiveCheckFn(scope, locals, assign, inputs) { - var expensiveCheckOldValue = runningChecksEnabled; - runningChecksEnabled = true; - try { - return fn(scope, locals, assign, inputs); - } finally { - runningChecksEnabled = expensiveCheckOldValue; - } - } + function $$getAst(exp) { + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + return parser.getAst(exp).ast; } - function expressionInputDirtyCheck(newValue, oldValueOfValue) { + function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) { if (newValue == null || oldValueOfValue == null) { // null/undefined return newValue === oldValueOfValue; @@ -14652,7 +17535,7 @@ function $ParseProvider() { // be cheaply dirty-checked newValue = getValueOf(newValue); - if (typeof newValue === 'object') { + if (typeof newValue === 'object' && !compareObjectIdentity) { // objects/arrays are not supported - deep-watching them would be too expensive return false; } @@ -14661,6 +17544,7 @@ function $ParseProvider() { } //Primitive or NaN + // eslint-disable-next-line no-self-compare return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); } @@ -14673,7 +17557,7 @@ function $ParseProvider() { inputExpressions = inputExpressions[0]; return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); - if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) { lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); oldInputValueOf = newInputValue && getValueOf(newInputValue); } @@ -14693,7 +17577,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]))) { + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) { oldInputValues[i] = newInputValue; oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } @@ -14707,95 +17591,127 @@ function $ParseProvider() { }, listener, objectEquality, prettyPrintExpression); } - function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var isDone = parsedExpression.literal ? isAllDefined : isDefined; var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { - return parsedExpression(scope); - }, function oneTimeListener(value, old, scope) { - lastValue = value; - if (isFunction(listener)) { - listener.apply(this, arguments); + + var exp = parsedExpression.$$intercepted || parsedExpression; + var post = parsedExpression.$$interceptor || identity; + + var useInputs = parsedExpression.inputs && !exp.inputs; + + // Propagate the literal/inputs/constant attributes + // ... but not oneTime since we are handling it + oneTimeWatch.literal = parsedExpression.literal; + oneTimeWatch.constant = parsedExpression.constant; + oneTimeWatch.inputs = parsedExpression.inputs; + + // Allow other delegates to run on this wrapped expression + addWatchDelegate(oneTimeWatch); + + unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression); + + return unwatch; + + function unwatchIfDone() { + if (isDone(lastValue)) { + unwatch(); } - if (isDefined(value)) { - scope.$$postDigest(function() { - if (isDefined(lastValue)) { - unwatch(); - } - }); + } + + function oneTimeWatch(scope, locals, assign, inputs) { + lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs); + if (isDone(lastValue)) { + scope.$$postDigest(unwatchIfDone); } - }, objectEquality); + return post(lastValue); + } } - function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { + function isAllDefined(value) { + var allDefined = true; + forEach(value, function(val) { + if (!isDefined(val)) allDefined = false; + }); + return allDefined; + } + + function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch = scope.$watch(function constantWatch(scope) { + unwatch(); 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(lastValue)) unwatch(); - }); - } - }, objectEquality); + }, listener, objectEquality); + return unwatch; + } - function isAllDefined(value) { - var allDefined = true; - forEach(value, function(val) { - if (!isDefined(val)) allDefined = false; - }); - return allDefined; + function addWatchDelegate(parsedExpression) { + if (parsedExpression.constant) { + parsedExpression.$$watchDelegate = constantWatchDelegate; + } else if (parsedExpression.oneTime) { + parsedExpression.$$watchDelegate = oneTimeWatchDelegate; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; } + + return parsedExpression; } - function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch; - return unwatch = scope.$watch(function constantWatch(scope) { - return parsedExpression(scope); - }, function constantListener(value, old, scope) { - if (isFunction(listener)) { - listener.apply(this, arguments); - } - unwatch(); - }, objectEquality); + function chainInterceptors(first, second) { + function chainedInterceptor(value) { + return second(first(value)); + } + chainedInterceptor.$stateful = first.$stateful || second.$stateful; + chainedInterceptor.$$pure = first.$$pure && second.$$pure; + + return chainedInterceptor; } function addInterceptor(parsedExpression, interceptorFn) { if (!interceptorFn) return parsedExpression; - var watchDelegate = parsedExpression.$$watchDelegate; - var useInputs = false; - var regularWatch = - watchDelegate !== oneTimeLiteralWatchDelegate && - watchDelegate !== oneTimeWatchDelegate; + // Extract any existing interceptors out of the parsedExpression + // to ensure the original parsedExpression is always the $$intercepted + if (parsedExpression.$$interceptor) { + interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn); + parsedExpression = parsedExpression.$$intercepted; + } + + var useInputs = false; - var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + var fn = function interceptedExpression(scope, locals, assign, inputs) { var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); - return interceptorFn(value, scope, locals); - } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { - var value = parsedExpression(scope, locals, assign, inputs); - var result = interceptorFn(value, scope, locals); - // we only return the interceptor's result if the - // initial value is defined (for bind-once) - return isDefined(value) ? result : value; + return interceptorFn(value); }; - // Propagate $$watchDelegates other then inputsWatchDelegate - if (parsedExpression.$$watchDelegate && - parsedExpression.$$watchDelegate !== inputsWatchDelegate) { - fn.$$watchDelegate = parsedExpression.$$watchDelegate; - } else if (!interceptorFn.$stateful) { - // If there is an interceptor, but no watchDelegate then treat the interceptor like - // we treat filters - it is assumed to be a pure function unless flagged with $stateful - fn.$$watchDelegate = inputsWatchDelegate; + // Maintain references to the interceptor/intercepted + fn.$$intercepted = parsedExpression; + fn.$$interceptor = interceptorFn; + + // Propagate the literal/oneTime/constant attributes + fn.literal = parsedExpression.literal; + fn.oneTime = parsedExpression.oneTime; + fn.constant = parsedExpression.constant; + + // Treat the interceptor like filters. + // If it is not $stateful then only watch its inputs. + // If the expression itself has no inputs then use the full expression as an input. + if (!interceptorFn.$stateful) { useInputs = !parsedExpression.inputs; fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; + + if (!interceptorFn.$$pure) { + fn.inputs = fn.inputs.map(function(e) { + // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a + // non-pure interceptor function. + if (e.isPure === PURITY_RELATIVE) { + return function depurifier(s) { return e(s); }; + } + return e; + }); + } } - return fn; + return addWatchDelegate(fn); } }]; } @@ -14809,13 +17725,13 @@ function $ParseProvider() { * 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). + * This is a [Promises/A+](https://promisesaplus.com/)-compliant 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 (ES2015) promises to some degree. * - * # $q constructor + * ## $q constructor * * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` * function as the first argument. This is similar to the native Promise implementation from ES6, @@ -14853,7 +17769,7 @@ function $ParseProvider() { * * Note: progress/notify callbacks are not currently supported via the ES6-style interface. * - * Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise. + * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise. * * However, the more traditional CommonJS-style usage is still available, and documented below. * @@ -14903,7 +17819,7 @@ function $ParseProvider() { * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * - * # The Deferred API + * ## The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. * @@ -14925,7 +17841,7 @@ function $ParseProvider() { * - promise – `{Promise}` – promise object associated with this deferred. * * - * # The Promise API + * ## The Promise API * * A new promise instance is created when a deferred instance is created and can be retrieved by * calling `deferred.promise`. @@ -14935,7 +17851,7 @@ function $ParseProvider() { * * **Methods** * - * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or + * - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously * as soon as the result is available. The callbacks are called with a single argument: the result * or rejection reason. Additionally, the notify callback may be called zero or more times to @@ -14946,7 +17862,8 @@ function $ParseProvider() { * with the value which is resolved in that promise using * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)). * It also notifies via the return value of the `notifyCallback` method. The promise cannot be - * resolved or rejected from the notifyCallback method. + * resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback + * arguments are optional. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * @@ -14956,7 +17873,7 @@ function $ParseProvider() { * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for * more information. * - * # Chaining promises + * ## Chaining promises * * Because calling the `then` method of a promise returns a new derived promise, it is easily * possible to create a chain of promises: @@ -14976,17 +17893,17 @@ function $ParseProvider() { * $http's response interceptors. * * - * # Differences between Kris Kowal's Q and $q + * ## Differences between Kris Kowal's Q and $q * * There are two main differences: * * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation - * mechanism in angular, which means faster propagation of resolution or rejection into your + * mechanism in AngularJS, which means faster propagation of resolution or rejection into your * models and avoiding unnecessary browser repaints, which would result in flickering UI. * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains * all the important functionality needed for common async tasks. * - * # Testing + * ## Testing * * ```js * it('should simulate promise', inject(function($q, $rootScope) { @@ -15016,21 +17933,61 @@ function $ParseProvider() { * * @returns {Promise} The newly created promise. */ +/** + * @ngdoc provider + * @name $qProvider + * @this + * + * @description + */ function $QProvider() { - + var errorOnUnhandledRejections = true; this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + /** + * @ngdoc method + * @name $qProvider#errorOnUnhandledRejections + * @kind function + * + * @description + * Retrieves or overrides whether to generate an error when a rejected promise is not handled. + * This feature is enabled by default. + * + * @param {boolean=} value Whether to generate an error when a rejected promise is not handled. + * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for + * chaining otherwise. + */ + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } +/** @this */ function $$QProvider() { + var errorOnUnhandledRejections = true; this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { return qFactory(function(callback) { $browser.defer(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } /** @@ -15039,22 +17996,14 @@ function $$QProvider() { * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. + * @param {boolean=} errorOnUnhandledRejections Whether an error should be generated on unhandled + * promises rejections. * @returns {object} Promise manager. */ -function qFactory(nextTick, exceptionHandler) { +function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { var $qMinErr = minErr('$q', TypeError); - function callOnce(self, resolveFn, rejectFn) { - var called = false; - function wrap(fn) { - return function(value) { - if (called) return; - called = true; - fn.call(self, value); - }; - } - - return [wrap(resolveFn), wrap(rejectFn)]; - } + var queueSize = 0; + var checkQueue = []; /** * @ngdoc method @@ -15066,9 +18015,18 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { + function defer() { return new Deferred(); - }; + } + + function Deferred() { + var promise = this.promise = new Promise(); + //Non prototype methods necessary to support unbound execution :/ + this.resolve = function(val) { resolvePromise(promise, val); }; + this.reject = function(reason) { rejectPromise(promise, reason); }; + this.notify = function(progress) { notifyPromise(promise, progress); }; + } + function Promise() { this.$$state = { status: 0 }; @@ -15079,137 +18037,166 @@ function qFactory(nextTick, exceptionHandler) { if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { return this; } - var result = new Deferred(); + var result = new Promise(); this.$$state.pending = this.$$state.pending || []; this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); - return result.promise; + return result; }, - "catch": function(callback) { + 'catch': function(callback) { return this.then(null, callback); }, - "finally": function(callback, progressBack) { + 'finally': function(callback, progressBack) { return this.then(function(value) { - return handleCallback(value, true, callback); + return handleCallback(value, resolve, callback); }, function(error) { - return handleCallback(error, false, callback); + return handleCallback(error, reject, callback); }, progressBack); } }); - //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native - function simpleBind(context, fn) { - return function(value) { - fn.call(context, value); - }; - } - function processQueue(state) { - var fn, deferred, pending; + var fn, promise, pending; pending = state.pending; state.processScheduled = false; state.pending = undefined; - for (var i = 0, ii = pending.length; i < ii; ++i) { - deferred = pending[i][0]; - fn = pending[i][state.status]; - try { - if (isFunction(fn)) { - deferred.resolve(fn(state.value)); - } else if (state.status === 1) { - deferred.resolve(state.value); + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + markQStateExceptionHandled(state); + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + resolvePromise(promise, fn(state.value)); + } else if (state.status === 1) { + resolvePromise(promise, state.value); + } else { + rejectPromise(promise, state.value); + } + } catch (e) { + rejectPromise(promise, e); + // This error is explicitly marked for being passed to the $exceptionHandler + if (e && e.$$passToExceptionHandler === true) { + exceptionHandler(e); + } + } + } + } finally { + --queueSize; + if (errorOnUnhandledRejections && queueSize === 0) { + nextTick(processChecks); + } + } + } + + function processChecks() { + // eslint-disable-next-line no-unmodified-loop-condition + while (!queueSize && checkQueue.length) { + var toCheck = checkQueue.shift(); + if (!isStateExceptionHandled(toCheck)) { + markQStateExceptionHandled(toCheck); + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (isError(toCheck.value)) { + exceptionHandler(toCheck.value, errorMessage); } else { - deferred.reject(state.value); + exceptionHandler(errorMessage); } - } catch (e) { - deferred.reject(e); - exceptionHandler(e); } } } function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) { + if (queueSize === 0 && checkQueue.length === 0) { + nextTick(processChecks); + } + checkQueue.push(state); + } if (state.processScheduled || !state.pending) return; state.processScheduled = true; + ++queueSize; nextTick(function() { processQueue(state); }); } - function Deferred() { - this.promise = new Promise(); - //Necessary to support unbound execution :/ - this.resolve = simpleBind(this, this.resolve); - this.reject = simpleBind(this, this.reject); - this.notify = simpleBind(this, this.notify); + function resolvePromise(promise, val) { + if (promise.$$state.status) return; + if (val === promise) { + $$reject(promise, $qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + $$resolve(promise, val); + } + } - extend(Deferred.prototype, { - resolve: function(val) { - if (this.promise.$$state.status) return; - if (val === this.promise) { - this.$$reject($qMinErr( - 'qcycle', - "Expected promise to be resolved with value other than itself '{0}'", - val)); + function $$resolve(promise, val) { + var then; + var done = false; + try { + if (isObject(val) || isFunction(val)) then = val.then; + if (isFunction(then)) { + promise.$$state.status = -1; + then.call(val, doResolve, doReject, doNotify); } else { - this.$$resolve(val); + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); } + } catch (e) { + doReject(e); + } - }, - - $$resolve: function(val) { - var then, fns; - - fns = callOnce(this, this.$$resolve, this.$$reject); - try { - if ((isObject(val) || isFunction(val))) then = val && val.then; - if (isFunction(then)) { - this.promise.$$state.status = -1; - then.call(val, fns[0], fns[1], this.notify); - } else { - this.promise.$$state.value = val; - this.promise.$$state.status = 1; - scheduleProcessQueue(this.promise.$$state); - } - } catch (e) { - fns[1](e); - exceptionHandler(e); - } - }, + function doResolve(val) { + if (done) return; + done = true; + $$resolve(promise, val); + } + function doReject(val) { + if (done) return; + done = true; + $$reject(promise, val); + } + function doNotify(progress) { + notifyPromise(promise, progress); + } + } - reject: function(reason) { - if (this.promise.$$state.status) return; - this.$$reject(reason); - }, + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } - $$reject: function(reason) { - this.promise.$$state.value = reason; - this.promise.$$state.status = 2; - scheduleProcessQueue(this.promise.$$state); - }, + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } - notify: function(progress) { - var callbacks = this.promise.$$state.pending; + function notifyPromise(promise, progress) { + var callbacks = promise.$$state.pending; - if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { - nextTick(function() { - var callback, result; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - result = callbacks[i][0]; - callback = callbacks[i][3]; - try { - result.notify(isFunction(callback) ? callback(progress) : progress); - } catch (e) { - exceptionHandler(e); - } + if ((promise.$$state.status <= 0) && callbacks && callbacks.length) { + nextTick(function() { + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + notifyPromise(result, isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); } - }); - } + } + }); } - }); + } /** * @ngdoc method @@ -15247,39 +18234,27 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - var reject = function(reason) { - var result = new Deferred(); - result.reject(reason); - return result.promise; - }; - - var makePromise = function makePromise(value, resolved) { - var result = new Deferred(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - }; + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } - var handleCallback = function handleCallback(value, isResolved, callback) { + function handleCallback(value, resolver, callback) { var callbackOutput = null; try { if (isFunction(callback)) callbackOutput = callback(); } catch (e) { - return makePromise(e, false); + return reject(e); } if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); + return resolver(value); + }, reject); } else { - return makePromise(value, isResolved); + return resolver(value); } - }; + } /** * @ngdoc method @@ -15299,11 +18274,11 @@ function qFactory(nextTick, exceptionHandler) { */ - var when = function(value, callback, errback, progressBack) { - var result = new Deferred(); - result.resolve(value); - return result.promise.then(callback, errback, progressBack); - }; + function when(value, callback, errback, progressBack) { + var result = new Promise(); + resolvePromise(result, value); + return result.then(callback, errback, progressBack); + } /** * @ngdoc method @@ -15338,63 +18313,102 @@ function qFactory(nextTick, exceptionHandler) { */ function all(promises) { - var deferred = new Deferred(), + var result = new Promise(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; when(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; results[key] = value; - if (!(--counter)) deferred.resolve(results); + if (!(--counter)) resolvePromise(result, results); }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); + rejectPromise(result, reason); }); }); if (counter === 0) { - deferred.resolve(results); + resolvePromise(result, results); } + return result; + } + + /** + * @ngdoc method + * @name $q#race + * @kind function + * + * @description + * Returns a promise that resolves or rejects as soon as one of those promises + * resolves or rejects, with the value or reason from that promise. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} a promise that resolves or rejects as soon as one of the `promises` + * resolves or rejects, with the value or reason from that promise. + */ + + function race(promises) { + var deferred = defer(); + + forEach(promises, function(promise) { + when(promise).then(deferred.resolve, deferred.reject); + }); + return deferred.promise; } - var $Q = function Q(resolver) { + function $Q(resolver) { if (!isFunction(resolver)) { - throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); - } - - if (!(this instanceof Q)) { - // More useful when $Q is the Promise itself. - return new Q(resolver); + throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); } - var deferred = new Deferred(); + var promise = new Promise(); function resolveFn(value) { - deferred.resolve(value); + resolvePromise(promise, value); } function rejectFn(reason) { - deferred.reject(reason); + rejectPromise(promise, reason); } resolver(resolveFn, rejectFn); - return deferred.promise; - }; + return promise; + } + + // Let's make the instanceof operator work for promises, so that + // `new $q(fn) instanceof $q` would evaluate to true. + $Q.prototype = Promise.prototype; $Q.defer = defer; $Q.reject = reject; $Q.when = when; $Q.resolve = resolve; $Q.all = all; + $Q.race = race; return $Q; } +function isStateExceptionHandled(state) { + return !!state.pur; +} +function markQStateExceptionHandled(state) { + state.pur = true; +} +function markQExceptionHandled(q) { + // Built-in `$q` promises will always have a `$$state` property. This check is to allow + // overwriting `$q` with a different promise library (e.g. Bluebird + angular-bluebird-promises). + // (Currently, this is the only method that might be called with a promise, even if it is not + // created by the built-in `$q`.) + if (q.$$state) { + markQStateExceptionHandled(q.$$state); + } +} + +/** @this */ function $$RAFProvider() { //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || @@ -15484,6 +18498,8 @@ function $$RAFProvider() { //rAF /** * @ngdoc service * @name $rootScope + * @this + * * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. @@ -15514,13 +18530,14 @@ function $RootScopeProvider() { this.$$watchersCount = 0; this.$id = nextUid(); this.$$ChildScope = null; + this.$$suspended = false; } ChildScope.prototype = parent; return ChildScope; } - this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function($injector, $exceptionHandler, $parse, $browser) { + this.$get = ['$exceptionHandler', '$parse', '$browser', + function($exceptionHandler, $parse, $browser) { function destroyChildScope($event) { $event.currentScope.$$destroyed = true; @@ -15528,14 +18545,19 @@ function $RootScopeProvider() { function cleanUpScope($scope) { + // Support: IE 9 only if (msie === 9) { // There is a memory leak in IE9 if all child scopes are not disconnected // completely when a scope is destroyed. So this code will recurse up through // all this scopes children // // See issue https://github.com/angular/angular.js/issues/10706 - $scope.$$childHead && cleanUpScope($scope.$$childHead); - $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling); + if ($scope.$$childHead) { + cleanUpScope($scope.$$childHead); + } + if ($scope.$$nextSibling) { + cleanUpScope($scope.$$nextSibling); + } } // The code below works around IE9 and V8's memory leaks @@ -15561,7 +18583,7 @@ function $RootScopeProvider() { * an in-depth introduction and usage examples. * * - * # Inheritance + * ## Inheritance * A scope can inherit from a parent scope, as in this example: * ```js var parent = $rootScope; @@ -15596,6 +18618,7 @@ function $RootScopeProvider() { this.$$childHead = this.$$childTail = null; this.$root = this; this.$$destroyed = false; + this.$$suspended = false; this.$$listeners = {}; this.$$listenerCount = {}; this.$$watchersCount = 0; @@ -15687,7 +18710,7 @@ function $RootScopeProvider() { // 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', destroyChildScope); + if (isolate || parent !== this) child.$on('$destroy', destroyChildScope); return child; }, @@ -15704,7 +18727,7 @@ function $RootScopeProvider() { * $digest()} and should return the value that will be watched. (`watchExpression` should not change * its value when executed multiple times with the same input because it may be executed multiple * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be - * [idempotent](http://en.wikipedia.org/wiki/Idempotence). + * [idempotent](http://en.wikipedia.org/wiki/Idempotence).) * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, * see below). Inequality is determined according to reference inequality, @@ -15715,6 +18738,8 @@ function $RootScopeProvider() { * according to the {@link angular.equals} function. To save the value of the object for * later comparison, the {@link angular.copy} function is used. This therefore means that * watching complex objects will have adverse memory and performance implications. + * - This should not be used to watch for changes in objects that are (or contain) + * [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. @@ -15734,7 +18759,7 @@ function $RootScopeProvider() { * * * - * # Example + * @example * ```js // let's assume that scope was dependency injected as the $rootScope var scope = $rootScope; @@ -15804,20 +18829,21 @@ function $RootScopeProvider() { * - `newVal` contains the current value of the `watchExpression` * - `oldVal` contains the previous value of the `watchExpression` * - `scope` refers to the current scope - * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of + * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { var get = $parse(watchExp); + var fn = isFunction(listener) ? listener : noop; if (get.$$watchDelegate) { - return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); + return get.$$watchDelegate(this, fn, objectEquality, get, watchExp); } var scope = this, array = scope.$$watchers, watcher = { - fn: listener, + fn: fn, last: initWatchVal, get: get, exp: prettyPrintExpression || watchExp, @@ -15826,21 +18852,23 @@ function $RootScopeProvider() { lastDirtyWatch = null; - if (!isFunction(listener)) { - watcher.fn = noop; - } - if (!array) { array = scope.$$watchers = []; + array.$$digestWatchIndex = -1; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); + array.$$digestWatchIndex++; incrementWatchersCount(this, 1); return function deregisterWatch() { - if (arrayRemove(array, watcher) >= 0) { + var index = arrayRemove(array, watcher); + if (index >= 0) { incrementWatchersCount(scope, -1); + if (index < array.$$digestWatchIndex) { + array.$$digestWatchIndex--; + } } lastDirtyWatch = null; }; @@ -15855,8 +18883,8 @@ function $RootScopeProvider() { * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. * If any one expression in the collection changes the `listener` is executed. * - * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every - * call to $digest() to see if any items changes. + * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return + * values are examined for changes on every call to `$digest`. * - The `listener` is called whenever any expression in the `watchExpressions` array changes. * * @param {Array.} watchExpressions Array of expressions that will be individually @@ -15900,9 +18928,8 @@ function $RootScopeProvider() { } forEach(watchExpressions, function(expr, i) { - var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + var unwatchFn = self.$watch(expr, function watchGroupSubAction(value) { newValues[i] = value; - oldValues[i] = oldValue; if (!changeReactionScheduled) { changeReactionScheduled = true; self.$evalAsync(watchGroupAction); @@ -15914,11 +18941,17 @@ function $RootScopeProvider() { function watchGroupAction() { changeReactionScheduled = false; - if (firstRun) { - firstRun = false; - listener(newValues, newValues, self); - } else { - listener(newValues, oldValues, self); + try { + if (firstRun) { + firstRun = false; + listener(newValues, newValues, self); + } else { + listener(newValues, oldValues, self); + } + } finally { + for (var i = 0; i < watchExpressions.length; i++) { + oldValues[i] = newValues[i]; + } } } @@ -15946,7 +18979,7 @@ function $RootScopeProvider() { * adding, removing, and moving items belonging to an object or array. * * - * # Example + * @example * ```js $scope.names = ['igor', 'matias', 'misko', 'james']; $scope.dataCount = 4; @@ -15986,7 +19019,11 @@ function $RootScopeProvider() { * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { - $watchCollectionInterceptor.$stateful = true; + // Mark the interceptor as + // ... $$pure when literal since the instance will change when any input changes + $watchCollectionInterceptor.$$pure = $parse(obj).literal; + // ... $stateful when non-literal since we must read the state of the collection + $watchCollectionInterceptor.$stateful = !$watchCollectionInterceptor.$$pure; var self = this; // the current value, updated on each dirty-check run @@ -16037,6 +19074,7 @@ function $RootScopeProvider() { oldItem = oldValue[i]; newItem = newValue[i]; + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -16059,6 +19097,7 @@ function $RootScopeProvider() { oldItem = oldValue[key]; if (key in oldValue) { + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -16142,7 +19181,7 @@ function $RootScopeProvider() { * * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * - * # Example + * @example * ```js var scope = ...; scope.name = 'misko'; @@ -16171,11 +19210,10 @@ function $RootScopeProvider() { $digest: function() { var watch, value, last, fn, get, watchers, - length, dirty, ttl = TTL, - next, current, target = this, + next, current, target = asyncQueue.length ? $rootScope : this, watchLog = [], - logIdx, logMsg, asyncTask; + logIdx, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest @@ -16194,24 +19232,29 @@ function $RootScopeProvider() { dirty = false; current = target; - while (asyncQueue.length) { + // It's safe for asyncQueuePosition to be a local variable here because this loop can't + // be reentered recursively. Calling $digest from a function passed to $evalAsync would + // lead to a '$digest already in progress' error. + for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { try { - asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); + asyncTask = asyncQueue[asyncQueuePosition]; + fn = asyncTask.fn; + fn(asyncTask.scope, asyncTask.locals); } catch (e) { $exceptionHandler(e); } lastDirtyWatch = null; } + asyncQueue.length = 0; traverseScopesLoop: do { // "traverse the scopes" loop - if ((watchers = current.$$watchers)) { + if ((watchers = !current.$$suspended && current.$$watchers)) { // process our watches - length = watchers.length; - while (length--) { + watchers.$$digestWatchIndex = watchers.length; + while (watchers.$$digestWatchIndex--) { try { - watch = watchers[length]; + watch = watchers[watchers.$$digestWatchIndex]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { @@ -16219,8 +19262,7 @@ function $RootScopeProvider() { if ((value = get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) - : (typeof value === 'number' && typeof last === 'number' - && isNaN(value) && isNaN(last)))) { + : (isNumberNaN(value) && isNumberNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; @@ -16251,7 +19293,9 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast - if (!(next = ((current.$$watchersCount && current.$$childHead) || + // (though it differs due to having the extra check for $$suspended and does not + // check $$listenerCount) + if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; @@ -16273,15 +19317,110 @@ function $RootScopeProvider() { clearPhase(); - while (postDigestQueue.length) { + // postDigestQueuePosition isn't local here because this loop can be reentered recursively. + while (postDigestQueuePosition < postDigestQueue.length) { try { - postDigestQueue.shift()(); + postDigestQueue[postDigestQueuePosition++](); } catch (e) { $exceptionHandler(e); } } + postDigestQueue.length = postDigestQueuePosition = 0; + + // Check for changes to browser url that happened during the $digest + // (for which no event is fired; e.g. via `history.pushState()`) + $browser.$$checkUrlChange(); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$suspend + * @kind function + * + * @description + * Suspend watchers of this scope subtree so that they will not be invoked during digest. + * + * This can be used to optimize your application when you know that running those watchers + * is redundant. + * + * **Warning** + * + * Suspending scopes from the digest cycle can have unwanted and difficult to debug results. + * Only use this approach if you are confident that you know what you are doing and have + * ample tests to ensure that bindings get updated as you expect. + * + * Some of the things to consider are: + * + * * Any external event on a directive/component will not trigger a digest while the hosting + * scope is suspended - even if the event handler calls `$apply()` or `$rootScope.$digest()`. + * * Transcluded content exists on a scope that inherits from outside a directive but exists + * as a child of the directive's containing scope. If the containing scope is suspended the + * transcluded scope will also be suspended, even if the scope from which the transcluded + * scope inherits is not suspended. + * * Multiple directives trying to manage the suspended status of a scope can confuse each other: + * * A call to `$suspend()` on an already suspended scope is a no-op. + * * A call to `$resume()` on a non-suspended scope is a no-op. + * * If two directives suspend a scope, then one of them resumes the scope, the scope will no + * longer be suspended. This could result in the other directive believing a scope to be + * suspended when it is not. + * * If a parent scope is suspended then all its descendants will be also excluded from future + * digests whether or not they have been suspended themselves. Note that this also applies to + * isolate child scopes. + * * Calling `$digest()` directly on a descendant of a suspended scope will still run the watchers + * for that scope and its descendants. When digesting we only check whether the current scope is + * locally suspended, rather than checking whether it has a suspended ancestor. + * * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be + * included in future digests until all its ancestors have been resumed. + * * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply()` + * against the `$rootScope` and so will still trigger a global digest even if the promise was + * initiated by a component that lives on a suspended scope. + */ + $suspend: function() { + this.$$suspended = true; + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$isSuspended + * @kind function + * + * @description + * Call this method to determine if this scope has been explicitly suspended. It will not + * tell you whether an ancestor has been suspended. + * To determine if this scope will be excluded from a digest triggered at the $rootScope, + * for example, you must check all its ancestors: + * + * ``` + * function isExcludedFromDigest(scope) { + * while(scope) { + * if (scope.$isSuspended()) return true; + * scope = scope.$parent; + * } + * return false; + * ``` + * + * Be aware that a scope may not be included in digests if it has a suspended ancestor, + * even if `$isSuspended()` returns false. + * + * @returns true if the current scope has been suspended. + */ + $isSuspended: function() { + return this.$$suspended; }, + /** + * @ngdoc method + * @name $rootScope.Scope#$resume + * @kind function + * + * @description + * Resume watchers of this scope subtree in case it was suspended. + * + * See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach. + */ + $resume: function() { + this.$$suspended = false; + }, /** * @ngdoc event @@ -16337,8 +19476,8 @@ function $RootScopeProvider() { // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) - if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + if (parent && parent.$$childHead === this) parent.$$childHead = this.$$nextSibling; + if (parent && parent.$$childTail === this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; @@ -16359,10 +19498,10 @@ function $RootScopeProvider() { * * @description * Executes the `expression` on the current scope and returns the result. Any exceptions in - * the expression are propagated (uncaught). This is useful when evaluating Angular + * the expression are propagated (uncaught). This is useful when evaluating AngularJS * expressions. * - * # Example + * @example * ```js var scope = ng.$rootScope.Scope(); scope.a = 1; @@ -16372,7 +19511,7 @@ function $RootScopeProvider() { expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); * ``` * - * @param {(string|function())=} expression An angular expression to be executed. + * @param {(string|function())=} expression An AngularJS expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. @@ -16407,7 +19546,7 @@ function $RootScopeProvider() { * will be scheduled. However, it is encouraged to always call code that changes the model * from within an `$apply` call. That includes code evaluated via `$evalAsync`. * - * @param {(string|function())=} expression An angular expression to be executed. + * @param {(string|function())=} expression An AngularJS expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. @@ -16422,10 +19561,10 @@ function $RootScopeProvider() { if (asyncQueue.length) { $rootScope.$digest(); } - }); + }, null, '$evalAsync'); } - asyncQueue.push({scope: this, expression: $parse(expr), locals: locals}); + asyncQueue.push({scope: this, fn: $parse(expr), locals: locals}); }, $$postDigest: function(fn) { @@ -16438,15 +19577,14 @@ function $RootScopeProvider() { * @kind function * * @description - * `$apply()` is used to execute an expression in angular from outside of the angular + * `$apply()` is used to execute an expression in AngularJS from outside of the AngularJS * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). - * Because we are calling into the angular framework we need to perform proper scope life + * Because we are calling into the AngularJS framework we need to perform proper scope life * cycle of {@link ng.$exceptionHandler exception handling}, * {@link ng.$rootScope.Scope#$digest executing watches}. * - * ## Life cycle + * **Life cycle: Pseudo-Code of `$apply()`** * - * # Pseudo-Code of `$apply()` * ```js function $apply(expr) { try { @@ -16470,7 +19608,7 @@ function $RootScopeProvider() { * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. * * - * @param {(string|function())=} exp An angular expression to be executed. + * @param {(string|function())=} exp An AngularJS expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with current `scope` parameter. @@ -16492,6 +19630,7 @@ function $RootScopeProvider() { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); + // eslint-disable-next-line no-unsafe-finally throw e; } } @@ -16509,14 +19648,16 @@ function $RootScopeProvider() { * This can be used to queue up multiple expressions which need to be evaluated in the same * digest. * - * @param {(string|function())=} exp An angular expression to be executed. + * @param {(string|function())=} exp An AngularJS expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with current `scope` parameter. */ $applyAsync: function(expr) { var scope = this; - expr && applyAsyncQueue.push($applyAsyncExpression); + if (expr) { + applyAsyncQueue.push($applyAsyncExpression); + } expr = $parse(expr); scheduleApplyAsync(); @@ -16571,7 +19712,10 @@ function $RootScopeProvider() { return function() { var indexOfListener = namedListeners.indexOf(listener); if (indexOfListener !== -1) { - namedListeners[indexOfListener] = null; + // Use delete in the hope of the browser deallocating the memory for the array entry, + // while not shifting the array indexes of other listeners. + // See issue https://github.com/angular/angular.js/issues/16135 + delete namedListeners[indexOfListener]; decrementListenerCount(self, 1, name); } }; @@ -16638,8 +19782,7 @@ function $RootScopeProvider() { } //if any listener on the current scope stops propagation, prevent bubbling if (stopPropagation) { - event.currentScope = null; - return event; + break; } //traverse upwards scope = scope.$parent; @@ -16713,7 +19856,8 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $digest - // (though it differs due to having the extra check for $$listenerCount) + // (though it differs due to having the extra check for $$listenerCount and + // does not check $$suspended) if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { @@ -16734,6 +19878,8 @@ function $RootScopeProvider() { var postDigestQueue = $rootScope.$$postDigestQueue = []; var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + var postDigestQueuePosition = 0; + return $rootScope; @@ -16786,7 +19932,7 @@ function $RootScopeProvider() { if (applyAsyncId === null) { applyAsyncId = $browser.defer(function() { $rootScope.$apply(flushApplyAsync); - }); + }, null, '$applyAsync'); } } }]; @@ -16797,7 +19943,7 @@ function $RootScopeProvider() { * @name $rootElement * * @description - * The root element of Angular application. This is either the element where {@link + * The root element of AngularJS application. This is either the element where {@link * ng.directive:ngApp ngApp} was declared or the element passed into * {@link angular.bootstrap}. The element represents the root element of application. It is also the * location where the application's {@link auto.$injector $injector} service gets @@ -16808,67 +19954,79 @@ function $RootScopeProvider() { // the implementation is in angular.bootstrap /** + * @this * @description * Private service to sanitize uris for links and images. Used by $compile and $sanitize. */ function $$SanitizeUriProvider() { - var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; + + var aHrefSanitizationTrustedUrlList = /^\s*(https?|s?ftp|mailto|tel|file):/, + imgSrcSanitizationTrustedUrlList = /^\s*((https?|ftp|file|blob):|data:image\/)/; /** * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted safe * urls during a[href] sanitization. * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * The sanitization is a security measure aimed at prevent XSS attacks via HTML anchor links. * - * Any url about to be assigned to a[href] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring + * the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)` + * which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL. + * + * If the URL matches the `aHrefSanitizationTrustedUrlList` regular expression, it is returned unchanged. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written + * to the DOM it is inactive and potentially malicious code will not be executed. + * + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.aHrefSanitizationWhitelist = function(regexp) { + this.aHrefSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - aHrefSanitizationWhitelist = regexp; + aHrefSanitizationTrustedUrlList = regexp; return this; } - return aHrefSanitizationWhitelist; + return aHrefSanitizationTrustedUrlList; }; /** * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted safe * urls during img[src] sanitization. * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links. * - * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * Any URL due to be assigned to an `img[src]` attribute via interpolation is marked as requiring + * the $sce.MEDIA_URL security context. When interpolation occurs a call is made to + * `$sce.trustAsMediaUrl(url)` which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize + * the potentially malicious URL. + * + * If the URL matches the `imgSrcSanitizationTrustedUrlList` regular expression, it is returned + * unchanged. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written + * to the DOM it is inactive and potentially malicious code will not be executed. + * + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.imgSrcSanitizationWhitelist = function(regexp) { + this.imgSrcSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - imgSrcSanitizationWhitelist = regexp; + imgSrcSanitizationTrustedUrlList = regexp; return this; } - return imgSrcSanitizationWhitelist; + return imgSrcSanitizationTrustedUrlList; }; this.$get = function() { - return function sanitizeUri(uri, isImage) { - var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; - var normalizedVal; - normalizedVal = urlResolve(uri).href; + return function sanitizeUri(uri, isMediaUrl) { + // if (!uri) return uri; + var regex = isMediaUrl ? imgSrcSanitizationTrustedUrlList : aHrefSanitizationTrustedUrlList; + var normalizedVal = urlResolve(uri && uri.trim()).href; if (normalizedVal !== '' && !normalizedVal.match(regex)) { return 'unsafe:' + normalizedVal; } @@ -16888,20 +20046,43 @@ function $$SanitizeUriProvider() { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* exported $SceProvider, $SceDelegateProvider */ + var $sceMinErr = minErr('$sce'); var SCE_CONTEXTS = { + // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding). HTML: 'html', + + // Style statements or stylesheets. Currently unused in AngularJS. CSS: 'css', + + // An URL used in a context where it refers to the source of media, which are not expected to be run + // as scripts, such as an image, audio, video, etc. + MEDIA_URL: 'mediaUrl', + + // An URL used in a context where it does not refer to a resource that loads code. + // A value that can be trusted as a URL can also trusted as a MEDIA_URL. URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) + + // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as + // code. (e.g. ng-include, script src binding, templateUrl) + // A value that can be trusted as a RESOURCE_URL, can also trusted as a URL and a MEDIA_URL. RESOURCE_URL: 'resourceUrl', + + // Script. Currently unused in AngularJS. JS: 'js' }; // Helper functions follow. +var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + +function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); +} + function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -16915,8 +20096,8 @@ function adjustMatcher(matcher) { 'Illegal sequence *** in string matcher. String: {0}', matcher); } matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. @@ -16951,6 +20132,16 @@ function adjustMatchers(matchers) { * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict * Contextual Escaping (SCE)} services to AngularJS. * + * For an overview of this service and the functionnality it provides in AngularJS, see the main + * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how + * SCE works in their application, which shouldn't be needed in most cases. + * + *
+ * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or + * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, + * changes to this service will also influence users, so be extra careful and document your changes. + *
+ * * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to @@ -16962,88 +20153,154 @@ function adjustMatchers(matchers) { * The default instance of `$sceDelegate` should work out of the box with little pain. While you * can override it completely to change the behavior of `$sce`, the common case would * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting - * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as - * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist - * $sceDelegateProvider.resourceUrlWhitelist} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * your own trusted and banned resource lists for trusting URLs used for loading AngularJS resources + * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList + * $sceDelegateProvider.trustedResourceUrlList} and {@link + * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList} */ /** * @ngdoc provider * @name $sceDelegateProvider + * @this + * * @description * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that the URLs used for sourcing Angular templates are safe. Refer {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. + * + * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and + * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularJS templates and + * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See + * {@link ng.$sceDelegateProvider#trustedResourceUrlList + * $sceDelegateProvider.trustedResourceUrlList} and + * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}, * - * For the general details about this service in Angular, read the main page for {@link ng.$sce + * For the general details about this service in AngularJS, read the main page for {@link ng.$sce * Strict Contextual Escaping (SCE)}. * * **Example**: Consider the following case.
* * - your app is hosted at url `http://myapp.example.com/` * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. + * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. * * Here is what a secure configuration for this scenario might look like: * * ``` * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.resourceUrlWhitelist([ + * $sceDelegateProvider.trustedResourceUrlList([ * // Allow same origin resource loads. * 'self', * // Allow loading from our assets domain. Notice the difference between * and **. * 'http://srv*.assets.example.com/**' * ]); * - * // The blacklist overrides the whitelist so the open redirect here is blocked. - * $sceDelegateProvider.resourceUrlBlacklist([ + * // The banned resource URL list overrides the trusted resource URL list so the open redirect + * // here is blocked. + * $sceDelegateProvider.bannedResourceUrlList([ * 'http://myapp.example.com/clickThru**' * ]); * }); * ``` + * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require + * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates + * requested by {@link ng.$templateRequest $templateRequest} that are present in + * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism + * to populate your templates in that cache at config time, then it is a good idea to remove 'self' + * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types + * of issues, like for instance attacker-controlled `ng-includes`. */ function $SceDelegateProvider() { this.SCE_CONTEXTS = SCE_CONTEXTS; // Resource URLs can also be trusted by policy. - var resourceUrlWhitelist = ['self'], - resourceUrlBlacklist = []; + var trustedResourceUrlList = ['self'], + bannedResourceUrlList = []; /** * @ngdoc method - * @name $sceDelegateProvider#resourceUrlWhitelist + * @name $sceDelegateProvider#trustedResourceUrlList * @kind function * - * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. + * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with + * the value provided. This must be an array or null. A snapshot of this array is used so + * further changes to the array are ignored. + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. + * @return {Array} The currently set trusted resource URL array. * - *
- * **Note:** an empty whitelist array will block all URLs! - *
+ * @description + * Sets/Gets the list trusted of resource URLs. + * + * The **default value** when no `trustedResourceUrlList` has been explicitly set is `['self']` + * allowing only same origin resource requests. + * + *
+ * **Note:** the default `trustedResourceUrlList` of 'self' is not recommended if your app shares + * its origin with other apps! It is a good idea to limit it to only your application's directory. + *
+ */ + this.trustedResourceUrlList = function(value) { + if (arguments.length) { + trustedResourceUrlList = adjustMatchers(value); + } + return trustedResourceUrlList; + }; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlWhitelist + * @kind function + * + * @deprecated + * sinceVersion="1.8.1" + * + * This method is deprecated. Use {@link $sceDelegateProvider#trustedResourceUrlList + * trustedResourceUrlList} instead. + */ + Object.defineProperty(this, 'resourceUrlWhitelist', { + get: function() { + return this.trustedResourceUrlList; + }, + set: function(value) { + this.trustedResourceUrlList = value; + } + }); + + /** + * @ngdoc method + * @name $sceDelegateProvider#bannedResourceUrlList + * @kind function * - * @return {Array} the currently set whitelist array. + * @param {Array=} bannedResourceUrlList When provided, replaces the `bannedResourceUrlList` with + * the value provided. This must be an array or null. A snapshot of this array is used so + * further changes to the array are ignored.

+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array.

+ * The typical usage for the `bannedResourceUrlList` is to **block + * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as + * these would otherwise be trusted but actually return content from the redirected domain. + *

+ * Finally, **the banned resource URL list overrides the trusted resource URL list** and has + * the final say. * - * The **default value** when no whitelist has been explicitly set is `['self']` allowing only - * same origin resource requests. + * @return {Array} The currently set `bannedResourceUrlList` array. * * @description - * Sets/Gets the whitelist of trusted resource URLs. + * Sets/Gets the `bannedResourceUrlList` of trusted resource URLs. + * + * The **default value** when no trusted resource URL list has been explicitly set is the empty + * array (i.e. there is no `bannedResourceUrlList`.) */ - this.resourceUrlWhitelist = function(value) { + this.bannedResourceUrlList = function(value) { if (arguments.length) { - resourceUrlWhitelist = adjustMatchers(value); + bannedResourceUrlList = adjustMatchers(value); } - return resourceUrlWhitelist; + return bannedResourceUrlList; }; /** @@ -17051,36 +20308,22 @@ function $SceDelegateProvider() { * @name $sceDelegateProvider#resourceUrlBlacklist * @kind function * - * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * - * The typical usage for the blacklist is to **block - * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as - * these would otherwise be trusted but actually return content from the redirected domain. + * @deprecated + * sinceVersion="1.8.1" * - * Finally, **the blacklist overrides the whitelist** and has the final say. - * - * @return {Array} the currently set blacklist array. - * - * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there - * is no blacklist.) - * - * @description - * Sets/Gets the blacklist of trusted resource URLs. + * This method is deprecated. Use {@link $sceDelegateProvider#bannedResourceUrlList + * bannedResourceUrlList} instead. */ - - this.resourceUrlBlacklist = function(value) { - if (arguments.length) { - resourceUrlBlacklist = adjustMatchers(value); + Object.defineProperty(this, 'resourceUrlBlacklist', { + get: function() { + return this.bannedResourceUrlList; + }, + set: function(value) { + this.bannedResourceUrlList = value; } - return resourceUrlBlacklist; - }; + }); - this.$get = ['$injector', function($injector) { + this.$get = ['$injector', '$$sanitizeUri', function($injector, $$sanitizeUri) { var htmlSanitizer = function htmlSanitizer(html) { throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); @@ -17093,7 +20336,7 @@ function $SceDelegateProvider() { function matchUrl(matcher, parsedUrl) { if (matcher === 'self') { - return urlIsSameOrigin(parsedUrl); + return urlIsSameOrigin(parsedUrl) || urlIsSameOriginAsBaseUrl(parsedUrl); } else { // definitely a regex. See adjustMatchers() return !!matcher.exec(parsedUrl.href); @@ -17103,17 +20346,17 @@ function $SceDelegateProvider() { function isResourceUrlAllowedByPolicy(url) { var parsedUrl = urlResolve(url.toString()); var i, n, allowed = false; - // Ensure that at least one item from the whitelist allows this url. - for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { - if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { + // Ensure that at least one item from the trusted resource URL list allows this url. + for (i = 0, n = trustedResourceUrlList.length; i < n; i++) { + if (matchUrl(trustedResourceUrlList[i], parsedUrl)) { allowed = true; break; } } if (allowed) { - // Ensure that no item from the blacklist blocked this url. - for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { - if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { + // Ensure that no item from the banned resource URL list has blocked this url. + for (i = 0, n = bannedResourceUrlList.length; i < n; i++) { + if (matchUrl(bannedResourceUrlList[i], parsedUrl)) { allowed = false; break; } @@ -17145,7 +20388,8 @@ function $SceDelegateProvider() { byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.MEDIA_URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.URL] = generateHolderType(byType[SCE_CONTEXTS.MEDIA_URL]); byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); @@ -17154,17 +20398,24 @@ function $SceDelegateProvider() { * @name $sceDelegate#trustAs * * @description - * Returns an object that is trusted by angular for use in specified strict - * contextual escaping contexts (such as ng-bind-html, ng-include, any src - * attribute interpolation, any dom event binding attribute interpolation - * such as for onclick, etc.) that uses the provided value. - * See {@link ng.$sce $sce} for enabling strict contextual escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. + * Returns a trusted representation of the parameter for the specified context. This trusted + * object will later on be used as-is, without any security check, by bindings or directives + * that require this security context. + * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass + * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as + * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the + * sanitizer loaded, passing the value itself will render all the HTML that does not pose a + * security risk. + * + * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those + * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual + * escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that should be considered trusted. + * @return {*} A trusted representation of value, that can be used in the given context. */ function trustAs(type, trustedValue) { var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); @@ -17196,11 +20447,11 @@ function $SceDelegateProvider() { * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is. * * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} - * call or anything else. - * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * call or anything else. + * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns * `value` unchanged. */ @@ -17217,28 +20468,56 @@ function $SceDelegateProvider() { * @name $sceDelegate#getTrusted * * @description - * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and - * returns the originally supplied value if the queried context type is a supertype of the - * created type. If this condition isn't satisfied, throws an exception. + * Given an object and a security context in which to assign it, returns a value that's safe to + * use in this context, which was represented by the parameter. To do so, this function either + * unwraps the safe type it has been given (for instance, a {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} result), or it might try to sanitize the value given, depending on + * the context and sanitizer availablility. + * + * The contexts that can be sanitized are $sce.MEDIA_URL, $sce.URL and $sce.HTML. The first two are available + * by default, and the third one relies on the `$sanitize` service (which may be loaded through + * the `ngSanitize` module). Furthermore, for $sce.RESOURCE_URL context, a plain string may be + * accepted if the resource url policy defined by {@link ng.$sceDelegateProvider#trustedResourceUrlList + * `$sceDelegateProvider.trustedResourceUrlList`} and {@link ng.$sceDelegateProvider#bannedResourceUrlList + * `$sceDelegateProvider.bannedResourceUrlList`} accepts that resource. + * + * This function will throw if the safe type isn't appropriate for this context, or if the + * value given cannot be accepted in the context (which might be caused by sanitization not + * being available, or the value not being recognized as safe). * - * @param {string} type The kind of context in which this value is to be used. + *

+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting + * (XSS) vulnerability in your application. + *
+ * + * @param {string} type The context in which this value is to be used (such as `$sce.HTML`). * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ function getTrusted(type, maybeTrusted) { if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return + // as-is. if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. - if (type === SCE_CONTEXTS.RESOURCE_URL) { + + // If maybeTrusted is a trusted class instance but not of the correct trusted type + // then unwrap it and allow it to pass through to the rest of the checks + if (isFunction(maybeTrusted.$$unwrapTrustedValue)) { + maybeTrusted = maybeTrusted.$$unwrapTrustedValue(); + } + + // If we get here, then we will either sanitize the value or throw an exception. + if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) { + // we attempt to sanitize non-resource URLs + return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL); + } else if (type === SCE_CONTEXTS.RESOURCE_URL) { if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { @@ -17247,8 +20526,10 @@ function $SceDelegateProvider() { maybeTrusted.toString()); } } else if (type === SCE_CONTEXTS.HTML) { + // htmlSanitizer throws its own error when no sanitizer is available. return htmlSanitizer(maybeTrusted); } + // Default error when the $sce service has no way to make the input safe. throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); } @@ -17262,6 +20543,8 @@ function $SceDelegateProvider() { /** * @ngdoc provider * @name $sceProvider + * @this + * * @description * * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. @@ -17271,8 +20554,6 @@ function $SceDelegateProvider() { * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. */ -/* jshint maxlen: false*/ - /** * @ngdoc service * @name $sce @@ -17282,23 +20563,29 @@ function $SceDelegateProvider() { * * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. * - * # Strict Contextual Escaping + * ## Strict Contextual Escaping * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. + * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render + * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and + * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. * - * As of version 1.2, Angular ships with SCE enabled by default. + * ### Overview * - * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow - * one to execute arbitrary javascript by the use of the expression() syntax. Refer - * to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `` - * to the top of your HTML document. + * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in + * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically + * run security checks on them (sanitizations, trusted URL resource, depending on context), or throw + * when it cannot guarantee the security of the result. That behavior depends strongly on contexts: + * HTML can be sanitized, but template URLs cannot, for instance. * - * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for - * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML: + * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it + * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and + * render the input as-is, you will need to mark it as trusted for that context before attempting + * to bind it. + * + * As of version 1.2, AngularJS ships with SCE enabled by default. + * + * ### In practice * * Here's an example of a binding in a privileged context: * @@ -17308,10 +20595,10 @@ function $SceDelegateProvider() { * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE - * disabled, this application allows the user to render arbitrary HTML into the DIV. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) + * disabled, this application allows the user to render arbitrary HTML into the DIV, which would + * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog + * articles, etc. via bindings. (HTML is just one example of a context where rendering user + * controlled input creates security vulnerabilities.) * * For the case of HTML, you might use a library, either on the client side, or on the server side, * to sanitize unsafe HTML before binding to the value and rendering it in the document. @@ -17321,25 +20608,29 @@ function $SceDelegateProvider() { * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some * properties/fields and forgot to update the binding to the sanitized value? * - * To be secure by default, you want to ensure that any such bindings are disallowed unless you can - * determine that something explicitly says it's safe to use a value for binding in that - * context. You can then audit your code (a simple grep would do) to ensure that this is only done - * for those values that you can easily tell are safe - because they were received from your server, - * sanitized by your library, etc. You can organize your codebase to help with this - perhaps - * allowing only the files in a specific directory to do this. Ensuring that the internal API - * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * To be secure by default, AngularJS makes sure bindings go through that sanitization, or + * any similar validation process, unless there's a good reason to trust the given value in this + * context. That trust is formalized with a function call. This means that as a developer, you + * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues, + * you just need to ensure the values you mark as trusted indeed are safe - because they were + * received from your server, sanitized by your library, etc. You can organize your codebase to + * help with this - perhaps allowing only the files in a specific directory to do this. + * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then + * becomes a more manageable task. * * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. + * build the trusted versions of your values. * - * - * ## How does it work? + * ### How does it work? * * 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#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. + * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as + * a way to enforce the required security context in your data sink. Directives use {@link + * 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. Also, + * when binding without directives, AngularJS will understand the context of your bindings + * automatically. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly @@ -17355,16 +20646,16 @@ function $SceDelegateProvider() { * }]; * ``` * - * ## Impact on loading templates + * ### Impact on loading templates * * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as * `templateUrl`'s specified by {@link guide/directive directives}. * - * By default, Angular only loads templates from the same domain and protocol as the application + * By default, AngularJS only loads templates from the same domain and protocol as the application * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or - * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist - * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * protocols, you may either add them to the {@link ng.$sceDelegateProvider#trustedResourceUrlList + * trustedResourceUrlList} or {@link ng.$sce#trustAsResourceUrl wrap them} into trusted values. * * *Please note*: * The browser's @@ -17375,40 +20666,58 @@ function $SceDelegateProvider() { * won't work on all browsers. Also, loading templates from `file://` URL does not work on some * browsers. * - * ## This feels like too much overhead + * ### This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * * If your expressions are constant literals, they're automatically trusted and you don't need to - * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. - * `
`) just works. - * - * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * call `$sce.trustAs` on them (e.g. + * `
`) just works (remember to include the + * `ngSanitize` module). The `$sceDelegate` will also use the `$sanitize` service if it is available + * when binding untrusted values to `$sce.HTML` context. + * AngularJS provides an implementation in `angular-sanitize.js`, and if you + * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in + * your application. * * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load * templates in `ng-include` from your application's domain without having to even know about SCE. * It blocks loading templates from other domains or loading templates over http from an https * served document. You can change these by setting your own custom {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * ng.$sceDelegateProvider#trustedResourceUrlList trusted resource URL list} and {@link + * ng.$sceDelegateProvider#bannedResourceUrlList banned resource URL list} for matching such URLs. * * This significantly reduces the overhead. It is far easier to pay the small overhead and have an * application that's secure and can be audited to verify that with much more ease than bolting * security onto an application later. * * - * ## What trusted context types are supported? + * ### What trusted context types are supported? * * | Context | Notes | * |---------------------|----------------| * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.MEDIA_URL` | For URLs that are safe to render as media. Is automatically converted from string by sanitizing when needed. | + * | `$sce.URL` | For URLs that are safe to follow as links. Is automatically converted from string by sanitizing when needed. Note that `$sce.URL` makes a stronger statement about the URL than `$sce.MEDIA_URL` does and therefore contexts requiring values trusted for `$sce.URL` can be used anywhere that values trusted for `$sce.MEDIA_URL` are required.| + * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.)

Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` or `$sce.MEDIA_URL` do and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` or `$sce.MEDIA_URL` are required.

The {@link $sceDelegateProvider#trustedResourceUrlList $sceDelegateProvider#trustedResourceUrlList()} and {@link $sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider#bannedResourceUrlList()} can be used to restrict trusted origins for `RESOURCE_URL` | * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | * - * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
+ * + *
+ * Be aware that, before AngularJS 1.7.0, `a[href]` and `img[src]` used to sanitize their + * interpolated values directly rather than rely upon {@link ng.$sce#getTrusted `$sce.getTrusted`}. + * + * **As of 1.7.0, this is no longer the case.** + * + * Now such interpolations are marked as requiring `$sce.URL` (for `a[href]`) or `$sce.MEDIA_URL` + * (for `img[src]`), so that the sanitization happens (via `$sce.getTrusted...`) when the `$interpolate` + * service evaluates the expressions. + *
+ * + * There are no CSS or JS context bindings in AngularJS currently, so their corresponding `$sce.trustAs` + * functions aren't useful yet. This might evolve. + * + * ### Format of items in {@link ng.$sceDelegateProvider#trustedResourceUrlList trustedResourceUrlList}/{@link ng.$sceDelegateProvider#bannedResourceUrlList bannedResourceUrlList} * * Each element in these arrays must be one of the following: * @@ -17422,7 +20731,7 @@ function $SceDelegateProvider() { * match themselves. * - `*`: matches zero or more occurrences of any character other than one of the following 6 * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use - * in a whitelist. + * for matching resource URL lists. * - `**`: matches zero or more occurrences of *any* character. As such, it's not * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might @@ -17455,9 +20764,9 @@ function $SceDelegateProvider() { * * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. * - * ## Show me an example using SCE. + * ### Show me an example using SCE. * - * + * * *
*

@@ -17478,10 +20787,10 @@ function $SceDelegateProvider() { * * angular.module('mySceApp', ['ngSanitize']) * .controller('AppController', ['$http', '$templateCache', '$sce', - * function($http, $templateCache, $sce) { + * function AppController($http, $templateCache, $sce) { * var self = this; - * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - * self.userComments = userComments; + * $http.get('test_data.json', {cache: $templateCache}).then(function(response) { + * self.userComments = response.data; * }); * self.explicitlyTrustedHtml = $sce.trustAsHtml( * ' * describe('SCE doc demo', function() { * it('should sanitize untrusted values', function() { - * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML')) * .toBe('Is anyone reading this?'); * }); * * it('should NOT sanitize explicitly trusted values', function() { - * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe( * 'Hover over this text.'); * }); @@ -17525,20 +20834,20 @@ function $SceDelegateProvider() { * for little coding overhead. It will be much harder to take an SCE disabled application and * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. + * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if + * you are writing a library, you will cause security bugs applications using it. * * That said, here's how you can completely disable SCE: * * ``` * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. + * // Do not use in new projects or libraries. * $sceProvider.enabled(false); * }); * ``` * */ -/* jshint maxlen: 100 */ function $SceProvider() { var enabled = true; @@ -17548,8 +20857,8 @@ function $SceProvider() { * @name $sceProvider#enabled * @kind function * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. + * @param {boolean=} value If provided, then enables/disables SCE application-wide. + * @return {boolean} True if SCE is enabled, false otherwise. * * @description * Enables/disables SCE and returns the current value. @@ -17580,7 +20889,7 @@ function $SceProvider() { * such a value. * * - getTrusted(contextEnum, value) - * This function should return the a value that is safe to use in the context specified by + * This function should return the value that is safe to use in the context specified by * contextEnum or throw and exception otherwise. * * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be @@ -17603,13 +20912,14 @@ function $SceProvider() { * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) * will also succeed. * - * Inheritance happens to capture this in a natural way. In some future, we - * may not use inheritance anymore. That is OK because no code outside of - * sce.js and sceSpecs.js would need to be aware of this detail. + * Inheritance happens to capture this in a natural way. In some future, we may not use + * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to + * be aware of this detail. */ this.$get = ['$parse', '$sceDelegate', function( $parse, $sceDelegate) { + // Support: IE 9-11 only // 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 && msie < 8) { @@ -17626,8 +20936,8 @@ function $SceProvider() { * @name $sce#isEnabled * @kind function * - * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you - * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. * * @description * Returns a boolean indicating if SCE is enabled. @@ -17649,19 +20959,19 @@ function $SceProvider() { * @name $sce#parseAs * * @description - * Converts Angular {@link guide/expression expression} into a function. This is like {@link + * Converts AngularJS {@link guide/expression expression} into a function. This is like {@link * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, * *result*)} * - * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} type The SCE context in which this result will be used. * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ sce.parseAs = function sceParseAs(type, expr) { var parsed = $parse(expr); @@ -17679,18 +20989,18 @@ function $SceProvider() { * @name $sce#trustAs * * @description - * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, - * returns an object that is trusted by angular for use in specified strict contextual - * escaping contexts (such as ng-bind-html, ng-include, any src attribute - * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) - * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual - * escaping. + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a + * wrapped object that represents your value, and the trust you have in its safety for the given + * context. AngularJS can then use that value as-is in bindings of the specified secure context. + * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute + * interpolations. See {@link ng.$sce $sce} for strict contextual escaping. * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that that should be considered trusted. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in the context you specified. */ /** @@ -17701,11 +21011,23 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsHtml(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml - * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.HTML` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.HTML` context (like `ng-bind-html`). + */ + + /** + * @ngdoc method + * @name $sce#trustAsCss + * + * @description + * Shorthand method. `$sce.trustAsCss(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.CSS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant + * of your `value` in `$sce.CSS` context. This context is currently unused, so there are + * almost no reasons to use this function so far. */ /** @@ -17716,11 +21038,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl - * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.URL` context. That context is currently unused, so there are almost no reasons + * to use this function so far. */ /** @@ -17731,11 +21052,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsResourceUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute + * bindings, ...) */ /** @@ -17746,11 +21066,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsJs(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs - * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.JS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to + * use this function so far. */ /** @@ -17759,16 +21078,17 @@ function $SceProvider() { * * @description * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the - * originally supplied value if the queried context type is a supertype of the created type. - * If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} - * call. - * @returns {*} The value the was originally provided to - * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. - * Otherwise, throws an exception. + * takes any input, and either returns a value that's safe to use in the specified context, + * or throws an exception. This function is aware of trusted values created by the `trustAs` + * function and its shorthands, and when contexts are appropriate, returns the unwrapped value + * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a + * safe value (e.g., no sanitization is available or possible.) + * + * @param {string} type The context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs + * `$sce.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ /** @@ -17780,7 +21100,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)` */ /** @@ -17792,7 +21112,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)` */ /** @@ -17804,7 +21124,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.URL, value)` */ /** @@ -17816,7 +21136,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} * * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` */ /** @@ -17828,7 +21148,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.JS, value)` */ /** @@ -17840,12 +21160,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17857,12 +21177,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17874,12 +21194,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17891,12 +21211,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17908,12 +21228,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ // Shorthand delegations. @@ -17923,13 +21243,13 @@ function $SceProvider() { forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function(expr) { + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function(value) { + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function(value) { + sce[snakeToCamel('trust_as_' + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -17938,12 +21258,15 @@ function $SceProvider() { }]; } +/* exported $SnifferProvider */ + /** * !!! This is an undocumented "private" service !!! * * @name $sniffer * @requires $window * @requires $document + * @this * * @property {boolean} history Does the browser support html5 history api ? * @property {boolean} transitions Does the browser support CSS transition events ? @@ -17955,37 +21278,32 @@ function $SceProvider() { function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, + // Chrome Packaged Apps are not allowed to access `history.pushState`. + // If not sandboxed, they can be detected by the presence of `chrome.app.runtime` + // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by + // the presence of an extension runtime ID and the absence of other Chrome runtime APIs + // (see https://developer.chrome.com/apps/manifest/sandbox). + // (NW.js apps have access to Chrome APIs, but do support `history`.) + isNw = $window.nw && $window.nw.process, + isChromePackagedApp = + !isNw && + $window.chrome && + ($window.chrome.app && $window.chrome.app.runtime || + !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id), + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - vendorPrefix, - vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, - animations = false, - match; + animations = false; if (bodyStyle) { - 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) { - 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)) { - transitions = isString(bodyStyle.webkitTransition); - animations = isString(bodyStyle.webkitAnimation); - } + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle); + animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle); } @@ -17998,16 +21316,15 @@ function $SnifferProvider() { // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), - // jshint +W018 + history: !!(hasHistoryPushState && !(android < 4) && !boxee), hasEvent: function(event) { + // Support: IE 9-11 only // 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 // when cut operation is performed. // IE10+ implements 'input' event but it erroneously fires under various situations, // e.g. when placeholder changes, or a form is focused. - if (event === 'input' && msie <= 11) return false; + if (event === 'input' && msie) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -18017,7 +21334,6 @@ function $SnifferProvider() { return eventSupport[event]; }, csp: csp(), - vendorPrefix: vendorPrefix, transitions: transitions, animations: animations, android: android @@ -18025,80 +21341,250 @@ function $SnifferProvider() { }]; } -var $compileMinErr = minErr('$compile'); - /** - * @ngdoc service - * @name $templateRequest + * ! This is a private undocumented service ! * + * @name $$taskTrackerFactory * @description - * The `$templateRequest` service runs security checks then downloads the provided template using - * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request - * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the - * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the - * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted - * when `tpl` is of type string and `$templateCache` has the matching entry. + * A function to create `TaskTracker` instances. * - * @param {string|TrustedResourceUrl} tpl The HTTP request template URL - * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * A `TaskTracker` can keep track of pending tasks (grouped by type) and can notify interested + * parties when all pending tasks (or tasks of a specific type) have been completed. * - * @return {Promise} a promise for the HTTP response data of the given URL. + * @param {$log} log - A logger instance (such as `$log`). Used to log error during callback + * execution. * - * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + * @this */ -function $TemplateRequestProvider() { - this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) { - function handleRequestFn(tpl, ignoreRequestError) { - handleRequestFn.totalPendingRequests++; +function $$TaskTrackerFactoryProvider() { + this.$get = valueFn(function(log) { return new TaskTracker(log); }); +} - // We consider the template cache holds only trusted templates, so - // there's no need to go through whitelisting again for keys that already - // are included in there. This also makes Angular accept any script - // directive, no matter its name. However, we still need to unwrap trusted - // types. - if (!isString(tpl) || isUndefined($templateCache.get(tpl))) { - tpl = $sce.getTrustedResourceUrl(tpl); +function TaskTracker(log) { + var self = this; + var taskCounts = {}; + var taskCallbacks = []; + + var ALL_TASKS_TYPE = self.ALL_TASKS_TYPE = '$$all$$'; + var DEFAULT_TASK_TYPE = self.DEFAULT_TASK_TYPE = '$$default$$'; + + /** + * Execute the specified function and decrement the appropriate `taskCounts` counter. + * If the counter reaches 0, all corresponding `taskCallbacks` are executed. + * + * @param {Function} fn - The function to execute. + * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task that is being completed. + */ + self.completeTask = completeTask; + + /** + * Increase the task count for the specified task type (or the default task type if non is + * specified). + * + * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task whose count will be increased. + */ + self.incTaskCount = incTaskCount; + + /** + * Execute the specified callback when all pending tasks have been completed. + * + * If there are no pending tasks, the callback is executed immediately. You can optionally limit + * the tasks that will be waited for to a specific type, by passing a `taskType`. + * + * @param {function} callback - The function to call when there are no pending tasks. + * @param {string=} [taskType=ALL_TASKS_TYPE] - The type of tasks that will be waited for. + */ + self.notifyWhenNoPendingTasks = notifyWhenNoPendingTasks; + + function completeTask(fn, taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + + try { + fn(); + } finally { + decTaskCount(taskType); + + var countForType = taskCounts[taskType]; + var countForAll = taskCounts[ALL_TASKS_TYPE]; + + // If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks. + if (!countForAll || !countForType) { + var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType; + var nextCb; + + while ((nextCb = getNextCallback(taskType))) { + try { + nextCb(); + } catch (e) { + log.error(e); + } + } } + } + } - var transformResponse = $http.defaults && $http.defaults.transformResponse; + function decTaskCount(taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + if (taskCounts[taskType]) { + taskCounts[taskType]--; + taskCounts[ALL_TASKS_TYPE]--; + } + } - if (isArray(transformResponse)) { - transformResponse = transformResponse.filter(function(transformer) { - return transformer !== defaultHttpResponseTransform; - }); - } else if (transformResponse === defaultHttpResponseTransform) { - transformResponse = null; + function getLastCallback() { + var cbInfo = taskCallbacks.pop(); + return cbInfo && cbInfo.cb; + } + + function getLastCallbackForType(taskType) { + for (var i = taskCallbacks.length - 1; i >= 0; --i) { + var cbInfo = taskCallbacks[i]; + if (cbInfo.type === taskType) { + taskCallbacks.splice(i, 1); + return cbInfo.cb; } + } + } - var httpOptions = { - cache: $templateCache, - transformResponse: transformResponse - }; + function incTaskCount(taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + taskCounts[taskType] = (taskCounts[taskType] || 0) + 1; + taskCounts[ALL_TASKS_TYPE] = (taskCounts[ALL_TASKS_TYPE] || 0) + 1; + } - return $http.get(tpl, httpOptions) - ['finally'](function() { - handleRequestFn.totalPendingRequests--; - }) - .then(function(response) { - $templateCache.put(tpl, response.data); - return response.data; - }, handleError); + function notifyWhenNoPendingTasks(callback, taskType) { + taskType = taskType || ALL_TASKS_TYPE; + if (!taskCounts[taskType]) { + callback(); + } else { + taskCallbacks.push({type: taskType, cb: callback}); + } + } +} + +var $templateRequestMinErr = minErr('$templateRequest'); + +/** + * @ngdoc provider + * @name $templateRequestProvider + * @this + * + * @description + * Used to configure the options passed to the {@link $http} service when making a template request. + * + * For example, it can be used for specifying the "Accept" header that is sent to the server, when + * requesting a template. + */ +function $TemplateRequestProvider() { + + var httpOptions; + + /** + * @ngdoc method + * @name $templateRequestProvider#httpOptions + * @description + * The options to be passed to the {@link $http} service when making the request. + * You can use this to override options such as the "Accept" header for template requests. + * + * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the + * options if not overridden here. + * + * @param {string=} value new value for the {@link $http} options. + * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter. + */ + this.httpOptions = function(val) { + if (val) { + httpOptions = val; + return this; + } + return httpOptions; + }; + + /** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service runs security checks then downloads the provided template using + * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request + * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the + * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the + * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted + * when `tpl` is of type string and `$templateCache` has the matching entry. + * + * If you want to pass custom options to the `$http` service, such as setting the Accept header you + * can configure this via {@link $templateRequestProvider#httpOptions}. + * + * `$templateRequest` is used internally by {@link $compile}, {@link ngRoute.$route}, and directives such + * as {@link ngInclude} to download and cache templates. + * + * 3rd party modules should use `$templateRequest` if their services or directives are loading + * templates. + * + * @param {string|TrustedResourceUrl} tpl The HTTP request template URL + * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * + * @return {Promise} a promise for the HTTP response data of the given URL. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ + this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce', + function($exceptionHandler, $templateCache, $http, $q, $sce) { + + function handleRequestFn(tpl, ignoreRequestError) { + handleRequestFn.totalPendingRequests++; + + // We consider the template cache holds only trusted templates, so + // there's no need to go through adding the template again to the trusted + // resources for keys that already are included in there. This also makes + // AngularJS accept any script directive, no matter its name. However, we + // still need to unwrap trusted types. + if (!isString(tpl) || isUndefined($templateCache.get(tpl))) { + tpl = $sce.getTrustedResourceUrl(tpl); + } + + var transformResponse = $http.defaults && $http.defaults.transformResponse; + + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + return $http.get(tpl, extend({ + cache: $templateCache, + transformResponse: transformResponse + }, httpOptions)) + .finally(function() { + handleRequestFn.totalPendingRequests--; + }) + .then(function(response) { + return $templateCache.put(tpl, response.data); + }, handleError); + + function handleError(resp) { + if (!ignoreRequestError) { + resp = $templateRequestMinErr('tpload', + 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); + + $exceptionHandler(resp); + } - function handleError(resp) { - if (!ignoreRequestError) { - throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', - tpl, resp.status, resp.statusText); + return $q.reject(resp); } - return $q.reject(resp); } - } - handleRequestFn.totalPendingRequests = 0; + handleRequestFn.totalPendingRequests = 0; - return handleRequestFn; - }]; + return handleRequestFn; + } + ]; } +/** @this */ function $$TestabilityProvider() { this.$get = ['$rootScope', '$browser', '$location', function($rootScope, $browser, $location) { @@ -18137,7 +21623,7 @@ function $$TestabilityProvider() { matches.push(binding); } } else { - if (bindingName.indexOf(expression) != -1) { + if (bindingName.indexOf(expression) !== -1) { matches.push(binding); } } @@ -18202,7 +21688,15 @@ function $$TestabilityProvider() { * @name $$testability#whenStable * * @description - * Calls the callback when $timeout and $http requests are completed. + * Calls the callback when all pending tasks are completed. + * + * Types of tasks waited for include: + * - Pending timeouts (via {@link $timeout}). + * - Pending HTTP requests (via {@link $http}). + * - In-progress route transitions (via {@link $route}). + * - Pending tasks scheduled via {@link $rootScope#$applyAsync}. + * - Pending tasks scheduled via {@link $rootScope#$evalAsync}. + * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises). * * @param {function} callback */ @@ -18214,6 +21708,9 @@ function $$TestabilityProvider() { }]; } +var $timeoutMinErr = minErr('$timeout'); + +/** @this */ function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', function($rootScope, $browser, $q, $$q, $exceptionHandler) { @@ -18221,35 +21718,35 @@ function $TimeoutProvider() { var deferreds = {}; - /** - * @ngdoc service - * @name $timeout - * - * @description - * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch - * block and delegates any exceptions to - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * The return value of calling `$timeout` is a promise, which will be resolved when - * the delay has passed and the timeout function, if provided, is executed. - * - * To cancel a timeout request, call `$timeout.cancel(promise)`. - * - * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to - * synchronously flush the queue of deferred functions. - * - * If you only want a promise that will be resolved after some specified delay - * then you can call `$timeout` without the `fn` function. - * - * @param {function()=} fn A function, whose execution should be delayed. - * @param {number=} [delay=0] Delay in milliseconds. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise - * will be resolved with the return value of the `fn` function. - * - */ + /** + * @ngdoc service + * @name $timeout + * + * @description + * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch + * block and delegates any exceptions to + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * The return value of calling `$timeout` is a promise, which will be resolved when + * the delay has passed and the timeout function, if provided, is executed. + * + * To cancel a timeout request, call `$timeout.cancel(promise)`. + * + * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to + * synchronously flush the queue of deferred functions. + * + * If you only want a promise that will be resolved after some specified delay + * then you can call `$timeout` without the `fn` function. + * + * @param {function()=} fn A function, whose execution should be delayed. + * @param {number=} [delay=0] Delay in milliseconds. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise + * will be resolved with the return value of the `fn` function. + * + */ function timeout(fn, delay, invokeApply) { if (!isFunction(fn)) { invokeApply = delay; @@ -18269,13 +21766,12 @@ function $TimeoutProvider() { } catch (e) { deferred.reject(e); $exceptionHandler(e); - } - finally { + } finally { delete deferreds[promise.$$timeoutId]; } if (!skipApply) $rootScope.$apply(); - }, delay); + }, delay, '$timeout'); promise.$$timeoutId = timeoutId; deferreds[timeoutId] = deferred; @@ -18284,25 +21780,37 @@ function $TimeoutProvider() { } - /** - * @ngdoc method - * @name $timeout#cancel - * - * @description - * Cancels a task associated with the `promise`. As a result of this, the promise will be - * resolved with a rejection. - * - * @param {Promise=} promise Promise returned by the `$timeout` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ + /** + * @ngdoc method + * @name $timeout#cancel + * + * @description + * Cancels a task associated with the `promise`. As a result of this, the promise will be + * resolved with a rejection. + * + * @param {Promise=} promise Promise returned by the `$timeout` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ timeout.cancel = function(promise) { - if (promise && promise.$$timeoutId in deferreds) { - deferreds[promise.$$timeoutId].reject('canceled'); - delete deferreds[promise.$$timeoutId]; - return $browser.defer.cancel(promise.$$timeoutId); + if (!promise) return false; + + if (!promise.hasOwnProperty('$$timeoutId')) { + throw $timeoutMinErr('badprom', + '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.'); } - return false; + + if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false; + + var id = promise.$$timeoutId; + var deferred = deferreds[id]; + + // Timeout cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + delete deferreds[id]; + + return $browser.defer.cancel(id); }; return timeout; @@ -18316,9 +21824,16 @@ function $TimeoutProvider() { // doesn't know about mocked locations and resolves URLs to the real document - which is // exactly the behavior needed here. There is little value is mocking these out for this // service. -var urlParsingNode = document.createElement("a"); +var urlParsingNode = window.document.createElement('a'); var originUrl = urlResolve(window.location.href); +var baseUrlParsingNode; +urlParsingNode.href = 'http://[::1]'; + +// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview) +// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets +// when parsed out of an anchor element. +var ipv6InBrackets = urlParsingNode.hostname === '[::1]'; /** * @@ -18329,7 +21844,7 @@ var originUrl = urlResolve(window.location.href); * URL will be resolved into an absolute URL in the context of the application document. * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * compatibility - Safari 1+, Mozilla 1+ etc. See * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * * Implementation Notes for IE @@ -18349,42 +21864,51 @@ var originUrl = urlResolve(window.location.href); * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * * @kind function - * @param {string} url The URL to be parsed. + * @param {string|object} url The URL to be parsed. If `url` is not a string, it will be returned + * unchanged. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. * - * | member name | Description | - * |---------------|----------------| + * | member name | Description | + * |---------------|------------------------------------------------------------------------| * | href | A normalized version of the provided URL if it was not an absolute URL | - * | protocol | The protocol including the trailing colon | + * | protocol | The protocol without the trailing colon | * | host | The host and port (if the port is non-default) of the normalizedUrl | * | search | The search params, minus the question mark | - * | hash | The hash string, minus the hash symbol - * | hostname | The hostname - * | port | The port, without ":" - * | pathname | The pathname, beginning with "/" + * | hash | The hash string, minus the hash symbol | + * | hostname | The hostname | + * | port | The port, without ":" | + * | pathname | The pathname, beginning with "/" | * */ function urlResolve(url) { + if (!isString(url)) return url; + var href = url; + // Support: IE 9-11 only if (msie) { // Normalize before parse. Refer Implementation Notes on why this is // done in two steps on IE. - urlParsingNode.setAttribute("href", href); + urlParsingNode.setAttribute('href', href); href = urlParsingNode.href; } urlParsingNode.setAttribute('href', href); - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + var hostname = urlParsingNode.hostname; + + if (!ipv6InBrackets && hostname.indexOf(':') > -1) { + hostname = '[' + hostname + ']'; + } + return { href: urlParsingNode.href, protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', host: urlParsingNode.host, search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', - hostname: urlParsingNode.hostname, + hostname: hostname, port: urlParsingNode.port, pathname: (urlParsingNode.pathname.charAt(0) === '/') ? urlParsingNode.pathname @@ -18393,26 +21917,107 @@ function urlResolve(url) { } /** - * Parse a request URL and determine whether this is a same-origin request as the application document. + * Parse a request URL and determine whether this is a same-origin request as the application + * document. * * @param {string|object} requestUrl The url of the request as a string that will be resolved * or a parsed URL object. * @returns {boolean} Whether the request is for the same origin as the application document. */ function urlIsSameOrigin(requestUrl) { - var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; - return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); + return urlsAreSameOrigin(requestUrl, originUrl); +} + +/** + * Parse a request URL and determine whether it is same-origin as the current document base URL. + * + * Note: The base URL is usually the same as the document location (`location.href`) but can + * be overriden by using the `` tag. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the URL is same-origin as the document base URL. + */ +function urlIsSameOriginAsBaseUrl(requestUrl) { + return urlsAreSameOrigin(requestUrl, getBaseUrl()); +} + +/** + * Create a function that can check a URL's origin against a list of allowed/trusted origins. + * The current location's origin is implicitly trusted. + * + * @param {string[]} trustedOriginUrls - A list of URLs (strings), whose origins are trusted. + * + * @returns {Function} - A function that receives a URL (string or parsed URL object) and returns + * whether it is of an allowed origin. + */ +function urlIsAllowedOriginFactory(trustedOriginUrls) { + var parsedAllowedOriginUrls = [originUrl].concat(trustedOriginUrls.map(urlResolve)); + + /** + * Check whether the specified URL (string or parsed URL object) has an origin that is allowed + * based on a list of trusted-origin URLs. The current location's origin is implicitly + * trusted. + * + * @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be + * resolved or a parsed URL object). + * + * @returns {boolean} - Whether the specified URL is of an allowed origin. + */ + return function urlIsAllowedOrigin(requestUrl) { + var parsedUrl = urlResolve(requestUrl); + return parsedAllowedOriginUrls.some(urlsAreSameOrigin.bind(null, parsedUrl)); + }; +} + +/** + * Determine if two URLs share the same origin. + * + * @param {string|Object} url1 - First URL to compare as a string or a normalized URL in the form of + * a dictionary object returned by `urlResolve()`. + * @param {string|object} url2 - Second URL to compare as a string or a normalized URL in the form + * of a dictionary object returned by `urlResolve()`. + * + * @returns {boolean} - True if both URLs have the same origin, and false otherwise. + */ +function urlsAreSameOrigin(url1, url2) { + url1 = urlResolve(url1); + url2 = urlResolve(url2); + + return (url1.protocol === url2.protocol && + url1.host === url2.host); +} + +/** + * Returns the current document base URL. + * @returns {string} + */ +function getBaseUrl() { + if (window.document.baseURI) { + return window.document.baseURI; + } + + // `document.baseURI` is available everywhere except IE + if (!baseUrlParsingNode) { + baseUrlParsingNode = window.document.createElement('a'); + baseUrlParsingNode.href = '.'; + + // Work-around for IE bug described in Implementation Notes. The fix in `urlResolve()` is not + // suitable here because we need to track changes to the base URL. + baseUrlParsingNode = baseUrlParsingNode.cloneNode(false); + } + return baseUrlParsingNode.href; } /** * @ngdoc service * @name $window + * @this * * @description * A reference to the browser's `window` object. While `window` * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the + * it is a global variable. In AngularJS we always refer to it through the * `$window` service, so it may be overridden, removed or mocked for testing. * * Expressions, like the one defined for the `ngClick` directive in the example @@ -18421,7 +22026,7 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - + +
+ +

{{title}}

+ +

{{title | uppercase}}

+
+
+
*/ var uppercaseFilter = valueFn(uppercase); @@ -19614,24 +23297,25 @@ var uppercaseFilter = valueFn(uppercase); * @kind function * * @description - * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array, string or number, as specified by - * the value and sign (positive or negative) of `limit`. If a number is used as input, it is - * converted to a string. - * - * @param {Array|string|number} input Source array, string or number to be limited. - * @param {string|number} limit The length of the returned array or string. If the `limit` number + * Creates a new array or string containing only a specified number of elements. The elements are + * taken from either the beginning or the end of the source array, string or number, as specified by + * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported + * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input, + * it is converted to a string. + * + * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited. + * @param {string|number} limit - The length of the returned array or string. If the `limit` number * is positive, `limit` number of items from the beginning of the source array/string are copied. * If the number is negative, `limit` number of items from the end of the source array/string * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined, * the input will be returned unchanged. - * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin` - * indicates an offset from the end of `input`. Defaults to `0`. - * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array - * had less than `limit` elements. + * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index, + * `begin` indicates an offset from the end of `input`. Defaults to `0`. + * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had + * less than `limit` elements. * * @example - + +
+ + Model as range: +
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}} +
+
+
+ + * ## Range Input with ngMin & ngMax attributes + + * @example + + + +
+ Model as range: +
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}} +
+
+
+ + */ + 'range': rangeInputType, /** * @ngdoc input @@ -22098,11 +26565,11 @@ var inputType = { * @description * HTML checkbox. * - * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string} ngModel Assignable AngularJS expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {expression=} ngTrueValue The value to which the expression should be set when selected. * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. - * @param {string=} ngChange Angular expression to be executed when input changes due to user + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @example @@ -22169,16 +26636,26 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var type = lowercase(element[0].type); - // In composition mode, users are still inputing intermediate text buffer, + // In composition mode, users are still inputting intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent if (!$sniffer.android) { var composing = false; - element.on('compositionstart', function(data) { + element.on('compositionstart', function() { composing = true; }); + // Support: IE9+ + element.on('compositionupdate', function(ev) { + // End composition when ev.data is empty string on 'compositionupdate' event. + // When the input de-focusses (e.g. by clicking away), IE triggers 'compositionupdate' + // instead of 'compositionend'. + if (isUndefined(ev.data) || ev.data === '') { + composing = false; + } + }); + element.on('compositionend', function() { composing = false; listener(); @@ -22227,7 +26704,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { } }; - element.on('keydown', function(event) { + element.on('keydown', /** @this */ function(event) { var key = event.keyCode; // ignore @@ -22237,9 +26714,9 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { deferListener(event, this, this.value); }); - // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it + // if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it if ($sniffer.hasEvent('paste')) { - element.on('paste cut', deferListener); + element.on('paste cut drop', deferListener); } } @@ -22252,7 +26729,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // For these event types, when native validators are present and the browser supports the type, // check for validity changes on various DOM events. if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) { - element.on(PARTIAL_VALIDATION_EVENTS, function(ev) { + element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) { if (!timeout) { var validity = this[VALIDITY_STATE_PROPERTY]; var origBadInput = validity.badInput; @@ -22309,7 +26786,7 @@ function weekParser(isoWeek, existingDate) { } function createDateParser(regexp, mapping) { - return function(iso, date) { + return function(iso, previousDate) { var parts, map; if (isDate(iso)) { @@ -22320,7 +26797,7 @@ function createDateParser(regexp, mapping) { // When a date is JSON'ified to wraps itself inside of an extra // set of double quotes. This makes the date parsing code unable // to match the date string and parse it as a date. - if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') { + if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') { iso = iso.substring(1, iso.length - 1); } if (ISO_DATE_REGEXP.test(iso)) { @@ -22331,15 +26808,15 @@ function createDateParser(regexp, mapping) { if (parts) { parts.shift(); - if (date) { + if (previousDate) { map = { - yyyy: date.getFullYear(), - MM: date.getMonth() + 1, - dd: date.getDate(), - HH: date.getHours(), - mm: date.getMinutes(), - ss: date.getSeconds(), - sss: date.getMilliseconds() / 1000 + yyyy: previousDate.getFullYear(), + MM: previousDate.getMonth() + 1, + dd: previousDate.getDate(), + HH: previousDate.getHours(), + mm: previousDate.getMinutes(), + ss: previousDate.getSeconds(), + sss: previousDate.getMilliseconds() / 1000 }; } else { map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; @@ -22350,7 +26827,15 @@ function createDateParser(regexp, mapping) { map[mapping[index]] = +part; } }); - return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + + var date = new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + if (map.yyyy < 100) { + // In the constructor, 2-digit years map to 1900-1999. + // Use `setFullYear()` to set the correct year. + date.setFullYear(map.yyyy); + } + + return date; } } @@ -22359,25 +26844,24 @@ function createDateParser(regexp, mapping) { } function createDateInputType(type, regexp, parseDate, format) { - return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { - badInputChecker(scope, element, attr, ctrl); + return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + badInputChecker(scope, element, attr, ctrl, type); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + + var isTimeType = type === 'time' || type === 'datetimelocal'; var previousDate; + var previousTimezone; - ctrl.$$parserName = type; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { // Note: We cannot read ctrl.$modelValue, as there might be a different // parser/formatter in the processing chain so that the model // contains some different data format! - var parsedDate = parseDate(value, previousDate); - if (timezone) { - parsedDate = convertTimezoneToLocal(parsedDate, timezone); - } - return parsedDate; + return parseDateAndConvertTimeZoneToLocal(value, previousDate); } + ctrl.$$parserName = type; return undefined; }); @@ -22387,35 +26871,50 @@ function createDateInputType(type, regexp, parseDate, format) { } if (isValidDate(value)) { previousDate = value; - if (previousDate && timezone) { + var timezone = ctrl.$options.getOption('timezone'); + + if (timezone) { + previousTimezone = timezone; previousDate = convertTimezoneToLocal(previousDate, timezone, true); } - return $filter('date')(value, format, timezone); + + return formatter(value, timezone); } else { previousDate = null; + previousTimezone = null; return ''; } }); if (isDefined(attr.min) || attr.ngMin) { - var minVal; + var minVal = attr.min || $parse(attr.ngMin)(scope); + var parsedMinVal = parseObservedDateValue(minVal); + ctrl.$validators.min = function(value) { - return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; + return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal; }; attr.$observe('min', function(val) { - minVal = parseObservedDateValue(val); - ctrl.$validate(); + if (val !== minVal) { + parsedMinVal = parseObservedDateValue(val); + minVal = val; + ctrl.$validate(); + } }); } if (isDefined(attr.max) || attr.ngMax) { - var maxVal; + var maxVal = attr.max || $parse(attr.ngMax)(scope); + var parsedMaxVal = parseObservedDateValue(maxVal); + ctrl.$validators.max = function(value) { - return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal; }; attr.$observe('max', function(val) { - maxVal = parseObservedDateValue(val); - ctrl.$validate(); + if (val !== maxVal) { + parsedMaxVal = parseObservedDateValue(val); + maxVal = val; + ctrl.$validate(); + } }); } @@ -22425,77 +26924,356 @@ function createDateInputType(type, regexp, parseDate, format) { } function parseObservedDateValue(val) { - return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val; + return isDefined(val) && !isDate(val) ? parseDateAndConvertTimeZoneToLocal(val) || undefined : val; + } + + function parseDateAndConvertTimeZoneToLocal(value, previousDate) { + var timezone = ctrl.$options.getOption('timezone'); + + if (previousTimezone && previousTimezone !== timezone) { + // If the timezone has changed, adjust the previousDate to the default timezone + // so that the new date is converted with the correct timezone offset + previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone)); + } + + var parsedDate = parseDate(value, previousDate); + + if (!isNaN(parsedDate) && timezone) { + parsedDate = convertTimezoneToLocal(parsedDate, timezone); + } + return parsedDate; + } + + function formatter(value, timezone) { + var targetFormat = format; + + if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) { + targetFormat = format + .replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat')) + .replace(/:$/, ''); + } + + var formatted = $filter('date')(value, targetFormat, timezone); + + if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) { + formatted = formatted.replace(/(?::00)?(?:\.000)?$/, ''); + } + + return formatted; } }; } -function badInputChecker(scope, element, attr, ctrl) { - var node = element[0]; - var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); - if (nativeValidation) { - ctrl.$parsers.push(function(value) { - var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; - // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): - // - also sets validity.badInput (should only be validity.typeMismatch). - // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) - // - can ignore this case as we can still read out the erroneous email... - return validity.badInput && !validity.typeMismatch ? undefined : value; - }); +function badInputChecker(scope, element, attr, ctrl, parserName) { + var node = element[0]; + var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); + if (nativeValidation) { + ctrl.$parsers.push(function(value) { + var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; + if (validity.badInput || validity.typeMismatch) { + ctrl.$$parserName = parserName; + return undefined; + } + + return value; + }); + } +} + +function numberFormatterParser(ctrl) { + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (NUMBER_REGEXP.test(value)) return parseFloat(value); + + ctrl.$$parserName = 'number'; + return undefined; + }); + + ctrl.$formatters.push(function(value) { + if (!ctrl.$isEmpty(value)) { + if (!isNumber(value)) { + throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + } + value = value.toString(); + } + return value; + }); +} + +function parseNumberAttrVal(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val); + } + return !isNumberNaN(val) ? val : undefined; +} + +function isNumberInteger(num) { + // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066 + // (minus the assumption that `num` is a number) + + // eslint-disable-next-line no-bitwise + return (num | 0) === num; +} + +function countDecimals(num) { + var numString = num.toString(); + var decimalSymbolIndex = numString.indexOf('.'); + + if (decimalSymbolIndex === -1) { + if (-1 < num && num < 1) { + // It may be in the exponential notation format (`1e-X`) + var match = /e-(\d+)$/.exec(numString); + + if (match) { + return Number(match[1]); + } + } + + return 0; + } + + return numString.length - decimalSymbolIndex - 1; +} + +function isValidForStep(viewValue, stepBase, step) { + // At this point `stepBase` and `step` are expected to be non-NaN values + // and `viewValue` is expected to be a valid stringified number. + var value = Number(viewValue); + + var isNonIntegerValue = !isNumberInteger(value); + var isNonIntegerStepBase = !isNumberInteger(stepBase); + var isNonIntegerStep = !isNumberInteger(step); + + // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or + // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers. + if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) { + var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0; + var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0; + var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0; + + var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + + if (isNonIntegerValue) value = Math.round(value); + if (isNonIntegerStepBase) stepBase = Math.round(stepBase); + if (isNonIntegerStep) step = Math.round(step); + } + + return (value - stepBase) % step === 0; +} + +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + badInputChecker(scope, element, attr, ctrl, 'number'); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var parsedMinVal; + + if (isDefined(attr.min) || attr.ngMin) { + var minVal = attr.min || $parse(attr.ngMin)(scope); + parsedMinVal = parseNumberAttrVal(minVal); + + ctrl.$validators.min = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal; + }; + + attr.$observe('min', function(val) { + if (val !== minVal) { + parsedMinVal = parseNumberAttrVal(val); + minVal = val; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + }); + } + + if (isDefined(attr.max) || attr.ngMax) { + var maxVal = attr.max || $parse(attr.ngMax)(scope); + var parsedMaxVal = parseNumberAttrVal(maxVal); + + ctrl.$validators.max = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal; + }; + + attr.$observe('max', function(val) { + if (val !== maxVal) { + parsedMaxVal = parseNumberAttrVal(val); + maxVal = val; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + }); + } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal = attr.step || $parse(attr.ngStep)(scope); + var parsedStepVal = parseNumberAttrVal(stepVal); + + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) || + isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal); + }; + + attr.$observe('step', function(val) { + // TODO(matsko): implement validateLater to reduce number of validations + if (val !== stepVal) { + parsedStepVal = parseNumberAttrVal(val); + stepVal = val; + ctrl.$validate(); + } + + }); + + } +} + +function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl, 'range'); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range', + minVal = supportsRange ? 0 : undefined, + maxVal = supportsRange ? 100 : undefined, + stepVal = supportsRange ? 1 : undefined, + validity = element[0].validity, + hasMinAttr = isDefined(attr.min), + hasMaxAttr = isDefined(attr.max), + hasStepAttr = isDefined(attr.step); + + var originalRender = ctrl.$render; + + ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ? + //Browsers that implement range will set these values automatically, but reading the adjusted values after + //$render would cause the min / max validators to be applied with the wrong value + function rangeRender() { + originalRender(); + ctrl.$setViewValue(element.val()); + } : + originalRender; + + if (hasMinAttr) { + minVal = parseNumberAttrVal(attr.min); + + ctrl.$validators.min = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMinValidator() { return true; } : + // non-support browsers validate the min val + function minValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; + }; + + setInitialValueAndObserver('min', minChange); + } + + if (hasMaxAttr) { + maxVal = parseNumberAttrVal(attr.max); + + ctrl.$validators.max = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMaxValidator() { return true; } : + // non-support browsers validate the max val + function maxValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; + }; + + setInitialValueAndObserver('max', maxChange); + } + + if (hasStepAttr) { + stepVal = parseNumberAttrVal(attr.step); + + ctrl.$validators.step = supportsRange ? + function nativeStepValidator() { + // Currently, only FF implements the spec on step change correctly (i.e. adjusting the + // input element value to a valid value). It's possible that other browsers set the stepMismatch + // validity error instead, so we can at least report an error in that case. + return !validity.stepMismatch; + } : + // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would + function stepValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + setInitialValueAndObserver('step', stepChange); } -} -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + function setInitialValueAndObserver(htmlAttrName, changeFn) { + // interpolated attributes set the attribute value only after a digest, but we need the + // attribute value when the input is first rendered, so that the browser can adjust the + // input value based on the min/max value + element.attr(htmlAttrName, attr[htmlAttrName]); + var oldVal = attr[htmlAttrName]; + attr.$observe(htmlAttrName, function wrappedObserver(val) { + if (val !== oldVal) { + oldVal = val; + changeFn(val); + } + }); + } - ctrl.$$parserName = 'number'; - ctrl.$parsers.push(function(value) { - if (ctrl.$isEmpty(value)) return null; - if (NUMBER_REGEXP.test(value)) return parseFloat(value); - return undefined; - }); + function minChange(val) { + minVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } - ctrl.$formatters.push(function(value) { - if (!ctrl.$isEmpty(value)) { - if (!isNumber(value)) { - throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the minVal is greater than the element value + if (minVal > elVal) { + elVal = minVal; + element.val(elVal); } - value = value.toString(); + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); } - return value; - }); + } - if (isDefined(attr.min) || attr.ngMin) { - var minVal; - ctrl.$validators.min = function(value) { - return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; - }; + function maxChange(val) { + maxVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } - attr.$observe('min', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the maxVal is less than the element value + if (maxVal < elVal) { + element.val(maxVal); + // IE11 and Chrome don't set the value to the minVal when max < min + elVal = maxVal < minVal ? minVal : maxVal; } - minVal = isNumber(val) && !isNaN(val) ? val : undefined; + ctrl.$setViewValue(elVal); + } else { // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); - }); + } } - if (isDefined(attr.max) || attr.ngMax) { - var maxVal; - ctrl.$validators.max = function(value) { - return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; - }; + function stepChange(val) { + stepVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } - attr.$observe('max', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); - } - maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + // Some browsers don't adjust the input value correctly, but set the stepMismatch error + if (!supportsRange) { // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); - }); + } else if (ctrl.$viewValue !== element.val()) { + ctrl.$setViewValue(element.val()); + } } } @@ -22505,7 +27283,6 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { baseInputType(scope, element, attr, ctrl, $sniffer, $browser); stringBasedInputType(ctrl); - ctrl.$$parserName = 'url'; ctrl.$validators.url = function(modelValue, viewValue) { var value = modelValue || viewValue; return ctrl.$isEmpty(value) || URL_REGEXP.test(value); @@ -22518,7 +27295,6 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { baseInputType(scope, element, attr, ctrl, $sniffer, $browser); stringBasedInputType(ctrl); - ctrl.$$parserName = 'email'; ctrl.$validators.email = function(modelValue, viewValue) { var value = modelValue || viewValue; return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); @@ -22526,22 +27302,31 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { } function radioInputType(scope, element, attr, ctrl) { + var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false'; // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } var listener = function(ev) { + var value; if (element[0].checked) { - ctrl.$setViewValue(attr.value, ev && ev.type); + value = attr.value; + if (doTrim) { + value = trim(value); + } + ctrl.$setViewValue(value, ev && ev.type); } }; - element.on('click', listener); + element.on('change', listener); ctrl.$render = function() { var value = attr.value; - element[0].checked = (value == ctrl.$viewValue); + if (doTrim) { + value = trim(value); + } + element[0].checked = (value === ctrl.$viewValue); }; attr.$observe('value', ctrl.$render); @@ -22568,7 +27353,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt ctrl.$setViewValue(element[0].checked, ev && ev.type); }; - element.on('click', listener); + element.on('change', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; @@ -22597,11 +27382,11 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @restrict E * * @description - * HTML textarea element control with angular data-binding. The data-binding and validation + * HTML textarea element control with AngularJS data-binding. The data-binding and validation * properties of this element are exactly the same as those of the * {@link ng.directive:input input element}. * - * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string} ngModel Assignable AngularJS expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to @@ -22612,8 +27397,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -22621,9 +27406,23 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to * start at the index of the last search's match, thus not taking the whole input value into * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. - * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input. + * + * @knownIssue + * + * When specifying the `placeholder` attribute of ` @@ -25100,8 +30157,6 @@ var ngInitDirective = ngDirective({ *
* * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. */ var ngListDirective = function() { return { @@ -25109,9 +30164,7 @@ var ngListDirective = function() { priority: 100, require: 'ngModel', link: function(scope, element, attr, ctrl) { - // We want to control whitespace trimming so we use this convoluted approach - // to access the ngList attribute, which doesn't pre-trim the attribute - var ngList = element.attr(attr.$attr.ngList) || ', '; + var ngList = attr.ngList || ', '; var trimValues = attr.ngTrim !== 'false'; var separator = trimValues ? trim(ngList) : ngList; @@ -25153,51 +30206,81 @@ var ngListDirective = function() { DIRTY_CLASS: true, UNTOUCHED_CLASS: true, TOUCHED_CLASS: true, + PENDING_CLASS: true, + addSetValidityMethod: true, + setupValidity: true, + defaultModelOptions: false */ + var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', DIRTY_CLASS = 'ng-dirty', UNTOUCHED_CLASS = 'ng-untouched', TOUCHED_CLASS = 'ng-touched', - PENDING_CLASS = 'ng-pending'; + EMPTY_CLASS = 'ng-empty', + NOT_EMPTY_CLASS = 'ng-not-empty'; var ngModelMinErr = minErr('ngModel'); /** * @ngdoc type * @name ngModel.NgModelController - * * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue * is set. + * * @property {*} $modelValue The value in the model that the control is bound to. + * * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever - the control reads value from the DOM. The functions are called in array order, each passing - its return value through to the next. The last return value is forwarded to the - {@link ngModel.NgModelController#$validators `$validators`} collection. + * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue + `$viewValue`} from the DOM, usually via user input. + See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation. + Note that the `$parsers` are not called when the bound ngModel expression changes programmatically. + + The functions are called in array order, each passing + its return value through to the next. The last return value is forwarded to the + {@link ngModel.NgModelController#$validators `$validators`} collection. + + Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue + `$viewValue`}. -Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue -`$viewValue`}. + Returning `undefined` from a parser means a parse error occurred. In that case, + no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` + will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} + is set to `true`. The parse error is stored in `ngModel.$error.parse`. -Returning `undefined` from a parser means a parse error occurred. In that case, -no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` -will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} -is set to `true`. The parse error is stored in `ngModel.$error.parse`. + This simple example shows a parser that would convert text input value to lowercase: + * ```js + * function parse(value) { + * if (value) { + * return value.toLowerCase(); + * } + * } + * ngModelController.$parsers.push(parse); + * ``` * * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever - the model value changes. The functions are called in reverse array order, each passing the value through to the - next. The last return value is used as the actual DOM value. - Used to format / convert values for display in the control. + the bound ngModel expression changes programmatically. The `$formatters` are not called when the + value of the control is changed by user interaction. + + Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue + `$modelValue`} for display in the control. + + The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. + + This simple example shows a formatter that would convert the model value to uppercase: + * ```js - * function formatter(value) { + * function format(value) { * if (value) { * return value.toUpperCase(); * } * } - * ngModel.$formatters.push(formatter); + * ngModel.$formatters.push(format); * ``` * * @property {Object.} $validators A collection of validators that are applied @@ -25244,8 +30327,10 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * }; * ``` * - * @property {Array.} $viewChangeListeners Array of functions to execute whenever the - * view value has changed. It is called with no arguments, and its return value is ignored. + * @property {Array.} $viewChangeListeners Array of functions to execute whenever + * a change to {@link ngModel.NgModelController#$viewValue `$viewValue`} has caused a change + * to {@link ngModel.NgModelController#$modelValue `$modelValue`}. + * It is called with no arguments, and its return value is ignored. * This can be used in place of additional $watches against the model value. * * @property {Object} $error An object hash with all failing validator ids as keys. @@ -25267,7 +30352,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * listening to DOM events. * Such DOM related logic should be provided by other directives which make use of * `NgModelController` for data-binding to control elements. - * Angular provides this DOM logic for most {@link input `input`} elements. + * AngularJS provides this DOM logic for most {@link input `input`} elements. * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example * custom control example} that uses `ngModelController` to bind to `contenteditable` elements. * @@ -25323,7 +30408,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. var html = element.html(); // When we clear the content editable the browser leaves a
behind // If strip-br attribute is provided then we strip this out - if ( attrs.stripBr && html == '
' ) { + if (attrs.stripBr && html === '
') { html = ''; } ngModel.$setViewValue(html); @@ -25345,7 +30430,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. it('should data-bind and become invalid', function() { - if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { + if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') { // SafariDriver can't handle contenteditable // and Firefox driver can't clear contenteditables very well return; @@ -25365,8 +30450,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { +NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate']; +function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. @@ -25386,40 +30471,61 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$pending = undefined; // keep pending keys here this.$name = $interpolate($attr.name || '', false)($scope); this.$$parentForm = nullFormCtrl; + this.$options = defaultModelOptions; + this.$$updateEvents = ''; + // Attach the correct context to the event handler function for updateOn + this.$$updateEventHandler = this.$$updateEventHandler.bind(this); + + this.$$parsedNgModel = $parse($attr.ngModel); + this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; + this.$$ngModelGet = this.$$parsedNgModel; + this.$$ngModelSet = this.$$parsedNgModelAssign; + this.$$pendingDebounce = null; + this.$$parserValid = undefined; + this.$$parserName = 'parse'; + + this.$$currentValidationRunId = 0; + + this.$$scope = $scope; + this.$$rootScope = $scope.$root; + this.$$attr = $attr; + this.$$element = $element; + this.$$animate = $animate; + this.$$timeout = $timeout; + this.$$parse = $parse; + this.$$q = $q; + this.$$exceptionHandler = $exceptionHandler; + + setupValidity(this); + setupModelWatcher(this); +} - var parsedNgModel = $parse($attr.ngModel), - parsedNgModelAssign = parsedNgModel.assign, - ngModelGet = parsedNgModel, - ngModelSet = parsedNgModelAssign, - pendingDebounce = null, - parserValid, - ctrl = this; - - this.$$setOptions = function(options) { - ctrl.$options = options; - if (options && options.getterSetter) { - var invokeModelGetter = $parse($attr.ngModel + '()'), - invokeModelSetter = $parse($attr.ngModel + '($$$p)'); - - ngModelGet = function($scope) { - var modelValue = parsedNgModel($scope); +NgModelController.prototype = { + $$initGetterSetters: function() { + if (this.$options.getOption('getterSetter')) { + var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'), + invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)'); + + this.$$ngModelGet = function($scope) { + var modelValue = this.$$parsedNgModel($scope); if (isFunction(modelValue)) { modelValue = invokeModelGetter($scope); } return modelValue; }; - ngModelSet = function($scope, newValue) { - if (isFunction(parsedNgModel($scope))) { - invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + this.$$ngModelSet = function($scope, newValue) { + if (isFunction(this.$$parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: newValue}); } else { - parsedNgModelAssign($scope, ctrl.$modelValue); + this.$$parsedNgModelAssign($scope, newValue); } }; - } else if (!parsedNgModel.assign) { - throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); + } else if (!this.$$parsedNgModel.assign) { + throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', + this.$$attr.ngModel, startingTag(this.$$element)); } - }; + }, + /** * @ngdoc method @@ -25437,11 +30543,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * the `$viewValue` are different from last time. * * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of - * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue` + * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue` * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be * invoked if you only change a property on the objects. */ - this.$render = noop; + $render: noop, /** * @ngdoc method @@ -25461,45 +30567,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value The value of the input to check for emptiness. * @returns {boolean} True if `value` is "empty". */ - this.$isEmpty = function(value) { + $isEmpty: function(value) { + // eslint-disable-next-line no-self-compare return isUndefined(value) || value === '' || value === null || value !== value; - }; - - var currentValidationRunId = 0; + }, - /** - * @ngdoc method - * @name ngModel.NgModelController#$setValidity - * - * @description - * Change the validity state, and notify the form. - * - * This method can be called within $parsers/$formatters or a custom validation implementation. - * However, in most cases it should be sufficient to use the `ngModel.$validators` and - * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. - * - * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned - * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` - * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), - * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. - * Skipped is used by Angular when validators do not run because of parse errors and - * when `$asyncValidators` do not run because any of the `$validators` failed. - */ - addSetValidityMethod({ - ctrl: this, - $element: $element, - set: function(object, property) { - object[property] = true; - }, - unset: function(object, property) { - delete object[property]; - }, - $animate: $animate - }); + $$updateEmptyClasses: function(value) { + if (this.$isEmpty(value)) { + this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS); + this.$$animate.addClass(this.$$element, EMPTY_CLASS); + } else { + this.$$animate.removeClass(this.$$element, EMPTY_CLASS); + this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS); + } + }, /** * @ngdoc method @@ -25512,12 +30593,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * state (`ng-pristine` class). A model is considered to be pristine when the control * has not been changed from when first compiled. */ - this.$setPristine = function() { - ctrl.$dirty = false; - ctrl.$pristine = true; - $animate.removeClass($element, DIRTY_CLASS); - $animate.addClass($element, PRISTINE_CLASS); - }; + $setPristine: function() { + this.$dirty = false; + this.$pristine = true; + this.$$animate.removeClass(this.$$element, DIRTY_CLASS); + this.$$animate.addClass(this.$$element, PRISTINE_CLASS); + }, /** * @ngdoc method @@ -25530,13 +30611,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed * from when first compiled. */ - this.$setDirty = function() { - ctrl.$dirty = true; - ctrl.$pristine = false; - $animate.removeClass($element, PRISTINE_CLASS); - $animate.addClass($element, DIRTY_CLASS); - ctrl.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$dirty = true; + this.$pristine = false; + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -25550,11 +30631,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * by default, however this function can be used to restore that state if the model has * already been touched by the user. */ - this.$setUntouched = function() { - ctrl.$touched = false; - ctrl.$untouched = true; - $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); - }; + $setUntouched: function() { + this.$touched = false; + this.$untouched = true; + this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }, /** * @ngdoc method @@ -25567,11 +30648,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * touched state (`ng-touched` class). A model is considered to be touched when the user has * first focused the control element and then shifted focus away from the control (blur event). */ - this.$setTouched = function() { - ctrl.$touched = true; - ctrl.$untouched = false; - $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); - }; + $setTouched: function() { + this.$touched = true; + this.$untouched = false; + this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }, /** * @ngdoc method @@ -25579,33 +30660,34 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * @description * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, - * which may be caused by a pending debounced event or because the input is waiting for a some + * which may be caused by a pending debounced event or because the input is waiting for some * future event. * * If you have an input that uses `ng-model-options` to set up debounced updates or updates that - * depend on special events such as blur, you can have a situation where there is a period when - * the `$viewValue` is out of sync with the ngModel's `$modelValue`. + * depend on special events such as `blur`, there can be a period when the `$viewValue` is out of + * sync with the ngModel's `$modelValue`. * * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update * and reset the input to the last committed view value. * * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue` - * programmatically before these debounced/future events have resolved/occurred, because Angular's + * programmatically before these debounced/future events have resolved/occurred, because AngularJS's * dirty checking mechanism is not able to tell whether the model has actually changed or not. * * The `$rollbackViewValue()` method should be called before programmatically changing the model of an * input which may have such events pending. This is important in order to make sure that the * input field will be updated with the new model value and any pending operations are cancelled. * + * @example * * * angular.module('cancel-update-example', []) * * .controller('CancelUpdateController', ['$scope', function($scope) { - * $scope.model = {}; + * $scope.model = {value1: '', value2: ''}; * * $scope.setEmpty = function(e, value, rollback) { - * if (e.keyCode == 27) { + * if (e.keyCode === 27) { * e.preventDefault(); * if (rollback) { * $scope.myForm[value].$rollbackViewValue(); @@ -25617,8 +30699,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * *
- *

Both of these inputs are only updated if they are blurred. Hitting escape should - * empty them. Follow these steps and observe the difference:

+ *

Both of these inputs are only updated if they are blurred. Hitting escape should + * empty them. Follow these steps and observe the difference:

*
    *
  1. Type something in the input. You will see that the model is not yet updated
  2. *
  3. Press the Escape key. @@ -25635,17 +30717,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * *
    *
    - *

    Without $rollbackViewValue():

    - * - * value1: "{{ model.value1 }}" + *

    Without $rollbackViewValue():

    + * + * value1: "{{ model.value1 }}" *
    * *
    - *

    With $rollbackViewValue():

    - * - * value2: "{{ model.value2 }}" + *

    With $rollbackViewValue():

    + * + * value2: "{{ model.value2 }}" *
    *
    *
@@ -25661,11 +30743,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
*
*/ - this.$rollbackViewValue = function() { - $timeout.cancel(pendingDebounce); - ctrl.$viewValue = ctrl.$$lastCommittedViewValue; - ctrl.$render(); - }; + $rollbackViewValue: function() { + this.$$timeout.cancel(this.$$pendingDebounce); + this.$viewValue = this.$$lastCommittedViewValue; + this.$render(); + }, /** * @ngdoc method @@ -25679,45 +30761,47 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * If the validity changes to valid, it will set the model to the last available valid * `$modelValue`, i.e. either the last parsed value or the last value set from the scope. */ - this.$validate = function() { + $validate: function() { + // ignore $validate before model is initialized - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + if (isNumberNaN(this.$modelValue)) { return; } - var viewValue = ctrl.$$lastCommittedViewValue; + var viewValue = this.$$lastCommittedViewValue; // Note: we use the $$rawModelValue as $modelValue might have been // set to undefined during a view -> model update that found validation // errors. We can't parse the view here, since that could change // the model although neither viewValue nor the model on the scope changed - var modelValue = ctrl.$$rawModelValue; + var modelValue = this.$$rawModelValue; - var prevValid = ctrl.$valid; - var prevModelValue = ctrl.$modelValue; + var prevValid = this.$valid; + var prevModelValue = this.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + var allowInvalid = this.$options.getOption('allowInvalid'); - ctrl.$$runValidators(modelValue, viewValue, function(allValid) { + var that = this; + this.$$runValidators(modelValue, viewValue, function(allValid) { // If there was no change in validity, don't update the model // This prevents changing an invalid modelValue to undefined if (!allowInvalid && prevValid !== allValid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } }); + }, - }; - - this.$$runValidators = function(modelValue, viewValue, doneCallback) { - currentValidationRunId++; - var localValidationRunId = currentValidationRunId; + $$runValidators: function(modelValue, viewValue, doneCallback) { + this.$$currentValidationRunId++; + var localValidationRunId = this.$$currentValidationRunId; + var that = this; // check parser error if (!processParseErrors()) { @@ -25731,34 +30815,36 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ processAsyncValidators(); function processParseErrors() { - var errorKey = ctrl.$$parserName || 'parse'; - if (isUndefined(parserValid)) { + var errorKey = that.$$parserName; + + if (isUndefined(that.$$parserValid)) { setValidity(errorKey, null); } else { - if (!parserValid) { - forEach(ctrl.$validators, function(v, name) { + if (!that.$$parserValid) { + forEach(that.$validators, function(v, name) { setValidity(name, null); }); - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); } + // Set the parse error last, to prevent unsetting it, should a $validators key == parserName - setValidity(errorKey, parserValid); - return parserValid; + setValidity(errorKey, that.$$parserValid); + return that.$$parserValid; } return true; } function processSyncValidators() { var syncValidatorsValid = true; - forEach(ctrl.$validators, function(validator, name) { - var result = validator(modelValue, viewValue); + forEach(that.$validators, function(validator, name) { + var result = Boolean(validator(modelValue, viewValue)); syncValidatorsValid = syncValidatorsValid && result; setValidity(name, result); }); if (!syncValidatorsValid) { - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); return false; @@ -25769,16 +30855,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ function processAsyncValidators() { var validatorPromises = []; var allValid = true; - forEach(ctrl.$asyncValidators, function(validator, name) { + forEach(that.$asyncValidators, function(validator, name) { var promise = validator(modelValue, viewValue); if (!isPromiseLike(promise)) { throw ngModelMinErr('nopromise', - "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); + 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise); } setValidity(name, undefined); validatorPromises.push(promise.then(function() { setValidity(name, true); - }, function(error) { + }, function() { allValid = false; setValidity(name, false); })); @@ -25786,25 +30872,25 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (!validatorPromises.length) { validationDone(true); } else { - $q.all(validatorPromises).then(function() { + that.$$q.all(validatorPromises).then(function() { validationDone(allValid); }, noop); } } function setValidity(name, isValid) { - if (localValidationRunId === currentValidationRunId) { - ctrl.$setValidity(name, isValid); + if (localValidationRunId === that.$$currentValidationRunId) { + that.$setValidity(name, isValid); } } function validationDone(allValid) { - if (localValidationRunId === currentValidationRunId) { + if (localValidationRunId === that.$$currentValidationRunId) { doneCallback(allValid); } } - }; + }, /** * @ngdoc method @@ -25817,83 +30903,91 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - this.$commitViewValue = function() { - var viewValue = ctrl.$viewValue; + $commitViewValue: function() { + var viewValue = this.$viewValue; - $timeout.cancel(pendingDebounce); + this.$$timeout.cancel(this.$$pendingDebounce); // If the view value has not changed then we should just exit, except in the case where there is // a native validator on the element. In this case the validation state may have changed even though // the viewValue has stayed empty. - if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) { return; } - ctrl.$$lastCommittedViewValue = viewValue; + this.$$updateEmptyClasses(viewValue); + this.$$lastCommittedViewValue = viewValue; // change to dirty - if (ctrl.$pristine) { + if (this.$pristine) { this.$setDirty(); } this.$$parseAndValidate(); - }; + }, - this.$$parseAndValidate = function() { - var viewValue = ctrl.$$lastCommittedViewValue; + $$parseAndValidate: function() { + var viewValue = this.$$lastCommittedViewValue; var modelValue = viewValue; - parserValid = isUndefined(modelValue) ? undefined : true; + var that = this; + + this.$$parserValid = isUndefined(modelValue) ? undefined : true; - if (parserValid) { - for (var i = 0; i < ctrl.$parsers.length; i++) { - modelValue = ctrl.$parsers[i](modelValue); + // Reset any previous parse error + this.$setValidity(this.$$parserName, null); + this.$$parserName = 'parse'; + + if (this.$$parserValid) { + for (var i = 0; i < this.$parsers.length; i++) { + modelValue = this.$parsers[i](modelValue); if (isUndefined(modelValue)) { - parserValid = false; + this.$$parserValid = false; break; } } } - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { - // ctrl.$modelValue has not been touched yet... - ctrl.$modelValue = ngModelGet($scope); + if (isNumberNaN(this.$modelValue)) { + // this.$modelValue has not been touched yet... + this.$modelValue = this.$$ngModelGet(this.$$scope); } - var prevModelValue = ctrl.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; - ctrl.$$rawModelValue = modelValue; + var prevModelValue = this.$modelValue; + var allowInvalid = this.$options.getOption('allowInvalid'); + this.$$rawModelValue = modelValue; if (allowInvalid) { - ctrl.$modelValue = modelValue; + this.$modelValue = modelValue; writeToModelIfNeeded(); } // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. // This can happen if e.g. $setViewValue is called from inside a parser - ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) { if (!allowInvalid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; writeToModelIfNeeded(); } }); function writeToModelIfNeeded() { - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } - }; + }, - this.$$writeModelToScope = function() { - ngModelSet($scope, ctrl.$modelValue); - forEach(ctrl.$viewChangeListeners, function(listener) { + $$writeModelToScope: function() { + this.$$ngModelSet(this.$$scope, this.$modelValue); + forEach(this.$viewChangeListeners, function(listener) { try { listener(); } catch (e) { - $exceptionHandler(e); + // eslint-disable-next-line no-invalid-this + this.$$exceptionHandler(e); } - }); - }; + }, this); + }, /** * @ngdoc method @@ -25909,9 +31003,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers` * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged - * value sent directly for processing, finally to be applied to `$modelValue` and then the - * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners, - * in the `$viewChangeListeners` list, are called. + * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and + * `$asyncValidators` are called and the value is applied to `$modelValue`. + * Finally, the value is set to the **expression** specified in the `ng-model` attribute and + * all the registered change listeners, in the `$viewChangeListeners` list are called. * * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` * and the `default` trigger is not listed, all those actions will remain pending until one of the @@ -25926,7 +31021,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * However, custom controls might also pass objects to this method. In this case, we should make * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not * perform a deep watch of objects, it only looks for a change of identity. If you only change - * the property of the object then ngModel will not realise that the object has changed and + * the property of the object then ngModel will not realize that the object has changed and * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should * not change properties of the copy once it has been passed to `$setViewValue`. * Otherwise you may cause the model value on the scope to change incorrectly. @@ -25942,46 +31037,242 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * to update the DOM, and finally call `$validate` on it. *
* - * @param {*} value value from the view. - * @param {string} trigger Event that triggered the update. + * @param {*} value value from the view. + * @param {string} trigger Event that triggered the update. + */ + $setViewValue: function(value, trigger) { + this.$viewValue = value; + if (this.$options.getOption('updateOnDefault')) { + this.$$debounceViewValueCommit(trigger); + } + }, + + $$debounceViewValueCommit: function(trigger) { + var debounceDelay = this.$options.getOption('debounce'); + + if (isNumber(debounceDelay[trigger])) { + debounceDelay = debounceDelay[trigger]; + } else if (isNumber(debounceDelay['default']) && + this.$options.getOption('updateOn').indexOf(trigger) === -1 + ) { + debounceDelay = debounceDelay['default']; + } else if (isNumber(debounceDelay['*'])) { + debounceDelay = debounceDelay['*']; + } + + this.$$timeout.cancel(this.$$pendingDebounce); + var that = this; + if (debounceDelay > 0) { // this fails if debounceDelay is an object + this.$$pendingDebounce = this.$$timeout(function() { + that.$commitViewValue(); + }, debounceDelay); + } else if (this.$$rootScope.$$phase) { + this.$commitViewValue(); + } else { + this.$$scope.$apply(function() { + that.$commitViewValue(); + }); + } + }, + + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$overrideModelOptions + * + * @description + * + * Override the current model options settings programmatically. + * + * The previous `ModelOptions` value will not be modified. Instead, a + * new `ModelOptions` object will inherit from the previous one overriding + * or inheriting settings that are defined in the given parameter. + * + * See {@link ngModelOptions} for information about what options can be specified + * and how model option inheritance works. + * + *
+ * **Note:** this function only affects the options set on the `ngModelController`, + * and not the options on the {@link ngModelOptions} directive from which they might have been + * obtained initially. + *
+ * + *
+ * **Note:** it is not possible to override the `getterSetter` option. + *
+ * + * @param {Object} options a hash of settings to override the previous options + * + */ + $overrideModelOptions: function(options) { + this.$options = this.$options.createChild(options); + this.$$setUpdateOnEvents(); + }, + + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$processModelValue + + * @description + * + * Runs the model -> view pipeline on the current + * {@link ngModel.NgModelController#$modelValue $modelValue}. + * + * The following actions are performed by this method: + * + * - the `$modelValue` is run through the {@link ngModel.NgModelController#$formatters $formatters} + * and the result is set to the {@link ngModel.NgModelController#$viewValue $viewValue} + * - the `ng-empty` or `ng-not-empty` class is set on the element + * - if the `$viewValue` has changed: + * - {@link ngModel.NgModelController#$render $render} is called on the control + * - the {@link ngModel.NgModelController#$validators $validators} are run and + * the validation status is set. + * + * This method is called by ngModel internally when the bound scope value changes. + * Application developers usually do not have to call this function themselves. + * + * This function can be used when the `$viewValue` or the rendered DOM value are not correctly + * formatted and the `$modelValue` must be run through the `$formatters` again. + * + * @example + * Consider a text input with an autocomplete list (for fruit), where the items are + * objects with a name and an id. + * A user enters `ap` and then selects `Apricot` from the list. + * Based on this, the autocomplete widget will call `$setViewValue({name: 'Apricot', id: 443})`, + * but the rendered value will still be `ap`. + * The widget can then call `ctrl.$processModelValue()` to run the model -> view + * pipeline again, which formats the object to the string `Apricot`, + * then updates the `$viewValue`, and finally renders it in the DOM. + * + * + +
+
+ Search Fruit: + +
+
+ Model:
+
{{selectedFruit | json}}
+
+
+
+ + angular.module('inputExample', []) + .controller('inputController', function($scope) { + $scope.items = [ + {name: 'Apricot', id: 443}, + {name: 'Clementine', id: 972}, + {name: 'Durian', id: 169}, + {name: 'Jackfruit', id: 982}, + {name: 'Strawberry', id: 863} + ]; + }) + .component('basicAutocomplete', { + bindings: { + items: '<', + onSelect: '&' + }, + templateUrl: 'autocomplete.html', + controller: function($element, $scope) { + var that = this; + var ngModel; + + that.$postLink = function() { + ngModel = $element.find('input').controller('ngModel'); + + ngModel.$formatters.push(function(value) { + return (value && value.name) || value; + }); + + ngModel.$parsers.push(function(value) { + var match = value; + for (var i = 0; i < that.items.length; i++) { + if (that.items[i].name === value) { + match = that.items[i]; + break; + } + } + + return match; + }); + }; + + that.selectItem = function(item) { + ngModel.$setViewValue(item); + ngModel.$processModelValue(); + that.onSelect({item: item}); + }; + } + }); + + +
+ +
    +
  • + +
  • +
+
+
+ *
+ * */ - this.$setViewValue = function(value, trigger) { - ctrl.$viewValue = value; - if (!ctrl.$options || ctrl.$options.updateOnDefault) { - ctrl.$$debounceViewValueCommit(trigger); + $processModelValue: function() { + var viewValue = this.$$format(); + + if (this.$viewValue !== viewValue) { + this.$$updateEmptyClasses(viewValue); + this.$viewValue = this.$$lastCommittedViewValue = viewValue; + this.$render(); + // It is possible that model and view value have been updated during render + this.$$runValidators(this.$modelValue, this.$viewValue, noop); } - }; + }, - this.$$debounceViewValueCommit = function(trigger) { - var debounceDelay = 0, - options = ctrl.$options, - debounce; + /** + * This method is called internally to run the $formatters on the $modelValue + */ + $$format: function() { + var formatters = this.$formatters, + idx = formatters.length; - if (options && isDefined(options.debounce)) { - debounce = options.debounce; - if (isNumber(debounce)) { - debounceDelay = debounce; - } else if (isNumber(debounce[trigger])) { - debounceDelay = debounce[trigger]; - } else if (isNumber(debounce['default'])) { - debounceDelay = debounce['default']; - } + var viewValue = this.$modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); } - $timeout.cancel(pendingDebounce); - if (debounceDelay) { - pendingDebounce = $timeout(function() { - ctrl.$commitViewValue(); - }, debounceDelay); - } else if ($rootScope.$$phase) { - ctrl.$commitViewValue(); - } else { - $scope.$apply(function() { - ctrl.$commitViewValue(); - }); + return viewValue; + }, + + /** + * This method is called internally when the bound scope value changes. + */ + $$setModelValue: function(modelValue) { + this.$modelValue = this.$$rawModelValue = modelValue; + this.$$parserValid = undefined; + this.$processModelValue(); + }, + + $$setUpdateOnEvents: function() { + if (this.$$updateEvents) { + this.$$element.off(this.$$updateEvents, this.$$updateEventHandler); } - }; + this.$$updateEvents = this.$options.getOption('updateOn'); + if (this.$$updateEvents) { + this.$$element.on(this.$$updateEvents, this.$$updateEventHandler); + } + }, + + $$updateEventHandler: function(ev) { + this.$$debounceViewValueCommit(ev && ev.type); + } +}; + +function setupModelWatcher(ctrl) { // model -> value // Note: we cannot use a normal scope.$watch as we want to detect the following: // 1. scope value is 'a' @@ -25990,44 +31281,63 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ // -> scope value did not change since the last digest as // ng-change executes in apply phase // 4. view should be changed back to 'a' - $scope.$watch(function ngModelWatch() { - var modelValue = ngModelGet($scope); + ctrl.$$scope.$watch(function ngModelWatch(scope) { + var modelValue = ctrl.$$ngModelGet(scope); // if scope model value and ngModel value are out of sync - // TODO(perf): why not move this to the action fn? + // This cannot be moved to the action function, because it would not catch the + // case where the model is changed in the ngChange function or the model setter if (modelValue !== ctrl.$modelValue && - // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator - (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) + // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + // eslint-disable-next-line no-self-compare + (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) ) { - ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - parserValid = undefined; - - var formatters = ctrl.$formatters, - idx = formatters.length; - - var viewValue = modelValue; - while (idx--) { - viewValue = formatters[idx](viewValue); - } - if (ctrl.$viewValue !== viewValue) { - ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; - ctrl.$render(); - - ctrl.$$runValidators(modelValue, viewValue, noop); - } + ctrl.$$setModelValue(modelValue); } return modelValue; }); -}]; +} + +/** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * classes and can be bound to as `{{ someForm.someControl.$error.myError }}`. + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by AngularJS when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ +addSetValidityMethod({ + clazz: NgModelController, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + } +}); /** * @ngdoc directive * @name ngModel - * - * @element input + * @restrict A * @priority 1 + * @param {expression} ngModel assignable {@link guide/expression Expression} to bind to. * * @description * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a @@ -26040,7 +31350,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * require. * - Providing validation behavior (i.e. required, number, email, url). * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, + * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations. * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the @@ -26068,7 +31379,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * - * # Complex Models (objects or collections) + * ## Complex Models (objects or collections) * * By default, `ngModel` watches the model by reference, not value. This is important to know when * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the @@ -26084,7 +31395,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * first level of the object (or only changing the properties of an item in the collection if it's an array) will still * not trigger a re-rendering of the model. * - * # CSS classes + * ## CSS classes * The following CSS classes are added and removed on the associated input/select/textarea element * depending on the validity of the model. * @@ -26097,13 +31408,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - `ng-touched`: the control has been blurred * - `ng-untouched`: the control hasn't been blurred * - `ng-pending`: any `$asyncValidators` are unfulfilled + * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined + * by the {@link ngModel.NgModelController#$isEmpty} method + * - `ng-not-empty`: the view contains a non-empty value * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * - * ## Animation Hooks - * + * @animations * Animations within models are triggered when any of the associated CSS classes are added and removed - * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, + * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`, * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. * The animations that are triggered within ngModel are similar to how they work in ngClass and * animations can be hooked into using CSS transitions, keyframes as well as JS animations. @@ -26125,7 +31438,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * * @example - * + * ### Basic Usage + * -
-
-
- {{text}} -
-
- - it('should have transcluded', function() { - var titleElement = element(by.model('title')); - titleElement.clear(); - titleElement.sendKeys('TITLE'); - var textElement = element(by.model('text')); - textElement.clear(); - textElement.sendKeys('TEXT'); - expect(element(by.binding('title')).getText()).toEqual('TITLE'); - expect(element(by.binding('text')).getText()).toEqual('TEXT'); - }); - -
+ * ### Basic transclusion + * This example demonstrates basic transclusion of content into a component directive. + * + * + * + *
+ *
+ *
+ * {{text}} + *
+ *
+ * + * it('should have transcluded', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.binding('title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * }); + * + *
+ * + * @example + * ### Transclude fallback content + * This example shows how to use `NgTransclude` with fallback content, that + * is displayed if no transcluded content is provided. * + * + * + * + * + * + * + * + * Button2 + * + * + * + * it('should have different transclude element content', function() { + * expect(element(by.id('fallback')).getText()).toBe('Button1'); + * expect(element(by.id('modified')).getText()).toBe('Button2'); + * }); + * + * + * + * @example + * ### Multi-slot transclusion + * This example demonstrates using multi-slot transclusion in a component directive. + * + * + * + *
+ *
+ *
+ * + * {{title}} + *

{{text}}

+ *
+ *
+ *
+ * + * angular.module('multiSlotTranscludeExample', []) + * .directive('pane', function() { + * return { + * restrict: 'E', + * transclude: { + * 'title': '?paneTitle', + * 'body': 'paneBody', + * 'footer': '?paneFooter' + * }, + * template: '
' + + * '
Fallback Title
' + + * '
' + + * '' + + * '
' + * }; + * }) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.title = 'Lorem Ipsum'; + * $scope.link = 'https://google.com'; + * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + * }]); + *
+ * + * it('should have transcluded the title and the body', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.css('.title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer'); + * }); + * + *
*/ -var ngTranscludeDirective = ngDirective({ - restrict: 'EAC', - link: function($scope, $element, $attrs, controller, $transclude) { - if (!$transclude) { - throw minErr('ngTransclude')('orphan', - 'Illegal use of ngTransclude directive in the template! ' + - 'No parent directive that requires a transclusion found. ' + - 'Element: {0}', - startingTag($element)); - } - - $transclude(function(clone) { - $element.empty(); - $element.append(clone); - }); - } -}); +var ngTranscludeMinErr = minErr('ngTransclude'); +var ngTranscludeDirective = ['$compile', function($compile) { + return { + restrict: 'EAC', + compile: function ngTranscludeCompile(tElement) { + + // Remove and cache any original content to act as a fallback + var fallbackLinkFn = $compile(tElement.contents()); + tElement.empty(); + + return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) { + + if (!$transclude) { + throw ngTranscludeMinErr('orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag($element)); + } + + + // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default + if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) { + $attrs.ngTransclude = ''; + } + var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot; + + // If the slot is required and no transclusion content is provided then this call will throw an error + $transclude(ngTranscludeCloneAttachFn, null, slotName); + + // If the slot is optional and no transclusion content is provided then use the fallback content + if (slotName && !$transclude.isSlotFilled(slotName)) { + useFallbackContent(); + } + + function ngTranscludeCloneAttachFn(clone, transcludedScope) { + if (clone.length && notWhitespace(clone)) { + $element.append(clone); + } else { + useFallbackContent(); + // There is nothing linked against the transcluded scope since no content was available, + // so it should be safe to clean up the generated scope. + transcludedScope.$destroy(); + } + } + + function useFallbackContent() { + // Since this is the fallback content rather than the transcluded content, + // we link against the scope of this directive rather than the transcluded scope + fallbackLinkFn($scope, function(clone) { + $element.append(clone); + }); + } + + function notWhitespace(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + var node = nodes[i]; + if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) { + return true; + } + } + } + }; + } + }; +}]; /** * @ngdoc directive @@ -28809,116 +35063,299 @@ var ngTranscludeDirective = ngDirective({ * ` + + Load inlined template +
+
+ + it('should load template defined inside script tag', function() { + element(by.css('#tpl-link')).click(); + expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); + }); + +
+ */ +var scriptDirective = ['$templateCache', function($templateCache) { + return { + restrict: 'E', + terminal: true, + compile: function(element, attr) { + if (attr.type === 'text/ng-template') { + var templateUrl = attr.id, + text = element[0].text; + + $templateCache.put(templateUrl, text); + } + } + }; +}]; + +/* exported selectDirective, optionDirective */ + +var noopNgModelController = { $setViewValue: noop, $render: noop }; + +function setOptionSelectedStatus(optionEl, value) { + optionEl.prop('selected', value); + /** + * When unselecting an option, setting the property to null / false should be enough + * However, screenreaders might react to the selected attribute instead, see + * https://github.com/angular/angular.js/issues/14419 + * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false + * or null + */ + optionEl.attr('selected', value); +} + +/** + * @ngdoc type + * @name select.SelectController + * + * @description + * The controller for the {@link ng.select select} directive. The controller exposes + * a few utility methods that can be used to augment the behavior of a regular or an + * {@link ng.ngOptions ngOptions} select element. + * + * @example + * ### Set a custom error when the unknown option is selected + * + * This example sets a custom error "unknownValue" on the ngModelController + * when the select element's unknown option is selected, i.e. when the model is set to a value + * that is not matched by any option. + * + * + * + *
+ *
+ *
+ *
+ * + * Error: The current model doesn't match any option
+ * + *
+ *
+ *
+ *
+ * + * angular.module('staticSelect', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.selected = null; + * + * $scope.forceUnknownOption = function() { + * $scope.selected = 'nonsense'; + * }; + * }]) + * .directive('unknownValueError', function() { + * return { + * require: ['ngModel', 'select'], + * link: function(scope, element, attrs, ctrls) { + * var ngModelCtrl = ctrls[0]; + * var selectCtrl = ctrls[1]; + * + * ngModelCtrl.$validators.unknownValue = function(modelValue, viewValue) { + * if (selectCtrl.$isUnknownOptionSelected()) { + * return false; + * } + * + * return true; + * }; + * } + * + * }; + * }); + * + *
+ * + * + * @example + * ### Set the "required" error when the unknown option is selected. + * + * By default, the "required" error on the ngModelController is only set on a required select + * when the empty option is selected. This example adds a custom directive that also sets the + * error when the unknown option is selected. + * + * + * + *
+ *
+ *
+ *
+ * Error: Please select a value
+ * + *
+ *
+ *
+ *
+ * + * angular.module('staticSelect', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.selected = null; + * + * $scope.forceUnknownOption = function() { + * $scope.selected = 'nonsense'; + * }; + * }]) + * .directive('unknownValueRequired', function() { + * return { + * priority: 1, // This directive must run after the required directive has added its validator + * require: ['ngModel', 'select'], + * link: function(scope, element, attrs, ctrls) { + * var ngModelCtrl = ctrls[0]; + * var selectCtrl = ctrls[1]; + * + * var originalRequiredValidator = ngModelCtrl.$validators.required; + * + * ngModelCtrl.$validators.required = function() { + * if (attrs.required && selectCtrl.$isUnknownOptionSelected()) { + * return false; + * } * - * @example - - - + * return originalRequiredValidator.apply(this, arguments); + * }; + * } + * }; + * }); + * + * + * it('should show the error message when the unknown option is selected', function() { - Load inlined template -
-
- - it('should load template defined inside script tag', function() { - element(by.css('#tpl-link')).click(); - expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); - }); - -
- */ -var scriptDirective = ['$templateCache', function($templateCache) { - return { - restrict: 'E', - terminal: true, - compile: function(element, attr) { - if (attr.type == 'text/ng-template') { - var templateUrl = attr.id, - text = element[0].text; + var error = element(by.className('error')); - $templateCache.put(templateUrl, text); - } - } - }; -}]; + expect(error.getText()).toBe('Error: Please select a value'); -var noopNgModelController = { $setViewValue: noop, $render: noop }; + element(by.cssContainingText('option', 'Option 1')).click(); -function chromeHack(optionElement) { - // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459 - // Adding an
+ *
+ * + * */ var SelectController = - ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { + ['$element', '$scope', /** @this */ function($element, $scope) { var self = this, - optionsMap = new HashMap(); + optionsMap = new NgMap(); + + self.selectValueMap = {}; // Keys are the hashed values, values the original values // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors self.ngModelCtrl = noopNgModelController; + self.multiple = false; // The "unknown" option is one that is prepended to the list if the viewValue // does not match any of the options. When it is rendered the value of the unknown // option is '? XXX ?' where XXX is the hashKey of the value that is not known. // + // Support: IE 9 only // We can't just jqLite('