From 6f87f886902a666b0af591b76931904d1fc6a208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 16 Jan 2017 21:31:55 +0100 Subject: [PATCH] tech(lib upgrade): upgraded angularjs from 1.5.8 to 1.6.1, closes #7274 --- CHANGELOG.md | 4 + bower.json | 8 +- public/app/app.ts | 2 + public/vendor/angular-mocks/.bower.json | 12 +- public/vendor/angular-mocks/angular-mocks.js | 514 +- public/vendor/angular-mocks/bower.json | 4 +- public/vendor/angular-mocks/package.json | 2 +- public/vendor/angular-route/.bower.json | 12 +- public/vendor/angular-route/angular-route.js | 291 +- .../vendor/angular-route/angular-route.min.js | 23 +- .../angular-route/angular-route.min.js.map | 6 +- public/vendor/angular-route/bower.json | 4 +- public/vendor/angular-route/package.json | 2 +- public/vendor/angular-sanitize/.bower.json | 12 +- .../angular-sanitize/angular-sanitize.js | 103 +- .../angular-sanitize/angular-sanitize.min.js | 16 +- .../angular-sanitize.min.js.map | 2 +- public/vendor/angular-sanitize/bower.json | 4 +- public/vendor/angular-sanitize/package.json | 2 +- public/vendor/angular/.bower.json | 10 +- public/vendor/angular/angular.js | 5810 ++++++++++------- public/vendor/angular/angular.min.js | 638 +- public/vendor/angular/angular.min.js.gzip | Bin 56905 -> 58025 bytes public/vendor/angular/angular.min.js.map | 6 +- public/vendor/angular/bower.json | 2 +- public/vendor/angular/package.json | 2 +- 26 files changed, 4577 insertions(+), 2914 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d36d1be06..a3c93731a06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ * **SingleStat**: Implements diff aggregation method for singlestat [#7234](https://github.com/grafana/grafana/issues/7234), thx [@oliverpool](https://github.com/oliverpool) * **Dataproxy**: Added setting to enable more verbose logging in dataproxy [#7209](https://github.com/grafana/grafana/pull/7209), thx [@Ricky-N](https://github.com/Ricky-N) +## Tech + +* **Library Upgrade**: Upgraded angularjs from 1.5.8 to 1.6.1 [#7274](https://github.com/grafana/grafana/issues/7274) + ## Bugfixes * **Alerting**: Fixes missing support for no_data and execution error when testing alerts [#7149](https://github.com/grafana/grafana/issues/7149) * **Dashboard**: Avoid duplicate data in dashboard json for panels with alerts [#7256](https://github.com/grafana/grafana/pull/7256) diff --git a/bower.json b/bower.json index 719c6f2b41b..1f7136128aa 100644 --- a/bower.json +++ b/bower.json @@ -15,10 +15,10 @@ "dependencies": { "jquery": "3.1.0", "lodash": "4.15.0", - "angular": "1.5.8", - "angular-route": "1.5.8", - "angular-mocks": "1.5.8", - "angular-sanitize": "1.5.8", + "angular": "1.6.1", + "angular-route": "1.6.1", + "angular-mocks": "1.6.1", + "angular-sanitize": "1.6.1", "angular-native-dragdrop": "1.2.2", "angular-bindonce": "0.3.3", "clipboard": "^1.5.16" diff --git a/public/app/app.ts b/public/app/app.ts index 22431a5110c..3603e793d5c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -44,6 +44,8 @@ export class GrafanaApp { moment.locale(config.bootData.user.locale); app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => { + // pre assing bindings before constructor calls + $compileProvider.preAssignBindingsEnabled(true); if (config.buildInfo.env !== 'development') { $compileProvider.debugInfoEnabled(false); diff --git a/public/vendor/angular-mocks/.bower.json b/public/vendor/angular-mocks/.bower.json index 119ddaab59a..839e25a0f23 100644 --- a/public/vendor/angular-mocks/.bower.json +++ b/public/vendor/angular-mocks/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-mocks", - "version": "1.5.8", + "version": "1.6.1", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.5.8" + "angular": "1.6.1" }, "homepage": "https://github.com/angular/bower-angular-mocks", - "_release": "1.5.8", + "_release": "1.6.1", "_resolution": { "type": "version", - "tag": "v1.5.8", - "commit": "482eefcf6b03057c5fcddb9750e460f458ee3487" + "tag": "v1.6.1", + "commit": "d8ac5a2016c9714b7c87284d21a34648036e8eea" }, "_source": "https://github.com/angular/bower-angular-mocks.git", - "_target": "1.5.8", + "_target": "1.6.1", "_originalSource": "angular-mocks" } \ No newline at end of file diff --git a/public/vendor/angular-mocks/angular-mocks.js b/public/vendor/angular-mocks/angular-mocks.js index 42f19b7aeab..41f67ca858f 100644 --- a/public/vendor/angular-mocks/angular-mocks.js +++ b/public/vendor/angular-mocks/angular-mocks.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.8 + * @license AngularJS v1.6.1 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -40,7 +40,7 @@ angular.mock.$Browser = function() { var self = this; this.isMock = true; - self.$$url = "http://server/"; + self.$$url = 'http://server/'; self.$$lastUrl = self.$$url; // used by url polling fn self.pollFns = []; @@ -252,19 +252,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!'); } }; @@ -414,8 +414,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')); } @@ -463,7 +463,7 @@ angular.mock.$IntervalProvider = function() { promise = deferred.promise; count = (angular.isDefined(count)) ? count : 0; - promise.then(null, null, (!hasParams) ? fn : function() { + promise.then(null, function() {}, (!hasParams) ? fn : function() { fn.apply(null, args); }); @@ -523,6 +523,7 @@ angular.mock.$IntervalProvider = function() { }); if (angular.isDefined(fnIndex)) { + repeatFns[fnIndex].deferred.promise.then(undefined, function() {}); repeatFns[fnIndex].deferred.reject('canceled'); repeatFns.splice(fnIndex, 1); return true; @@ -558,16 +559,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; @@ -650,9 +648,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 { @@ -758,7 +757,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'); }; }); @@ -767,7 +766,6 @@ angular.mock.TzDate = function(offset, timestamp) { //make "tzDateInstance instanceof Date" return true angular.mock.TzDate.prototype = Date.prototype; -/* jshint +W101 */ /** @@ -1215,7 +1213,7 @@ 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'); @@ -1357,7 +1355,11 @@ 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(handleTimeout); + } else { + $timeout(handleTimeout, timeout); + } } return handleResponse; @@ -1426,7 +1428,7 @@ 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. @@ -1448,6 +1450,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * the `requestHandler` object for possible overrides. */ $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) { @@ -1475,7 +1480,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @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 above. @@ -1490,7 +1495,7 @@ 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 {(Array)=} keys Array of keys to assign to regex matches in request url described above. @@ -1505,7 +1510,7 @@ 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 {(Array)=} keys Array of keys to assign to regex matches in request url described above. @@ -1520,7 +1525,7 @@ 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. @@ -1537,7 +1542,7 @@ 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. @@ -1554,7 +1559,7 @@ 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 @@ -1590,7 +1595,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { url = url .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { + .replace(/(\/)?:(\w+)([?*])?/g, function(_, slash, key, option) { var optional = option === '?' ? option : null; var star = option === '*' ? option : null; keys.push({ name: key, optional: !!optional }); @@ -1604,7 +1609,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { + ')' + (optional || ''); }) - .replace(/([\/$\*])/g, '\\$1'); + .replace(/([/$*])/g, '\\$1'); ret.regexp = new RegExp('^' + url, 'i'); return ret; @@ -1617,7 +1622,7 @@ 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 @@ -1640,6 +1645,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * the `requestHandler` object for possible overrides. */ $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) { @@ -1658,7 +1666,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @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 + * @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 {(Array)=} keys Array of keys to assign to regex matches in request url described above. @@ -1673,7 +1681,7 @@ 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 + * @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 {(Array)=} keys Array of keys to assign to regex matches in request url described above. @@ -1688,7 +1696,7 @@ 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 + * @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 {(Array)=} keys Array of keys to assign to regex matches in request url described above. @@ -1703,7 +1711,7 @@ 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 + * @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 @@ -1721,7 +1729,7 @@ 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 + * @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 @@ -1739,7 +1747,7 @@ 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 + * @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 @@ -1757,7 +1765,7 @@ 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 + * @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 {(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 @@ -1788,24 +1796,34 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @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. * - * @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). + * 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. 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); @@ -1847,7 +1865,8 @@ 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); } @@ -1873,18 +1892,35 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { function createShortMethods(prefix) { angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { $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, 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 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 MockHttpExpectation(method, url, data, headers, keys) { function getUrlParams(u) { @@ -1893,14 +1929,15 @@ function MockHttpExpectation(method, url, data, headers, keys) { } function compareUrl(u) { - return (url.slice(0, url.indexOf('?')) == u.slice(0, u.indexOf('?')) && getUrlParams(url).join() == getUrlParams(u).join()); + return (url.slice(0, url.indexOf('?')) === u.slice(0, u.indexOf('?')) && + getUrlParams(url).join() === getUrlParams(u).join()); } this.data = data; this.headers = headers; this.match = function(m, u, d, h) { - if (method != m) return false; + if (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; @@ -1911,7 +1948,7 @@ function MockHttpExpectation(method, url, data, headers, keys) { if (!url) return true; if (angular.isFunction(url.test)) return url.test(u); if (angular.isFunction(url)) return url(u); - return (url == u || compareUrl(u)); + return (url === u || compareUrl(u)); }; this.matchHeaders = function(h) { @@ -1927,6 +1964,7 @@ function MockHttpExpectation(method, url, data, headers, keys) { if (data && !angular.isString(data)) { return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d)); } + // eslint-disable-next-line eqeqeq return data == d; }; @@ -1958,7 +1996,7 @@ function MockHttpExpectation(method, url, data, headers, keys) { var obj = {}, key_value, key, queryStr = u.indexOf('?') > -1 ? u.substring(u.indexOf('?') + 1) - : ""; + : ''; angular.forEach(queryStr.split('&'), function(keyValue) { if (keyValue) { @@ -2025,7 +2063,7 @@ function MockXhr() { 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; }; @@ -2098,7 +2136,7 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $ function formatPendingTasksAsString(tasks) { var result = []; angular.forEach(tasks, function(task) { - result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); + result.push('{id: ' + task.id + ', time: ' + task.time + '}'); }); return result.join(', '); @@ -2153,6 +2191,10 @@ 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`}. * + * Depending on the value of + * {@link ng.$compileProvider#preAssignBindingsEnabled `preAssignBindingsEnabled()`}, the properties + * will be bound before or after invoking the constructor. + * * * ## Example * @@ -2171,18 +2213,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']); + * })); + * }); * }); * * ``` @@ -2194,44 +2242,61 @@ 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) + * `window` object (deprecated, 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 before invoking the constructor. This is used - * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @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. */ -angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { - return function(expression, locals, later, ident) { - if (later && typeof later === 'object') { - var instantiate = $delegate(expression, locals, true, ident); - angular.extend(instantiate.instance, later); +function createControllerDecorator(compileProvider) { + angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var preAssignBindingsEnabled = compileProvider.preAssignBindingsEnabled(); - var instance = instantiate(); - if (instance !== instantiate.instance) { - angular.extend(instance, later); + var instantiate = $delegate(expression, locals, true, ident); + if (preAssignBindingsEnabled) { + angular.extend(instantiate.instance, later); + } + + var instance = instantiate(); + if (!preAssignBindingsEnabled || instance !== instantiate.instance) { + angular.extend(instance, later); + } + + return instance; } + return $delegate(expression, locals, later, ident); + }; + }]; - 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. - *
+ * 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 @@ -2239,7 +2304,8 @@ angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { * @param {string=} ident Override the property name to use when attaching the controller to the scope. * @return {Object} Instance of requested controller. */ -angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compileProvider) { +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 @@ -2288,6 +2354,7 @@ angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compi * * [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"` @@ -2319,11 +2386,11 @@ angular.module('ngMock', ['ng']).provider({ $httpBackend: angular.mock.$HttpBackendProvider, $rootElement: angular.mock.$RootElementProvider, $componentController: angular.mock.$ComponentControllerProvider -}).config(['$provide', function($provide) { +}).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)); }]); /** @@ -2387,7 +2454,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * phones.push(phone); * return [200, phone, {}]; * }); - * $httpBackend.whenGET(/^\/templates\//).passThrough(); // Requests for templare are handled by the real server + * $httpBackend.whenGET(/^\/templates\//).passThrough(); // Requests for templates are handled by the real server * //... * }); * ``` @@ -2399,7 +2466,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * * var myApp = angular.module('myApp', []); * - * myApp.controller('main', function($http) { + * myApp.controller('MainCtrl', function MainCtrl($http) { * var ctrl = this; * * ctrl.phones = []; @@ -2441,7 +2508,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * }); * * - *
+ *
*
* * @@ -2465,9 +2532,10 @@ 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 @@ -2497,7 +2565,7 @@ 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 @@ -2514,7 +2582,7 @@ 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 @@ -2531,7 +2599,7 @@ 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 @@ -2548,9 +2616,10 @@ 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}. @@ -2566,9 +2635,10 @@ 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}. @@ -2584,9 +2654,10 @@ 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}. @@ -2602,7 +2673,7 @@ 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}. @@ -2654,6 +2725,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. * @@ -2662,7 +2734,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; @@ -2684,6 +2755,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. @@ -2694,7 +2766,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; @@ -2714,7 +2785,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { }]; -!(function(jasmineOrMocha) { +(function(jasmineOrMocha) { if (!jasmineOrMocha) { return; @@ -2809,7 +2880,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { * * You cannot call `sharedInjector()` from within a context already using `sharedInjector()`. * - * ## Example + * ## 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 @@ -2847,14 +2918,14 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { */ module.sharedInjector = function() { if (!(module.$$beforeAllHook && module.$$afterAllHook)) { - throw Error("sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll"); + throw Error('sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll'); } var initialized = false; - module.$$beforeAllHook(function() { + module.$$beforeAllHook(/** @this */ function() { if (injectorState.shared) { - injectorState.sharedError = Error("sharedInjector() cannot be called inside a context that has already called sharedInjector()"); + injectorState.sharedError = Error('sharedInjector() cannot be called inside a context that has already called sharedInjector()'); throw injectorState.sharedError; } initialized = true; @@ -2873,10 +2944,10 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { }; module.$$beforeEach = function() { - if (injectorState.shared && currentSpec && currentSpec != this) { + if (injectorState.shared && currentSpec && currentSpec !== this) { var state = currentSpec; currentSpec = this; - angular.forEach(["$injector","$modules","$providerInjector", "$injectorStrict"], function(k) { + angular.forEach(['$injector','$modules','$providerInjector', '$injectorStrict'], function(k) { currentSpec[k] = state[k]; state[k] = null; }); @@ -2967,7 +3038,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { * 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. * * ``` @@ -3031,7 +3102,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { - 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; @@ -3049,11 +3120,11 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { if (!errorForStack.stack) { try { throw errorForStack; - } catch (e) {} + } catch (e) { /* empty */ } } - return wasInjectorCreated() ? 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) { @@ -3066,7 +3137,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { 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); } }); @@ -3081,9 +3152,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { 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); @@ -3122,5 +3191,218 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { } })(window.jasmine || window.mocha); +'use strict'; + +(function() { + /** + * Triggers a browser event. Attempts to choose the right event if one is + * not specified. + * + * @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement + * @param {string} eventType Optional event type + * @param {Object=} eventData An optional object which contains additional event data (such as x,y + * coordinates, keys, etc...) that are passed into the event when triggered + */ + 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, false, true); + } else { + try { + evnt = new window.TransitionEvent(eventType, eventData); + } catch (e) { + evnt = window.document.createEvent('TransitionEvent'); + evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0); + } + } + } else if (/animationend/.test(eventType)) { + if (window.WebKitAnimationEvent) { + evnt = new window.WebKitAnimationEvent(eventType, eventData); + evnt.initEvent(eventType, false, true); + } else { + try { + evnt = new window.AnimationEvent(eventType, eventData); + } catch (e) { + evnt = window.document.createEvent('AnimationEvent'); + evnt.initAnimationEvent(eventType, null, 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 { + 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; + + var originalPreventDefault = evnt.preventDefault, + appWindow = element.ownerDocument.defaultView, + fakeProcessDefault = true, + finalProcessDefault, + angular = appWindow.angular || {}; + + // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208 + angular['ff-684208-preventDefault'] = false; + evnt.preventDefault = function() { + fakeProcessDefault = false; + return originalPreventDefault.apply(evnt, arguments); + }; + + if (!eventData.bubbles || supportsEventBubblingInDetachedTree() || isAttachedToDocument(element)) { + element.dispatchEvent(evnt); + } else { + triggerForPath(element, evnt); + } + + finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault); + + delete angular['ff-684208-preventDefault']; + + return finalProcessDefault; + }; + + 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/public/vendor/angular-mocks/bower.json b/public/vendor/angular-mocks/bower.json index 3a3d60a6b06..5cd129ac3eb 100644 --- a/public/vendor/angular-mocks/bower.json +++ b/public/vendor/angular-mocks/bower.json @@ -1,10 +1,10 @@ { "name": "angular-mocks", - "version": "1.5.8", + "version": "1.6.1", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.5.8" + "angular": "1.6.1" } } diff --git a/public/vendor/angular-mocks/package.json b/public/vendor/angular-mocks/package.json index 631e187985b..6661aafe4ac 100644 --- a/public/vendor/angular-mocks/package.json +++ b/public/vendor/angular-mocks/package.json @@ -1,6 +1,6 @@ { "name": "angular-mocks", - "version": "1.5.8", + "version": "1.6.1", "description": "AngularJS mocks for testing", "main": "angular-mocks.js", "scripts": { diff --git a/public/vendor/angular-route/.bower.json b/public/vendor/angular-route/.bower.json index 9639a728a0e..5b5ce26b9d3 100644 --- a/public/vendor/angular-route/.bower.json +++ b/public/vendor/angular-route/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-route", - "version": "1.5.8", + "version": "1.6.1", "license": "MIT", "main": "./angular-route.js", "ignore": [], "dependencies": { - "angular": "1.5.8" + "angular": "1.6.1" }, "homepage": "https://github.com/angular/bower-angular-route", - "_release": "1.5.8", + "_release": "1.6.1", "_resolution": { "type": "version", - "tag": "v1.5.8", - "commit": "e96eff424fdd9689061659603ca59470375bf024" + "tag": "v1.6.1", + "commit": "409c45cfc589d66457f7cbb11aa1fc47f8dbbf78" }, "_source": "https://github.com/angular/bower-angular-route.git", - "_target": "1.5.8", + "_target": "1.6.1", "_originalSource": "angular-route" } \ No newline at end of file diff --git a/public/vendor/angular-route/angular-route.js b/public/vendor/angular-route/angular-route.js index 6654d83afef..42e25cec361 100644 --- a/public/vendor/angular-route/angular-route.js +++ b/public/vendor/angular-route/angular-route.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.8 + * @license AngularJS v1.6.1 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -34,10 +34,11 @@ function shallowCopy(src, dst) { /* global shallowCopy: false */ -// There are necessary for `shallowCopy()` (included via `src/shallowCopy.js`). +// `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; /** * @ngdoc module @@ -54,14 +55,22 @@ var isObject; * *
*/ - /* global -ngRouteModule */ -var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider), - $routeMinErr = angular.$$minErr('ngRoute'); +/* global -ngRouteModule */ +var ngRouteModule = angular. + module('ngRoute', []). + 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 * @@ -76,6 +85,7 @@ var ngRouteModule = angular.module('ngRoute', ['ng']). function $RouteProvider() { isArray = angular.isArray; isObject = angular.isObject; + isDefined = angular.isDefined; function inherit(parent, extra) { return angular.extend(Object.create(parent), extra); @@ -112,12 +122,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`. @@ -127,7 +137,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: @@ -135,7 +147,9 @@ 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. @@ -155,7 +169,7 @@ function $RouteProvider() { * 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 @@ -165,7 +179,7 @@ function $RouteProvider() { * - `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 + * - `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: @@ -176,7 +190,31 @@ 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. + * + * - `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. * * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()` * or `$location.hash()` changes. @@ -210,7 +248,7 @@ function $RouteProvider() { // 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 + '/'; @@ -255,7 +293,7 @@ function $RouteProvider() { path = path .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) { + .replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) { var optional = (option === '?' || option === '*?') ? '?' : null; var star = (option === '*' || option === '*?') ? '*' : null; keys.push({ name: key, optional: !!optional }); @@ -269,7 +307,7 @@ function $RouteProvider() { + ')' + (optional || ''); }) - .replace(/([\/$\*])/g, '\\$1'); + .replace(/([/$*])/g, '\\$1'); ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); return ret; @@ -295,6 +333,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', @@ -388,12 +467,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; * }) * @@ -426,15 +505,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/); * }); * * @@ -482,12 +561,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. */ /** @@ -628,37 +709,103 @@ 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(); - } - } - } - $q.when(nextRoute). - 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); - } - }, function(error) { - if (nextRoute == $route.current) { + var nextRoutePromise = $q.resolve(nextRoute); + + 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); } }); } } + 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(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; + } + } + } else if (route.resolveRedirectTo) { + return $q. + resolve($injector.invoke(route.resolveRedirectTo)). + then(function(newUrl) { + if (angular.isDefined(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; + } + + 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); @@ -675,7 +822,6 @@ function $RouteProvider() { } } - function getTemplateFor(route) { var template, templateUrl; if (angular.isDefined(template = route.template)) { @@ -694,7 +840,6 @@ function $RouteProvider() { return template; } - /** * @returns {Object} the current active route, by matching it against the URL */ @@ -734,6 +879,14 @@ function $RouteProvider() { }]; } +instantiateRoute.$inject = ['$injector']; +function instantiateRoute($injector) { + if (isEagerInstantiationEnabled) { + // Instantiate `$route` + $injector.get('$route'); + } +} + ngRouteModule.provider('$routeParams', $RouteParamsProvider); @@ -741,6 +894,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. @@ -800,13 +954,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * * The enter and leave animation occur concurrently. * - * @knownIssue If `ngView` is contained in an asynchronously loaded template (e.g. in another - * directive's templateUrl or in a template loaded using `ngInclude`), then you need to - * make sure that `$route` is instantiated in time to capture the initial - * `$locationChangeStart` event and load the appropriate view. One way to achieve this - * is to have it as a dependency in a `.run` block: - * `myModule.run(['$route', function() {}]);` - * * @scope * @priority 400 * @param {string=} onload Expression to evaluate whenever the view updates. @@ -917,17 +1064,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; }]); @@ -937,15 +1084,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/); }); @@ -988,8 +1135,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; } @@ -1010,8 +1157,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(); } diff --git a/public/vendor/angular-route/angular-route.min.js b/public/vendor/angular-route/angular-route.min.js index 2fa073f61aa..7d1409b276b 100644 --- a/public/vendor/angular-route/angular-route.min.js +++ b/public/vendor/angular-route/angular-route.min.js @@ -1,16 +1,17 @@ /* - AngularJS v1.5.8 + AngularJS v1.6.1 (c) 2010-2016 Google, Inc. http://angularjs.org License: MIT */ -(function(E,d){'use strict';function y(t,l,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(b,e,a,c,k){function p(){m&&(g.cancel(m),m=null);h&&(h.$destroy(),h=null);n&&(m=g.leave(n),m.then(function(){m=null}),n=null)}function B(){var a=t.current&&t.current.locals;if(d.isDefined(a&&a.$template)){var a=b.$new(),c=t.current;n=k(a,function(a){g.enter(a,null,n||e).then(function(){!d.isDefined(A)||A&&!b.$eval(A)||l()});p()});h=c.scope=a;h.$emit("$viewContentLoaded"); -h.$eval(s)}else p()}var h,n,m,A=a.autoscroll,s=a.onload||"";b.$on("$routeChangeSuccess",B);B()}}}function w(d,l,g){return{restrict:"ECA",priority:-400,link:function(b,e){var a=g.current,c=a.locals;e.html(c.$template);var k=d(e.contents());if(a.controller){c.$scope=b;var p=l(a.controller,c);a.controllerAs&&(b[a.controllerAs]=p);e.data("$ngControllerController",p);e.children().data("$ngControllerController",p)}b[a.resolveAs||"$resolve"]=c;k(b)}}}var x,C,s=d.module("ngRoute",["ng"]).provider("$route", -function(){function t(b,e){return d.extend(Object.create(b),e)}function l(b,d){var a=d.caseInsensitiveMatch,c={originalPath:b,regexp:b},g=c.keys=[];b=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g,function(b,a,d,c){b="?"===c||"*?"===c?"?":null;c="*"===c||"*?"===c?"*":null;g.push({name:d,optional:!!b});a=a||"";return""+(b?"":a)+"(?:"+(b?a:"")+(c&&"(.+?)"||"([^/]+)")+(b||"")+")"+(b||"")}).replace(/([\/$\*])/g,"\\$1");c.regexp=new RegExp("^"+b+"$",a?"i":"");return c}x=d.isArray;C= -d.isObject;var g={};this.when=function(b,e){var a;a=void 0;if(x(e)){a=a||[];for(var c=0,k=e.length;c + + + + 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 @@ -23947,7 +24509,7 @@ 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) { @@ -24005,7 +24567,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 @@ -24030,7 +24592,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; @@ -24098,7 +24660,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)) { @@ -24140,7 +24702,7 @@ function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var timezone = ctrl && ctrl.$options.getOption('timezone'); var previousDate; ctrl.$$parserName = type; @@ -24219,10 +24781,7 @@ function badInputChecker(scope, element, attr, ctrl) { } } -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - +function numberFormatterParser(ctrl) { ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; @@ -24239,38 +24798,241 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } 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); + + // 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 (!isNumberInteger(value) || !isNumberInteger(stepBase) || !isNumberInteger(step)) { + var decimalCount = Math.max(countDecimals(value), countDecimals(stepBase), countDecimals(step)); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + } + + return (value - stepBase) % step === 0; +} + +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var minVal; + var maxVal; if (isDefined(attr.min) || attr.ngMin) { - var minVal; ctrl.$validators.min = function(value) { return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; attr.$observe('min', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val); - } - minVal = isNumber(val) && !isNaN(val) ? val : undefined; + minVal = parseNumberAttrVal(val); // 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; }; attr.$observe('max', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val); - } - maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + maxVal = parseNumberAttrVal(val); // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); }); } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal; + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + attr.$observe('step', function(val) { + stepVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } +} + +function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + 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) { + 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) { + 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) { + 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 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]); + attr.$observe(htmlAttrName, changeFn); + } + + function minChange(val) { + minVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + 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); + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + + function maxChange(val) { + maxVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + 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; + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + + function stepChange(val) { + stepVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + // Some browsers don't adjust the input value correctly, but set the stepMismatch error + if (supportsRange && ctrl.$viewValue !== element.val()) { + ctrl.$setViewValue(element.val()); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { @@ -24300,14 +25062,20 @@ 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); } }; @@ -24315,7 +25083,10 @@ function radioInputType(scope, element, attr, ctrl) { 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); @@ -24398,6 +25169,20 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {string=} ngChange Angular 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. + * + * @knownIssue + * + * When specifying the `placeholder` attribute of `
- * {{text}} + * {{text}} * * * @@ -30565,7 +31567,7 @@ var ngSwitchDefaultDirective = ngDirective({ * This example shows how to use `NgTransclude` with fallback content, that * is displayed if no transcluded content is provided. * - * + * * *