diff --git a/.gitignore b/.gitignore index aea33b02..37fce33f 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ OurUmbraco.Site/apidocs/ OurUmbraco.Site/App_Data/Documentation/ OurUmbraco.Site/App_Data/NotificationTest/ build/tools/node* +wwwcache/* diff --git a/OurUmbraco.Site/OurUmbraco.Site.csproj b/OurUmbraco.Site/OurUmbraco.Site.csproj index 60fe7a56..6103b786 100644 --- a/OurUmbraco.Site/OurUmbraco.Site.csproj +++ b/OurUmbraco.Site/OurUmbraco.Site.csproj @@ -1,6 +1,6 @@  - + Debug @@ -55,8 +55,8 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\businesslogic.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\businesslogic.dll True @@ -67,12 +67,12 @@ ..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\cms.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\cms.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\controls.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\controls.dll True @@ -83,8 +83,8 @@ ..\packages\EasyHttp.1.6.29.0\lib\net40\EasyHttp.dll True - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.80\lib\net45\Examine.dll True @@ -95,20 +95,20 @@ ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll True - - ..\packages\ImageProcessor.2.4.5.0\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll True - - ..\packages\ImageProcessor.Web.4.6.6.0\lib\net45\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.8.0\lib\net45\ImageProcessor.Web.dll True ..\packages\UrlTracker.3.11\lib\InfoCaster.Umbraco.UrlTracker.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\interfaces.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\interfaces.dll True @@ -116,7 +116,7 @@ ..\packages\JsonFx.2.0.1209.2802\lib\net40\JsonFx.dll - ..\packages\UmbracoCms.Core.7.5.5\lib\log4net.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\log4net.dll True @@ -128,7 +128,7 @@ True - ..\packages\UmbracoCms.Core.7.5.5\lib\Microsoft.ApplicationBlocks.Data.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\Microsoft.ApplicationBlocks.Data.dll True @@ -148,6 +148,10 @@ True + + ..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.0\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll + True + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -204,8 +208,8 @@ ..\packages\semver.1.1.2\lib\net451\Semver.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\SQLCE4Umbraco.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\SQLCE4Umbraco.dll True @@ -214,11 +218,11 @@ - ..\packages\UmbracoCms.Core.7.5.5\lib\System.Data.SqlServerCe.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\System.Data.SqlServerCe.dll True - ..\packages\UmbracoCms.Core.7.5.5\lib\System.Data.SqlServerCe.Entity.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\System.Data.SqlServerCe.Entity.dll True @@ -282,47 +286,47 @@ - ..\packages\UmbracoCms.Core.7.5.5\lib\TidyNet.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\TidyNet.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\umbraco.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\Umbraco.Core.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\Umbraco.Core.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\umbraco.DataLayer.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.DataLayer.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\umbraco.editorControls.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.editorControls.dll True ..\packages\UmbracoCms.IdentityExtensions.1.0.0\lib\net45\Umbraco.IdentityExtensions.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\umbraco.MacroEngines.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.MacroEngines.dll True ..\packages\Umbraco.ModelsBuilder.3.0.5\lib\Umbraco.ModelsBuilder.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\umbraco.providers.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.providers.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\Umbraco.Web.UI.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\Umbraco.Web.UI.dll True - - ..\packages\UmbracoCms.Core.7.5.5\lib\UmbracoExamine.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\UmbracoExamine.dll True @@ -597,6 +601,9 @@ + + + @@ -4366,10 +4373,10 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + - + + + + + + + + + + + + + \ No newline at end of file diff --git a/OurUmbraco.Site/packages.config b/OurUmbraco.Site/packages.config index da6bd338..d273dba7 100644 --- a/OurUmbraco.Site/packages.config +++ b/OurUmbraco.Site/packages.config @@ -6,10 +6,11 @@ - + - - + + + @@ -33,6 +34,7 @@ + @@ -51,8 +53,8 @@ - - + + diff --git a/OurUmbraco.Site/umbraco/js/umbraco.controllers.js b/OurUmbraco.Site/umbraco/js/umbraco.controllers.js index ce2b5f78..07e2f90f 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.controllers.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.controllers.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ @@ -6369,8 +6369,6 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig //share state editorState.set($scope.content); - - dataTypeHelper.rebindChangedProperties($scope.content, data); }); } @@ -15823,7 +15821,7 @@ function sliderController($scope, $log, $element, assetsService, angularHelper) angular.module("umbraco").controller("Umbraco.PropertyEditors.SliderController", sliderController); angular.module("umbraco") .controller("Umbraco.PropertyEditors.TagsController", - function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element, $sanitize) { + function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element) { var $typeahead; @@ -15864,7 +15862,6 @@ angular.module("umbraco") //Helper method to add a tag on enter or on typeahead select function addTag(tagToAdd) { - tagToAdd = String(tagToAdd).htmlEncode(); if (tagToAdd != null && tagToAdd.length > 0) { if ($scope.model.value.indexOf(tagToAdd) < 0) { $scope.model.value.push(tagToAdd); diff --git a/OurUmbraco.Site/umbraco/js/umbraco.directives.js b/OurUmbraco.Site/umbraco/js/umbraco.directives.js index 2076f874..47f84c90 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.directives.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.directives.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ @@ -3643,6 +3643,55 @@ angular.module("umbraco.directives") }); + // pin toolbar to top of screen if we have focus and it scrolls off the screen + var pinToolbar = function () { + + var _toolbar = $(editor.editorContainer).find(".mce-toolbar"); + var toolbarHeight = _toolbar.height(); + + var _tinyMce = $(editor.editorContainer); + var tinyMceRect = _tinyMce[0].getBoundingClientRect(); + var tinyMceTop = tinyMceRect.top; + var tinyMceBottom = tinyMceRect.bottom; + var tinyMceWidth = tinyMceRect.width; + + var _tinyMceEditArea = _tinyMce.find(".mce-edit-area"); + + // set padding in top of mce so the content does not "jump" up + _tinyMceEditArea.css("padding-top", toolbarHeight); + + if (tinyMceTop < 160 && ((160 + toolbarHeight) < tinyMceBottom)) { + _toolbar + .css("visibility", "visible") + .css("position", "fixed") + .css("top", "160px") + .css("margin-top", "0") + .css("width", tinyMceWidth); + } else { + _toolbar + .css("visibility", "visible") + .css("position", "absolute") + .css("top", "auto") + .css("margin-top", "0") + .css("width", tinyMceWidth); + } + + }; + + // unpin toolbar to top of screen + var unpinToolbar = function() { + + var _toolbar = $(editor.editorContainer).find(".mce-toolbar"); + var _tinyMce = $(editor.editorContainer); + var _tinyMceEditArea = _tinyMce.find(".mce-edit-area"); + + // reset padding in top of mce so the content does not "jump" up + _tinyMceEditArea.css("padding-top", "0"); + + _toolbar.css("position", "static"); + + }; + //when we leave the editor (maybe) editor.on('blur', function (e) { editor.save(); @@ -3656,6 +3705,9 @@ angular.module("umbraco.directives") scope.onBlur(); } + unpinToolbar(); + $('.umb-panel-body').off('scroll', pinToolbar); + }); }); @@ -3667,6 +3719,9 @@ angular.module("umbraco.directives") scope.onFocus(); } + pinToolbar(); + $('.umb-panel-body').on('scroll', pinToolbar); + }); }); @@ -3678,6 +3733,9 @@ angular.module("umbraco.directives") scope.onClick(); } + pinToolbar(); + $('.umb-panel-body').on('scroll', pinToolbar); + }); }); diff --git a/OurUmbraco.Site/umbraco/js/umbraco.filters.js b/OurUmbraco.Site/umbraco/js/umbraco.filters.js index d9bfd464..bbacda62 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.filters.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.filters.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ diff --git a/OurUmbraco.Site/umbraco/js/umbraco.installer.js b/OurUmbraco.Site/umbraco/js/umbraco.installer.js index 90538bf3..ae0cb09b 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.installer.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.installer.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ diff --git a/OurUmbraco.Site/umbraco/js/umbraco.resources.js b/OurUmbraco.Site/umbraco/js/umbraco.resources.js index 437868e7..a8c4c200 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.resources.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.resources.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ diff --git a/OurUmbraco.Site/umbraco/js/umbraco.security.js b/OurUmbraco.Site/umbraco/js/umbraco.security.js index 59c4d215..85da3bde 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.security.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.security.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ diff --git a/OurUmbraco.Site/umbraco/js/umbraco.services.js b/OurUmbraco.Site/umbraco/js/umbraco.services.js index f5410fa5..dd960565 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.services.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.services.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ @@ -8305,15 +8305,15 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ * @description * This returns a promise with an underlying http call, it is a helper method to reduce * the amount of duplicate code needed to query http resources and automatically handle any - * Http errors. See /docs/source/using-promises-resources.md + * 500 Http server errors. * - * @param {object} opts A mixed object which can either be a string representing the error message to be - * returned OR an object containing either: + * @param {object} opts A mixed object which can either be a `string` representing the error message to be + * returned OR an `object` containing either: * { success: successCallback, errorMsg: errorMessage } * OR * { success: successCallback, error: errorCallback } - * In both of the above, the successCallback must accept these parameters: data, status, headers, config - * If using the errorCallback it must accept these parameters: data, status, headers, config + * In both of the above, the successCallback must accept these parameters: `data`, `status`, `headers`, `config` + * If using the errorCallback it must accept these parameters: `data`, `status`, `headers`, `config` * The success callback must return the data which will be resolved by the deferred object. * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } */ diff --git a/OurUmbraco.Site/umbraco/js/umbraco.testing.js b/OurUmbraco.Site/umbraco/js/umbraco.testing.js index eed826e8..9bcd3eb7 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.testing.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.testing.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ diff --git a/OurUmbraco.Site/umbraco/lib/angular/angular.js b/OurUmbraco.Site/umbraco/lib/angular/angular.js index 400c7740..0f3f3858 100644 --- a/OurUmbraco.Site/umbraco/lib/angular/angular.js +++ b/OurUmbraco.Site/umbraco/lib/angular/angular.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.9 + * @license AngularJS v1.6.1 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -57,7 +57,7 @@ function minErr(module, ErrorConstructor) { return match; }); - message += '\nhttp://errors.angularjs.org/1.5.9/' + + message += '\nhttp://errors.angularjs.org/1.6.1/' + (module ? module + '/' : '') + code; for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { @@ -157,6 +157,7 @@ function minErr(module, ErrorConstructor) { getBlockNodes, hasOwnProperty, createMap, + stringify, NODE_TYPE_ELEMENT, NODE_TYPE_ATTRIBUTE, @@ -190,9 +191,41 @@ 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 + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead. + * + * @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;}; + +/** + * @ngdoc function + * @name angular.uppercase + * @module ng + * @kind function + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead. + * + * @description Converts the specified string to uppercase. + * @param {string} string String to be converted to uppercase. + * @returns {string} Uppercased string. + */ var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; @@ -237,6 +270,7 @@ 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 @@ -312,9 +346,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); } } @@ -671,7 +703,7 @@ function isDate(value) { * @kind function * * @description - * Determines if a reference is an `Array`. + * Determines if a reference is an `Array`. Alias of Array.isArray. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. @@ -747,7 +779,7 @@ 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)); } @@ -766,7 +798,7 @@ var trim = function(value) { // Prereq: s is a string. var escapeForRegexp = function(s) { return s - .replace(/([-()\[\]{}+?*.$\^|,:#]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); + replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); } catch (e) { return lowercase(elemHtml); } @@ -1484,7 +1517,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 @@ -1521,12 +1554,14 @@ function allowAutoBootstrap(document) { var src = document.currentScript.getAttribute('src'); var link = document.createElement('a'); link.href = src; - var scriptProtocol = link.protocol; - var docLoadProtocol = document.location.protocol; - if (docLoadProtocol === scriptProtocol) { + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for non-whitelisted schemes. return true; } - switch (scriptProtocol) { + // 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:': @@ -2026,6 +2061,27 @@ function createMap() { return Object.create(null); } +function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + if (hasCustomToString(value) && !isArray(value) && !isDate(value)) { + value = value.toString(); + } else { + value = toJson(value); + } + } + + return value; +} + var NODE_TYPE_ELEMENT = 1; var NODE_TYPE_ATTRIBUTE = 2; var NODE_TYPE_TEXT = 3; @@ -2236,7 +2292,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#decorator $provide.decorator()}. */ - decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), + decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks), /** * @ngdoc method @@ -2382,10 +2438,11 @@ function setupModuleLoader(window) { * @param {string} method * @returns {angular.Module} */ - function invokeLaterAndSetModuleName(provider, method) { + function invokeLaterAndSetModuleName(provider, method, queue) { + if (!queue) queue = invokeQueue; return function(recipeName, factoryFunction) { if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; - invokeQueue.push([provider, method, arguments]); + queue.push([provider, method, arguments]); return moduleInstance; }; } @@ -2514,6 +2571,7 @@ function toDebugString(obj) { $ControllerProvider, $DateProvider, $DocumentProvider, + $$IsDocumentHiddenProvider, $ExceptionHandlerProvider, $FilterProvider, $$ForceReflowProvider, @@ -2565,11 +2623,11 @@ function toDebugString(obj) { var version = { // These placeholder strings will be replaced by grunt's `build` task. // They need to be double- or single-quoted. - full: '1.5.9', + full: '1.6.1', major: 1, - minor: 5, - dot: 9, - codeName: 'timeturning-lockdown' + minor: 6, + dot: 1, + codeName: 'promise-rectification' }; @@ -2602,9 +2660,12 @@ function publishExternalAPI(angular) { 'uppercase': uppercase, 'callbacks': {$$counter: 0}, 'getTestability': getTestability, + 'reloadWithDebugInfo': reloadWithDebugInfo, '$$minErr': minErr, '$$csp': csp, - 'reloadWithDebugInfo': reloadWithDebugInfo + '$$encodeUriSegment': encodeUriSegment, + '$$encodeUriQuery': encodeUriQuery, + '$$stringify': stringify }); angularModule = setupModuleLoader(window); @@ -2678,6 +2739,7 @@ function publishExternalAPI(angular) { $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, + $$isDocumentHidden: $$IsDocumentHiddenProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $$forceReflow: $$ForceReflowProvider, @@ -2723,9 +2785,8 @@ function publishExternalAPI(angular) { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -/* global JQLitePrototype: true, - addEventListenerFn: true, - removeEventListenerFn: true, +/* global + JQLitePrototype: true, BOOLEAN_ATTR: true, ALIASED_ATTR: true */ @@ -2769,7 +2830,7 @@ function publishExternalAPI(angular) { * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters - * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData + * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) @@ -2789,16 +2850,16 @@ function publishExternalAPI(angular) { * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) + * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`) * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument * - [`removeData()`](http://api.jquery.com/removeData/) * - [`replaceWith()`](http://api.jquery.com/replaceWith/) * - [`text()`](http://api.jquery.com/text/) * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers - * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter + * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -2836,13 +2897,7 @@ function publishExternalAPI(angular) { JQLite.expando = 'ng339'; var jqCache = JQLite.cache = {}, - jqId = 1, - addEventListenerFn = function(element, type, fn) { - element.addEventListener(type, fn, false); - }, - removeEventListenerFn = function(element, type, fn) { - element.removeEventListener(type, fn, false); - }; + jqId = 1; /* * !!! This is an undocumented "private" function !!! @@ -2855,22 +2910,31 @@ JQLite._data = function(node) { function jqNextId() { return ++jqId; } -var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; -var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var DASH_LOWERCASE_REGEXP = /-([a-z])/g; +var MS_HACK_REGEXP = /^-ms-/; var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. * @param name Name to normalize */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); +function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); +} + +function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); +} + +/** + * Converts kebab-case to camelCase. + * @param name Name to normalize + */ +function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; @@ -3007,6 +3071,8 @@ function JQLite(element) { if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); + } else if (isFunction(element)) { + jqLiteReady(element); } else { jqLiteAddNodes(this, element); } @@ -3039,7 +3105,7 @@ function jqLiteOff(element, type, fn, unsupported) { if (!type) { for (type in events) { if (type !== '$destroy') { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); } delete events[type]; } @@ -3051,7 +3117,7 @@ function jqLiteOff(element, type, fn, unsupported) { arrayRemove(listenerFns || [], fn); } if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); delete events[type]; } }; @@ -3102,6 +3168,7 @@ function jqLiteExpandoStore(element, createIfNecessary) { function jqLiteData(element, key, value) { if (jqLiteAcceptsData(element)) { + var prop; var isSimpleSetter = isDefined(value); var isSimpleGetter = !isSimpleSetter && key && !isObject(key); @@ -3110,16 +3177,18 @@ function jqLiteData(element, key, value) { var data = expandoStore && expandoStore.data; if (isSimpleSetter) { // data('key', value) - data[key] = value; + data[kebabToCamel(key)] = value; } else { if (massGetter) { // data() return data; } else { if (isSimpleGetter) { // data('key') // don't force creation of expandoStore if it doesn't exist yet - return data && data[key]; + return data && data[kebabToCamel(key)]; } else { // mass-setter: data({key1: val1, key2: val2}) - extend(data, key); + for (prop in key) { + data[kebabToCamel(prop)] = key[prop]; + } } } } @@ -3238,29 +3307,32 @@ function jqLiteDocumentLoaded(action, win) { } } +function jqLiteReady(fn) { + function trigger() { + window.document.removeEventListener('DOMContentLoaded', trigger); + window.removeEventListener('load', trigger); + fn(); + } + + // check if document is already loaded + if (window.document.readyState === 'complete') { + window.setTimeout(fn); + } else { + // We can not use jqLite since we are not done loading and jQuery could be loaded later. + + // Works for modern browsers and IE9 + window.document.addEventListener('DOMContentLoaded', trigger); + + // Fallback to window.onload for others + window.addEventListener('load', trigger); + } +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document is already loaded - if (window.document.readyState === 'complete') { - window.setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // eslint-disable-next-line new-cap - JQLite(window).on('load', trigger); // fallback to window.onload for others - } - }, + ready: jqLiteReady, toString: function() { var value = []; forEach(this, function(e) { value.push('' + e);}); @@ -3295,7 +3367,8 @@ var ALIASED_ATTR = { 'ngMaxlength': 'maxlength', 'ngMin': 'min', 'ngMax': 'max', - 'ngPattern': 'pattern' + 'ngPattern': 'pattern', + 'ngStep': 'step' }; function getBooleanAttrName(element, name) { @@ -3346,7 +3419,7 @@ forEach({ hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; @@ -3356,33 +3429,33 @@ forEach({ }, attr: function(element, name, value) { + var ret; var nodeType = element.nodeType; - if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) { + if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT || + !element.getAttribute) { return; } + var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } + var isBooleanAttr = BOOLEAN_ATTR[lowercasedName]; + + if (isDefined(value)) { + // setter + + if (value === null || (value === false && isBooleanAttr)) { + element.removeAttribute(name); } else { - return (element[name] || - (element.attributes.getNamedItem(name) || noop).specified) - ? lowercasedName - : undefined; + element.setAttribute(name, isBooleanAttr ? lowercasedName : value); } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) + } else { + // getter + + ret = element.getAttribute(name); + + if (isBooleanAttr && ret !== null) { + ret = lowercasedName; + } + // Normalize non-existing attributes to undefined (as jQuery). return ret === null ? undefined : ret; } }, @@ -3417,7 +3490,7 @@ forEach({ result.push(option.value || option.text); } }); - return result.length === 0 ? null : result; + return result; } return element.value; } @@ -3587,7 +3660,7 @@ forEach({ eventFns = events[type] = []; eventFns.specialHandlerWrapper = specialHandlerWrapper; if (type !== '$destroy' && !noEventListener) { - addEventListenerFn(element, type, handle); + element.addEventListener(type, handle); } } @@ -3680,12 +3753,15 @@ forEach({ after: function(element, newElement) { var index = element, parent = element.parentNode; - newElement = new JQLite(newElement); - for (var i = 0, ii = newElement.length; i < ii; i++) { - var node = newElement[i]; - parent.insertBefore(node, index.nextSibling); - index = node; + if (parent) { + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; + parent.insertBefore(node, index.nextSibling); + index = node; + } } }, @@ -3779,12 +3855,12 @@ forEach({ } return isDefined(value) ? value : this; }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; }); +// bind legacy bind/unbind to on/off +JQLite.prototype.bind = JQLite.prototype.on; +JQLite.prototype.unbind = JQLite.prototype.off; + // Provider for private $$jqLite service /** @this */ @@ -3949,8 +4025,8 @@ var $$HashMapProvider = [/** @this */function() { * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ -var ARROW_ARG = /^([^\(]+?)=>/; -var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; +var ARROW_ARG = /^([^(]+?)=>/; +var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; @@ -4735,14 +4811,18 @@ function createInjector(modulesToLoad, strictDi) { } function isClass(func) { + // Support: IE 9-11 only // IE 9-11 do not support classes and IE9 leaks with the code below. - if (msie <= 11) { + if (msie || typeof func !== 'function') { return false; } - // Support: Edge 12-13 only - // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ - return typeof func === 'function' - && /^(?:class\b|constructor\()/.test(stringifyFn(func)); + var result = func.$$ngIsClass; + if (!isBoolean(result)) { + // Support: Edge 12-13 only + // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ + result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func)); + } + return result; } function invoke(fn, self, locals, serviceName) { @@ -5030,7 +5110,8 @@ function $AnchorScrollProvider() { } function scroll(hash) { - hash = isString(hash) ? hash : $location.hash(); + // Allow numeric hashes + hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash(); var elm; // empty hash, scroll to the top of the page @@ -5316,7 +5397,6 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { var reservedRegex = new RegExp('(\\s+|\\/)' + NG_ANIMATE_CLASSNAME + '(\\s+|\\/)'); if (reservedRegex.test(this.$$classNameFilter.toString())) { throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); - } } } @@ -5674,7 +5754,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element. * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take - * on the provided styles. For example, if a transition animation is set for the given classNamem, then the provided `from` and + * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and * `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding * style in `to`, the style in `from` is applied immediately, and no animation is run. * If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate` @@ -5752,8 +5832,8 @@ var $$AnimateAsyncRunFactoryProvider = /** @this */ function() { }; var $$AnimateRunnerFactoryProvider = /** @this */ function() { - this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout', - function($q, $sniffer, $$animateAsyncRun, $document, $timeout) { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) { var INITIAL_STATE = 0; var DONE_PENDING_STATE = 1; @@ -5805,11 +5885,7 @@ var $$AnimateRunnerFactoryProvider = /** @this */ function() { this._doneCallbacks = []; this._tick = function(fn) { - var doc = $document[0]; - - // the document may not be ready or attached - // to the module for some internal tests - if (doc && doc.hidden) { + if ($$isDocumentHidden()) { timeoutTick(fn); } else { rafTick(fn); @@ -6289,7 +6365,7 @@ function Browser(window, document, $log, $sniffer) { */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^(https?:)?\/\/[^\/]*/, '') : ''; + return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; }; /** @@ -6733,12 +6809,14 @@ function $CacheFactoryProvider() { * }); * ``` * - * To retrieve the template later, simply use it in your HTML: - * ```html - *
+ * To retrieve the template later, simply use it in your component: + * ```js + * myApp.component('myComponent', { + * templateUrl: 'templateId.html' + * }); * ``` * - * or get it via Javascript: + * or get it via the `$templateCache` service: * ```js * $templateCache.get('templateId.html') * ``` @@ -6760,7 +6838,7 @@ function $TemplateCacheProvider() { * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * + * Or gives undesired access to variables like document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! @@ -6819,33 +6897,34 @@ function $TemplateCacheProvider() { * * myModule.directive('directiveName', function factory(injectables) { * var directiveDefinitionObject = { - * priority: 0, - * template: '
', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-priority- priority}: 0, + * {@link $compile#-template- template}: '
', // or // function(tElement, tAttrs) { ... }, * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * transclude: false, - * restrict: 'A', - * templateNamespace: 'html', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * controllerAs: 'stringIdentifier', - * bindToController: false, - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { + * // {@link $compile#-templateurl- templateUrl}: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-transclude- transclude}: false, + * {@link $compile#-restrict- restrict}: 'A', + * {@link $compile#-templatenamespace- templateNamespace}: 'html', + * {@link $compile#-scope- scope}: false, + * {@link $compile#-controller- controller}: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * {@link $compile#-controlleras- controllerAs}: 'stringIdentifier', + * {@link $compile#-bindtocontroller- bindToController}: false, + * {@link $compile#-require- require}: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * {@link $compile#-multielement- multiElement}: false, + * {@link $compile#-compile- compile}: function compile(tElement, tAttrs, transclude) { * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } + * {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * } * // or * // return function postLink( ... ) { ... } * }, * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } + * // {@link $compile#-link- link}: { + * // {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * // } * // or - * // link: function postLink( ... ) { ... } + * // {@link $compile#-link- link}: function postLink( ... ) { ... } * }; * return directiveDefinitionObject; * }); @@ -6997,7 +7076,7 @@ function $TemplateCacheProvider() { * compiler}. The attributes are: * * #### `multiElement` - * When this property is set to true, the HTML compiler will collect DOM nodes between + * When this property is set to true (default is `false`), the HTML compiler will collect DOM nodes between * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them * together as the directive elements. It is recommended that this feature be used on directives * which are not strictly behavioral (such as {@link ngClick}), and which @@ -7018,14 +7097,14 @@ function $TemplateCacheProvider() { * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * The scope property can be `true`, an object or a falsy value: + * The scope property can be `false`, `true`, or an object: * - * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope. + * * **`false` (default):** No scope will be created for the directive. The directive will use its + * parent's scope. * * * **`true`:** A new child scope that prototypically inherits from its parent will be created for * the directive's element. If multiple directives on the same element request a new scope, - * only one new scope is created. The new scope rule does not apply for the root of the template - * since the root of the template always gets a new scope. + * only one new scope is created. * * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent @@ -7112,9 +7191,7 @@ function $TemplateCacheProvider() { * * #### `bindToController` * This property is used to bind scope properties directly to the controller. It can be either - * `true` or an object hash with the same format as the `scope` property. Additionally, a controller - * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller - * definition: `controller: 'myCtrl as myAlias'`. + * `true` or an object hash with the same format as the `scope` property. * * When an isolate scope is used for a directive (see above), `bindToController: true` will * allow a component to have its properties bound to the controller, rather than to scope. @@ -7155,12 +7232,12 @@ function $TemplateCacheProvider() { * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) - * and when the `cloneLinkinFn` is passed, + * and when the `cloneLinkingFn` is passed, * as those elements need to created and cloned in a special way when they are defined outside their * usual containers (e.g. like ``). * * See also the `directive.templateNamespace` property. * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`) - * then the default translusion is provided. + * then the default transclusion is provided. * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns * `true` if the specified slot contains content (i.e. one or more DOM nodes). * @@ -7449,7 +7526,7 @@ function $TemplateCacheProvider() { * * When you call a transclusion function you can pass in a **clone attach function**. This function accepts * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded - * content and the `scope` is the newly created transclusion scope, to which the clone is bound. + * content and the `scope` is the newly created transclusion scope, which the clone will be linked to. * *
* **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function @@ -7696,6 +7773,16 @@ function $TemplateCacheProvider() { * * For information on how the compiler works, see the * {@link guide/compiler Angular 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'); @@ -7714,8 +7801,8 @@ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w\-]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?::([^;]+))?;?)/, + COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/, ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; @@ -7779,20 +7866,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); - } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); } return bindings; } @@ -7823,6 +7901,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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 @@ -7839,6 +7928,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertValidDirectiveName(name); @@ -7860,7 +7950,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.index = index; directive.name = directive.name || name; directive.require = getDirectiveRequire(directive); - directive.restrict = directive.restrict || 'EA'; + directive.restrict = getDirectiveRestrict(directive.restrict, name); directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { @@ -8109,6 +8199,35 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return debugInfoEnabled; }; + /** + * @ngdoc method + * @name $compileProvider#preAssignBindingsEnabled + * + * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the + * current preAssignBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable whether directive controllers are assigned bindings before + * calling the controller's constructor. + * If enabled (true), the compiler assigns the value of each of the bindings to the + * properties of the controller object before the constructor of this object is called. + * + * If disabled (false), the compiler calls the constructor first before assigning bindings. + * + * The default value is true in Angular 1.5.x but will switch to false in Angular 1.6.x. + */ + var preAssignBindingsEnabled = false; + this.preAssignBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + preAssignBindingsEnabled = enabled; + return this; + } + return preAssignBindingsEnabled; + }; + var TTL = 10; /** @@ -8562,25 +8681,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] = window.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) { @@ -8635,6 +8744,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } @@ -8667,12 +8780,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); @@ -8763,6 +8887,32 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } + 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; + + 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) { @@ -8826,7 +8976,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attr = nAttrs[j]; name = attr.name; - value = trim(attr.value); + value = attr.value; // support ngAttr attribute binding ngAttrName = directiveNormalize(name); @@ -8882,13 +9032,6 @@ 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 */ @@ -8920,7 +9063,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } /** - * 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 @@ -9107,7 +9250,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; controllerDirectives = controllerDirectives || createMap(); assertNoDuplicate('\'' + directiveName + '\' controller', controllerDirectives[directiveName], directive, $compileNode); @@ -9162,9 +9304,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var slots = createMap(); - $template = jqLite(jqLiteClone(compileNode)).contents(); - - if (isObject(directiveValue)) { + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { // We have transclusion slots, // collect them up, compile them and store their transclusion functions @@ -9406,22 +9548,29 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (controller.identifier && bindings) { - controller.bindingInfo = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } else { - controller.bindingInfo = {}; - } - - 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); - if (controller.bindingInfo.removeWatches) { - controller.bindingInfo.removeWatches(); + if (preAssignBindingsEnabled) { + if (bindings) { + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } else { + controller.bindingInfo = {}; } + + 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); + if (controller.bindingInfo.removeWatches) { + controller.bindingInfo.removeWatches(); + } + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } + } else { + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); controller.bindingInfo = initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } @@ -9650,24 +9799,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; - } - } - tDirectives.push(directive); - match = directive; + directive = directives[i]; + if ((isUndefined(maxPriority) || maxPriority > directive.priority) && + directive.restrict.indexOf(location) !== -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); } - } catch (e) { $exceptionHandler(e); } + if (!directive.$$bindings) { + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; + } + } + tDirectives.push(directive); + match = directive; + } } } return match; @@ -9711,7 +9858,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { forEach(dst, function(value, key) { 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]); } @@ -9830,7 +9981,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; - }); + }).catch(function(error) { + if (error instanceof Error) { + $exceptionHandler(error); + } + }).catch(noop); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { var childBoundTranscludeFn = boundTranscludeFn; @@ -9930,29 +10085,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } // maction[xlink:href] can source SVG. It's not limited to . } else if (attrNormalizedName === 'xlinkHref' || - (tag === 'form' && attrNormalizedName === 'action') + (tag === 'form' && attrNormalizedName === 'action') || + // links can be stylesheets or imports, which can run script in the current origin + (tag === 'link' && attrNormalizedName === 'href') ) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { var trustedContext = getTrustedContext(node, name); - allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + 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}', startingTag(node)); } + 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.'); + } + directives.push({ priority: 100, compile: function() { @@ -9960,12 +10123,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) { @@ -10099,8 +10256,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. + // Set up $watches for isolate scope and controller bindings. function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { var removeWatchCollection = []; var initialChanges = {}; @@ -10118,7 +10274,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!optional && !hasOwnProperty.call(attrs, attrName)) { destination[scopeName] = attrs[attrName] = undefined; } - attrs.$observe(attrName, function(value) { + removeWatch = attrs.$observe(attrName, function(value) { if (isString(value) || isBoolean(value)) { var oldValue = destination[scopeName]; recordChanges(scopeName, value, oldValue); @@ -10137,6 +10293,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { destination[scopeName] = lastValue; } initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); break; case '=': @@ -10192,18 +10349,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (optional && !attrs[attrName]) break; parentGet = $parse(attrs[attrName]); + var deepWatch = parentGet.literal; var initialValue = destination[scopeName] = parentGet(scope); initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) { if (oldValue === newValue) { - if (oldValue === initialValue) return; + if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) { + return; + } oldValue = initialValue; } recordChanges(scopeName, newValue, oldValue); destination[scopeName] = newValue; - }, parentGet.literal); + }, deepWatch); removeWatchCollection.push(removeWatch); break; @@ -10223,7 +10383,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); function recordChanges(key, currentValue, previousValue) { - if (isFunction(destination.$onChanges) && currentValue !== previousValue) { + if (isFunction(destination.$onChanges) && currentValue !== previousValue && + // eslint-disable-next-line no-self-compare + (currentValue === currentValue || previousValue === previousValue)) { // If we have not already scheduled the top level onChangesQueue handler then do so now if (!onChangesQueue) { scope.$$postDigest(flushOnChangesQueue); @@ -10269,12 +10431,16 @@ SimpleChange.prototype.isFirstChange = function() { return this.previousValue == 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, fnCamelCaseReplace); } /** @@ -10363,8 +10529,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; @@ -10429,6 +10596,11 @@ function $ControllerProvider() { * @ngdoc method * @name $controllerProvider#allowGlobals * @description If called, allows `$controller` to find controller constructors on `window` + * + * @deprecated + * sinceVersion="v1.3.0" + * removeVersion="v1.7.0" + * This method of finding controllers has been deprecated. */ this.allowGlobals = function() { globals = true; @@ -10449,7 +10621,7 @@ 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) + * `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 @@ -10492,6 +10664,11 @@ function $ControllerProvider() { : getter(locals.$scope, constructor, true) || (globals ? getter($window, constructor, true) : undefined); + if (!expression) { + throw $controllerMinErr('ctrlreg', + 'The controller with the name \'{0}\' is not registered.', constructor); + } + assertArgFn(expression, constructor, true); } @@ -10583,6 +10760,33 @@ 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 @@ -10665,13 +10869,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)) { @@ -10853,7 +11052,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) { @@ -10944,6 +11143,10 @@ function $HttpProvider() { * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * + * - **`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'`. + * **/ var defaults = this.defaults = { // transform incoming response data @@ -10967,7 +11170,9 @@ function $HttpProvider() { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', - paramSerializer: '$httpParamSerializer' + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' }; var useApplyAsync = false; @@ -10998,30 +11203,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 @@ -11037,8 +11218,8 @@ function $HttpProvider() { **/ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { var defaultCache = $cacheFactory('$http'); @@ -11155,14 +11336,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 @@ -11170,7 +11343,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) @@ -11460,7 +11633,8 @@ function $HttpProvider() { * 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. @@ -11526,11 +11700,11 @@ function $HttpProvider() {
http status code: {{status}}
@@ -11539,6 +11713,13 @@ function $HttpProvider() { angular.module('httpExample', []) + .config(['$sceDelegateProvider', function($sceDelegateProvider) { + // We must whitelist the JSONP endpoint that we are using to show that we trust it + $sceDelegateProvider.resourceUrlWhitelist([ + 'self', + 'https://angularjs.org/**' + ]); + }]) .controller('FetchController', ['$scope', '$http', '$templateCache', function($scope, $http, $templateCache) { $scope.method = 'GET'; @@ -11606,15 +11787,16 @@ function $HttpProvider() { throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); } - if (!isString(requestConfig.url)) { - throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url); + if (!isString($sce.valueOf(requestConfig.url))) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url); } var config = extend({ method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse, - paramSerializer: defaults.paramSerializer + paramSerializer: defaults.paramSerializer, + jsonpCallbackParam: defaults.jsonpCallbackParam }, requestConfig); config.headers = mergeHeaders(requestConfig); @@ -11622,9 +11804,11 @@ function $HttpProvider() { config.paramSerializer = isString(config.paramSerializer) ? $injector.get(config.paramSerializer) : config.paramSerializer; + $browser.$$incOutstandingRequestCount(); + var requestInterceptors = []; var responseInterceptors = []; - var promise = $q.when(config); + var promise = $q.resolve(config); // apply interceptors forEach(reversedInterceptors, function(interceptor) { @@ -11639,29 +11823,7 @@ function $HttpProvider() { promise = chainInterceptors(promise, requestInterceptors); promise = promise.then(serverRequest); promise = chainInterceptors(promise, responseInterceptors); - - if (useLegacyPromise) { - promise.success = function(fn) { - assertArgFn(fn, 'fn'); - - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - - promise.error = function(fn) { - assertArgFn(fn, 'fn'); - - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - } else { - promise.success = $httpMinErrLegacyFn('success'); - promise.error = $httpMinErrLegacyFn('error'); - } + promise = promise.finally(completeOutstandingRequest); return promise; @@ -11679,6 +11841,10 @@ function $HttpProvider() { return promise; } + function completeOutstandingRequest() { + $browser.$$completeOutstandingRequest(noop); + } + function executeHeaderFns(headers, config) { var headerContent, processedHeaders = {}; @@ -11762,7 +11928,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `GET` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11774,7 +11941,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `DELETE` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11786,7 +11954,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `HEAD` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11797,11 +11966,34 @@ function $HttpProvider() { * * @description * Shortcut method to perform `JSONP` request. + * + * Note that, since JSONP requests are sensitive because the response is given full access to the browser, + * the url must be declared, via {@link $sce} as a trusted resource URL. + * You can trust a URL by adding it to the whitelist via + * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or + * by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}. + * + * JSONP requests must specify a callback to be used in the response from the server. This callback + * is passed as a query parameter in the request. You must specify the name of this parameter by + * setting the `jsonpCallbackParam` property on the request config object. + * + * ``` + * $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'}) + * ``` + * + * You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`. + * Initially this is set to `'callback'`. + * + *
+ * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback + * parameter value should go. + *
+ * * If you would like to customise where and how the callbacks are stored then try overriding * or decorating the {@link $jsonpCallbacks} service. * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * The name of the callback should be the string `JSON_CALLBACK`. + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11900,12 +12092,28 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, - url = buildUrl(config.url, config.paramSerializer(config.params)); + isJsonp = lowercase(config.method) === 'jsonp', + url = config.url; + + if (isJsonp) { + // JSONP is a pretty sensitive operation where we're allowing a script to have full access to + // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL. + url = $sce.getTrustedResourceUrl(url); + } else if (!isString(url)) { + // If it is not a string then the URL must be a $sce trusted object + url = $sce.valueOf(url); + } + + url = buildUrl(url, config.paramSerializer(config.params)); + + if (isJsonp) { + // Check the url and add the JSONP callback placeholder + url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam); + } $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - if ((config.cache || defaults.cache) && config.cache !== false && (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache @@ -12037,6 +12245,24 @@ function $HttpProvider() { } return url; } + + function sanitizeJsonpCallbackParam(url, key) { + if (/[&?][^=]+=JSON_CALLBACK/.test(url)) { + // Throw if the url already contains a reference to JSON_CALLBACK + throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url); + } + + var callbackParamRegex = new RegExp('[&?]' + key + '='); + if (callbackParamRegex.test(url)) { + // Throw if the callback param was already provided + throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url); + } + + // Add in the JSON_CALLBACK callback param value + url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK'; + + return url; + } }]; } @@ -12097,7 +12323,6 @@ function $HttpBackendProvider() { function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { // TODO(vojta): fix the signature return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { - $browser.$$incOutstandingRequestCount(); url = url || $browser.url(); if (lowercase(method) === 'jsonp') { @@ -12209,7 +12434,6 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc jsonpDone = xhr = null; callback(status, response, headersString, statusText); - $browser.$$completeOutstandingRequest(noop); } }; @@ -12224,8 +12448,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc script.async = true; callback = function(event) { - removeEventListenerFn(script, 'load', callback); - removeEventListenerFn(script, 'error', callback); + script.removeEventListener('load', callback); + script.removeEventListener('error', callback); rawDocument.body.removeChild(script); script = null; var status = -1; @@ -12244,8 +12468,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } }; - addEventListenerFn(script, 'load', callback); - addEventListenerFn(script, 'error', callback); + script.addEventListener('load', callback); + script.addEventListener('error', callback); rawDocument.body.appendChild(script); return callback; } @@ -12363,23 +12587,6 @@ function $InterpolateProvider() { replace(escapedEndRegexp, endSymbol); } - function stringify(value) { - if (value == null) { // null || undefined - return ''; - } - switch (typeof value) { - case 'string': - break; - case 'number': - value = '' + value; - break; - default: - value = toJson(value); - } - - return value; - } - // TODO: this is the same as the constantWatchDelegate in parse.js function constantWatchDelegate(scope, listener, objectEquality, constantInterp) { var unwatch = scope.$watch(function constantInterpolateWatch(scope) { @@ -12689,7 +12896,8 @@ function $IntervalProvider() { * 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 {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. * @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. @@ -12844,6 +13052,8 @@ function $IntervalProvider() { */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { + // Interval cancels should not report as unhandled promise. + intervals[promise.$$intervalId].promise.catch(noop); intervals[promise.$$intervalId].reject('canceled'); $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; @@ -12948,7 +13158,7 @@ var $jsonpCallbacksProvider = /** @this */ function() { * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -13001,8 +13211,8 @@ function parseAppUrl(url, locationObj) { } } -function startsWith(haystack, needle) { - return haystack.lastIndexOf(needle, 0) === 0; +function startsWith(str, search) { + return str.slice(0, search.length) === search; } /** @@ -13040,13 +13250,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; @@ -13055,8 +13265,8 @@ 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) { @@ -13119,7 +13329,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. * @@ -13134,8 +13344,8 @@ 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) { @@ -13144,7 +13354,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { 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 = stripBaseUrl(hashPrefix, withoutBaseUrl); if (isUndefined(withoutHashUrl)) { @@ -13209,7 +13419,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { }; /** - * Compose hashbang url and update `absUrl` property + * Compose hashbang URL and update `absUrl` property * @private */ this.$$compose = function() { @@ -13231,7 +13441,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. * @@ -13283,7 +13493,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var locationPrototype = { /** - * Ensure absolute url is initialized. + * Ensure absolute URL is initialized. * @private */ $$absUrl:'', @@ -13307,17 +13517,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'), @@ -13328,18 +13538,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) { @@ -13362,16 +13572,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'), @@ -13382,24 +13592,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. * * * ```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'), @@ -13410,11 +13620,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 * ``` @@ -13430,7 +13640,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`. * @@ -13439,7 +13649,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" * ``` @@ -13459,13 +13669,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'} * @@ -13481,7 +13691,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. @@ -13543,7 +13753,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" * ``` @@ -13666,7 +13876,7 @@ function locationGetterSetter(property, preprocess) { * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ function $LocationProvider() { - var hashPrefix = '', + var hashPrefix = '!', html5Mode = { enabled: false, requireBase: true, @@ -13677,6 +13887,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 */ @@ -13703,8 +13914,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 */ @@ -13722,7 +13937,7 @@ function $LocationProvider() { html5Mode.requireBase = mode.requireBase; } - if (isBoolean(mode.rewriteLinks)) { + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { html5Mode.rewriteLinks = mode.rewriteLinks; } @@ -13819,10 +14034,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); @@ -13832,6 +14048,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 @@ -13873,7 +14091,7 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { - if (isUndefined(stripBaseUrl(appBaseNoFile, newUrl))) { + if (!startsWith(newUrl, appBaseNoFile)) { // If we are navigating outside of the app then force a reload $window.location.href = newUrl; return; @@ -14133,60 +14351,23 @@ function $LogProvider() { var $parseMinErr = minErr('$parse'); -var ARRAY_CTOR = [].constructor; -var BOOLEAN_CTOR = (false).constructor; -var FUNCTION_CTOR = Function.constructor; -var NUMBER_CTOR = (0).constructor; -var OBJECT_CTOR = {}.constructor; -var STRING_CTOR = ''.constructor; -var ARRAY_CTOR_PROTO = ARRAY_CTOR.prototype; -var BOOLEAN_CTOR_PROTO = BOOLEAN_CTOR.prototype; -var FUNCTION_CTOR_PROTO = FUNCTION_CTOR.prototype; -var NUMBER_CTOR_PROTO = NUMBER_CTOR.prototype; -var OBJECT_CTOR_PROTO = OBJECT_CTOR.prototype; -var STRING_CTOR_PROTO = STRING_CTOR.prototype; - -var CALL = FUNCTION_CTOR_PROTO.call; -var APPLY = FUNCTION_CTOR_PROTO.apply; -var BIND = FUNCTION_CTOR_PROTO.bind; - -var objectValueOf = OBJECT_CTOR_PROTO.valueOf; +var objectValueOf = {}.constructor.prototype.valueOf; // Sandboxing Angular Expressions // ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. +// Angular 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: // // {}.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) { // 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 @@ -14205,67 +14386,6 @@ function getStringValue(name) { 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; -} - -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); - } - } -} - -function ensureSafeAssignContext(obj, fullExpression) { - if (obj) { - if (obj === ARRAY_CTOR || - obj === BOOLEAN_CTOR || - obj === FUNCTION_CTOR || - obj === NUMBER_CTOR || - obj === OBJECT_CTOR || - obj === STRING_CTOR || - obj === ARRAY_CTOR_PROTO || - obj === BOOLEAN_CTOR_PROTO || - obj === FUNCTION_CTOR_PROTO || - obj === NUMBER_CTOR_PROTO || - obj === OBJECT_CTOR_PROTO || - obj === STRING_CTOR_PROTO) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor or its prototype is disallowed! Expression: {0}', - fullExpression); - } - } -} var OPERATORS = createMap(); forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); @@ -14556,6 +14676,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; @@ -14982,13 +15106,12 @@ function ASTCompiler(astBuilder, $filter) { } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { 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: [] @@ -15031,24 +15154,14 @@ ASTCompiler.prototype = { // 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); + plusFn); this.state = this.stage = undefined; fn.literal = isLiteral(ast); fn.constant = isConstant(ast); @@ -15122,7 +15235,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; }); @@ -15162,22 +15275,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: @@ -15185,32 +15294,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; @@ -15242,21 +15343,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'); @@ -15268,14 +15364,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); @@ -15285,13 +15376,13 @@ 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 = []; @@ -15333,15 +15424,15 @@ ASTCompiler.prototype = { break; case AST.ThisExpression: this.assign(intoId, 's'); - recursionFn('s'); + recursionFn(intoId || 's'); break; case AST.LocalsExpression: this.assign(intoId, 'l'); - recursionFn('l'); + recursionFn(intoId || 'l'); break; case AST.NGValueParameter: this.assign(intoId, 'v'); - recursionFn('v'); + recursionFn(intoId || 'v'); break; } }, @@ -15400,6 +15491,10 @@ ASTCompiler.prototype = { return '!(' + expression + ')'; }, + isNull: function(expression) { + return expression + '==null'; + }, + notNull: function(expression) { return expression + '!=null'; }, @@ -15423,42 +15518,10 @@ 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 + ')'); }, - ensureSafeAssignContext: function(item) { - return 'ensureSafeAssignContext(' + item + ',text)'; - }, - lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { var self = this; return function() { @@ -15510,11 +15573,9 @@ function ASTInterpreter(astBuilder, $filter) { } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); - this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -15585,20 +15646,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) { @@ -15619,13 +15676,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; }; @@ -15635,8 +15690,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; }; @@ -15712,7 +15765,7 @@ ASTInterpreter.prototype = { if (isDefined(arg)) { arg = -arg; } else { - arg = 0; + arg = -0; } return context ? {value: arg} : arg; }; @@ -15828,16 +15881,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 { @@ -15845,7 +15895,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; @@ -15853,15 +15903,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}; @@ -15870,19 +15917,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 { @@ -15914,14 +15957,10 @@ Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); + return this.astCompiler.compile(text); } }; -function isPossiblyDangerousMemberName(name) { - return name === 'constructor'; -} - function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } @@ -15979,8 +16018,7 @@ function getValueOf(value) { * service. */ function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); + var cache = createMap(); var literals = { 'true': true, 'false': false, @@ -16020,7 +16058,7 @@ function $ParseProvider() { * 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 extensivelly, keep the implementation of these functions fast, + * 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 @@ -16038,37 +16076,20 @@ function $ParseProvider() { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false, - literals: copy(literals), - isIdentifierStart: isFunction(identStart) && identStart, - isIdentifierContinue: isFunction(identContinue) && identContinue - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true, literals: copy(literals), isIdentifierStart: isFunction(identStart) && identStart, isIdentifierContinue: isFunction(identContinue) && identContinue }; - var runningChecksEnabled = false; - - $parse.$$runningExpensiveChecks = function() { - return runningChecksEnabled; - }; - return $parse; - function $parse(exp, interceptorFn, expensiveChecks) { + function $parse(exp, interceptorFn) { var parsedExpression, oneTime, cacheKey; - expensiveChecks = expensiveChecks || runningChecksEnabled; - switch (typeof exp) { case 'string': exp = exp.trim(); cacheKey = exp; - var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { @@ -16076,9 +16097,8 @@ function $ParseProvider() { 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; @@ -16088,9 +16108,6 @@ function $ParseProvider() { } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - if (expensiveChecks) { - parsedExpression = expensiveChecksInterceptor(parsedExpression); - } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -16103,30 +16120,6 @@ 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 expressionInputDirtyCheck(newValue, oldValueOfValue) { if (newValue == null || oldValueOfValue == null) { // null/undefined @@ -16196,14 +16189,22 @@ function $ParseProvider() { }, listener, objectEquality, prettyPrintExpression); } - function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { var unwatch, lastValue; - unwatch = scope.$watch(function oneTimeWatch(scope) { + if (parsedExpression.inputs) { + unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression); + } else { + unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality); + } + return unwatch; + + function oneTimeWatch(scope) { return parsedExpression(scope); - }, /** @this */ function oneTimeListener(value, old, scope) { + } + function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { - listener.apply(this, arguments); + listener(value, old, scope); } if (isDefined(value)) { scope.$$postDigest(function() { @@ -16212,18 +16213,17 @@ function $ParseProvider() { } }); } - }, objectEquality); - return unwatch; + } } function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { var unwatch, lastValue; unwatch = scope.$watch(function oneTimeWatch(scope) { return parsedExpression(scope); - }, /** @this */ function oneTimeListener(value, old, scope) { + }, function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { - listener.call(this, value, old, scope); + listener(value, old, scope); } if (isAllDefined(value)) { scope.$$postDigest(function() { @@ -16272,14 +16272,15 @@ function $ParseProvider() { }; // Propagate $$watchDelegates other then inputsWatchDelegate + useInputs = !parsedExpression.inputs; if (parsedExpression.$$watchDelegate && parsedExpression.$$watchDelegate !== inputsWatchDelegate) { fn.$$watchDelegate = parsedExpression.$$watchDelegate; + fn.inputs = parsedExpression.inputs; } 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; - useInputs = !parsedExpression.inputs; fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; } @@ -16292,14 +16293,13 @@ function $ParseProvider() { * @ngdoc service * @name $q * @requires $rootScope - * @this * * @description * 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. @@ -16506,22 +16506,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; + } + }; } /** @@ -16530,10 +16569,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); + var queueSize = 0; + var checkQueue = []; /** * @ngdoc method @@ -16545,14 +16588,18 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { - var d = new Deferred(); - //Necessary to support unbound execution :/ - d.resolve = simpleBind(d, d.resolve); - d.reject = simpleBind(d, d.reject); - d.notify = simpleBind(d, d.notify); - return d; - }; + 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 }; @@ -16563,13 +16610,13 @@ 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) { @@ -16578,129 +16625,147 @@ function qFactory(nextTick, exceptionHandler) { '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); - } else { - deferred.reject(state.value); + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + state.pur = true; + 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); + } + } + } 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 (!toCheck.pur) { + toCheck.pur = true; + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (toCheck.value instanceof Error) { + exceptionHandler(toCheck.value, errorMessage); + } else { + exceptionHandler(errorMessage); } - } catch (e) { - deferred.reject(e); - exceptionHandler(e); } } } function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) { + 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(); + 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); - } - - }, - - $$resolve: function(val) { - var then; - var that = this; - var done = false; - try { - if ((isObject(val) || isFunction(val))) then = val && val.then; - if (isFunction(then)) { - this.promise.$$state.status = -1; - then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify)); - } else { - this.promise.$$state.value = val; - this.promise.$$state.status = 1; - scheduleProcessQueue(this.promise.$$state); - } - } catch (e) { - rejectPromise(e); - exceptionHandler(e); - } - - function resolvePromise(val) { - if (done) return; - done = true; - that.$$resolve(val); - } - function rejectPromise(val) { - if (done) return; - done = true; - that.$$reject(val); - } - }, - - reject: function(reason) { - if (this.promise.$$state.status) return; - this.$$reject(reason); - }, - - $$reject: function(reason) { - this.promise.$$state.value = reason; - this.promise.$$state.status = 2; - scheduleProcessQueue(this.promise.$$state); - }, - - notify: function(progress) { - var callbacks = this.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); - } - } - }); + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); } + } catch (e) { + doReject(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); + } + } + + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } + + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } + + function notifyPromise(promise, progress) { + var callbacks = promise.$$state.pending; + + 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 @@ -16738,39 +16803,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; - }; + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } - var makePromise = function makePromise(value, resolved) { - var result = new Deferred(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - }; - - 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 @@ -16790,11 +16843,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 @@ -16829,27 +16882,25 @@ 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 deferred.promise; + return result; } /** @@ -16876,25 +16927,25 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } - var $Q = function Q(resolver) { + function $Q(resolver) { if (!isFunction(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. @@ -17046,6 +17097,7 @@ 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 @@ -17226,7 +17278,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, @@ -17237,6 +17289,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. @@ -17354,15 +17408,21 @@ function $RootScopeProvider() { 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; }; @@ -17377,8 +17437,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 @@ -17695,7 +17755,6 @@ function $RootScopeProvider() { $digest: function() { var watch, value, last, fn, get, watchers, - length, dirty, ttl = TTL, next, current, target = this, watchLog = [], @@ -17736,10 +17795,10 @@ function $RootScopeProvider() { do { // "traverse the scopes" loop if ((watchers = 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) { @@ -18439,6 +18498,13 @@ var SCE_CONTEXTS = { // 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; @@ -18452,8 +18518,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. @@ -19024,8 +19090,8 @@ function $SceDelegateProvider() { * .controller('AppController', ['$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( * '
default currency symbol ($): {{amount | currency}}
- custom currency identifier (USD$): {{amount | currency:"USD$"}} + custom currency identifier (USD$): {{amount | currency:"USD$"}}
no fractions (0): {{amount | currency:"USD$":0}} @@ -21004,7 +21071,7 @@ var DATE_FORMATS = { }; var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, - NUMBER_STRING = /^\-?\d+$/; + NUMBER_STRING = /^-?\d+$/; /** * @ngdoc filter @@ -21091,7 +21158,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+ expect(element(by.binding("1288323623006 | date:'medium'")).getText()). toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); + toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). @@ -21387,7 +21454,7 @@ function sliceFn(input, begin, end) { * String, etc). * * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker - * for the preceeding one. The `expression` is evaluated against each item and the output is used + * for the preceding one. The `expression` is evaluated against each item and the output is used * for comparing with other items. * * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in @@ -21457,6 +21524,9 @@ function sliceFn(input, begin, end) { * * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being * saved as numbers and not strings. + * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e. + * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to + * other values. * * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort. * @param {(Function|string|Array.)=} expression - A predicate (or list of @@ -22078,12 +22148,10 @@ function ngDirective(directive) { * @restrict E * * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when + * Modifies the default behavior of the html a tag so that the default action is prevented when * the href attribute is empty. * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `
Add Item` + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. */ var htmlAnchorDirective = valueFn({ restrict: 'E', @@ -22533,10 +22601,11 @@ forEach(['src', 'srcset', 'href'], function(attrName) { attr.$set(name, value); - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // Support: IE 9-11 only + // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. + // We use attr[attrName] value since $set can sanitize the url. if (msie && propName) element.prop(propName, attr[name]); }); } @@ -22544,7 +22613,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true +/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS */ var nullFormCtrl = { $addControl: noop, @@ -22555,6 +22624,7 @@ var nullFormCtrl = { $setPristine: noop, $setSubmitted: noop }, +PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { @@ -22605,22 +22675,28 @@ function nullFormRenameControl(control, name) { */ //asks for $scope to fool the BC controller module FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; -function FormController(element, attrs, $scope, $animate, $interpolate) { - var form = this, - controls = []; +function FormController($element, $attrs, $scope, $animate, $interpolate) { + this.$$controls = []; // init state - form.$error = {}; - form.$$success = {}; - form.$pending = undefined; - form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; - form.$submitted = false; - form.$$parentForm = nullFormCtrl; + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; + this.$$element = $element; + this.$$animate = $animate; + + setupValidity(this); +} + +FormController.prototype = { /** * @ngdoc method * @name form.FormController#$rollbackViewValue @@ -22632,11 +22708,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is typically needed by the reset button of * a form that uses `ng-model-options` to pend updates. */ - form.$rollbackViewValue = function() { - forEach(controls, function(control) { + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { control.$rollbackViewValue(); }); - }; + }, /** * @ngdoc method @@ -22649,11 +22725,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - form.$commitViewValue = function() { - forEach(controls, function(control) { + $commitViewValue: function() { + forEach(this.$$controls, function(control) { control.$commitViewValue(); }); - }; + }, /** * @ngdoc method @@ -22676,29 +22752,29 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * For example, if an input control is added that is already `$dirty` and has `$error` properties, * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. */ - form.$addControl = function(control) { + $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + this.$$controls.push(control); if (control.$name) { - form[control.$name] = control; + this[control.$name] = control; } - control.$$parentForm = form; - }; + control.$$parentForm = this; + }, // Private API: rename a form control - form.$$renameControl = function(control, newName) { + $$renameControl: function(control, newName) { var oldName = control.$name; - if (form[oldName] === control) { - delete form[oldName]; + if (this[oldName] === control) { + delete this[oldName]; } - form[newName] = control; + this[newName] = control; control.$name = newName; - }; + }, /** * @ngdoc method @@ -22716,60 +22792,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * different from case to case. For example, removing the only `$dirty` control from a form may or * may not mean that the form is still `$dirty`. */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; } - forEach(form.$pending, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$error, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$$success, function(value, name) { - form.$setValidity(name, null, control); - }); + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); - arrayRemove(controls, control); + arrayRemove(this.$$controls, control); control.$$parentForm = nullFormCtrl; - }; - - - /** - * @ngdoc method - * @name form.FormController#$setValidity - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - addSetValidityMethod({ - ctrl: this, - $element: element, - set: function(object, property, controller) { - var list = object[property]; - if (!list) { - object[property] = [controller]; - } else { - var index = list.indexOf(controller); - if (index === -1) { - list.push(controller); - } - } - }, - unset: function(object, property, controller) { - var list = object[property]; - if (!list) { - return; - } - arrayRemove(list, controller); - if (list.length === 0) { - delete object[property]; - } - }, - $animate: $animate - }); + }, /** * @ngdoc method @@ -22781,13 +22823,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ - form.$setDirty = function() { - $animate.removeClass(element, PRISTINE_CLASS); - $animate.addClass(element, DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - form.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -22805,15 +22847,15 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function() { - $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); - form.$dirty = false; - form.$pristine = true; - form.$submitted = false; - forEach(controls, function(control) { + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { control.$setPristine(); }); - }; + }, /** * @ngdoc method @@ -22828,11 +22870,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Setting a form controls back to their untouched state is often useful when setting the form * back to its pristine state. */ - form.$setUntouched = function() { - forEach(controls, function(control) { + $setUntouched: function() { + forEach(this.$$controls, function(control) { control.$setUntouched(); }); - }; + }, /** * @ngdoc method @@ -22841,12 +22883,46 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its submitted state. */ - form.$setSubmitted = function() { - $animate.addClass(element, SUBMITTED_CLASS); - form.$submitted = true; - form.$$parentForm.$setSubmitted(); - }; -} + $setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + this.$$parentForm.$setSubmitted(); + } +}; + +/** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ +addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); + } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } +}); /** * @ngdoc directive @@ -23043,13 +23119,13 @@ var formDirectiveFactory = function(isNgForm) { event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].addEventListener('submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].removeEventListener('submit', handleFormSubmission); }, 0, false); }); } @@ -23094,6 +23170,111 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); + + +// helper methods +function setupValidity(instance) { + instance.$$classCache = {}; + instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); +} +function addSetValidityMethod(context) { + var clazz = context.clazz, + set = context.set, + unset = context.unset; + + clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet(this, '$pending', validationErrorKey, controller); + } else { + unsetAndCleanup(this, '$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(this.$error, validationErrorKey, controller); + set(this.$$success, validationErrorKey, controller); + } else { + set(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } + } + if (this.$pending) { + cachedToggleClass(this, PENDING_CLASS, true); + this.$valid = this.$invalid = undefined; + toggleValidationCss(this, '', null); + } else { + cachedToggleClass(this, PENDING_CLASS, false); + this.$valid = isObjectEmpty(this.$error); + this.$invalid = !this.$valid; + toggleValidationCss(this, '', this.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in this.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (this.$pending && this.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (this.$error[validationErrorKey]) { + combinedState = false; + } else if (this.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(this, validationErrorKey, combinedState); + this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); + }; + + function createAndSet(ctrl, name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(ctrl, name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(ctrl, className, switchValue) { + if (switchValue && !ctrl.$$classCache[className]) { + ctrl.$$animate.addClass(ctrl.$$element, className); + ctrl.$$classCache[className] = true; + } else if (!switchValue && ctrl.$$classCache[className]) { + ctrl.$$animate.removeClass(ctrl.$$element, className); + ctrl.$$classCache[className] = false; + } + } + + function toggleValidationCss(ctrl, validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; +} + /* global VALID_CLASS: false, INVALID_CLASS: false, @@ -23115,11 +23296,11 @@ var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+- // 7. Path // 8. Query // 9. Fragment -// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999 -var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; +// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999 +var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; // eslint-disable-next-line max-len -var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; +var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; +var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/; var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/; @@ -23773,7 +23954,17 @@ var inputType = { * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * Can be interpolated. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * Can be interpolated. + * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint. + * Can be interpolated. + * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint, + * but does not trigger HTML5 native validation. Takes an expression. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -24128,28 +24319,6 @@ var inputType = { * @description * Native range input with validation and transformation. * - *
- *

- * In v1.5.9+, in order to avoid interfering with already existing, custom directives for - * `input[range]`, you need to let Angular know that you want to enable its built-in support. - * You can do this by adding the `ng-input-range` attribute to the input element. E.g.: - * `` - *


- *

- * Input elements without the `ng-input-range` attibute will continue to be treated the same - * as in previous versions (e.g. their model value will be a string not a number and Angular - * will not take `min`/`max`/`step` attributes and properties into account). - *


- *

- * **Note:** From v1.6.x onwards, the support for `input[range]` will be always enabled and - * the `ng-input-range` attribute will have no effect. - *


- *

- * This documentation page refers to elements which have the built-in support enabled; i.e. - * elements _with_ the `ng-input-range` attribute. - *

- *
- * * The model for the range input must always be a `Number`. * * IE9 and other browsers that do not support the `range` type fall back @@ -24171,7 +24340,7 @@ var inputType = { * * Since the element value should always reflect the current model value, a range input * will set the bound ngModel expression to the value that the browser has set for the - * input element. For example, in the following input ``, + * input element. For example, in the following input ``, * if the application sets `model.value = null`, the browser will set the input to `'50'`. * Angular will then set the model to `50`, to prevent input and model value being out of sync. * @@ -24190,12 +24359,10 @@ var inputType = { * instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step` * error on the input, and set the model to `undefined`. * - * Note that `input[range]` is not compatible with `ngMax`, `ngMin`, and `ngStep`, because they do + * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do * not set the `min` and `max` attributes, which means that the browser won't automatically adjust * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. * - * @param ngInputRange The presense of this attribute enables the built-in support for - * `input[range]`. * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation to ensure that the value entered is greater @@ -24206,6 +24373,9 @@ var inputType = { * Can be interpolated. * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. * * @example @@ -24220,7 +24390,7 @@ var inputType = {
- Model as range: + Model as range:
Model as number:
Min:
@@ -24246,7 +24416,7 @@ var inputType = { }]); - Model as range: + Model as range:
Model as number:
Min:
@@ -24339,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) { @@ -24532,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; @@ -24686,8 +24856,8 @@ function isValidForStep(viewValue, stepBase, step) { function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var minVal; var maxVal; @@ -24715,6 +24885,20 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { 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) { @@ -24878,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); } }; @@ -24893,6 +25083,9 @@ function radioInputType(scope, element, attr, ctrl) { ctrl.$render = function() { var value = attr.value; + if (doTrim) { + value = trim(value); + } element[0].checked = (value === ctrl.$viewValue); }; @@ -25138,11 +25331,7 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', link: { pre: function(scope, element, attr, ctrls) { if (ctrls[0]) { - var type = lowercase(attr.type); - if ((type === 'range') && !attr.hasOwnProperty('ngInputRange')) { - type = 'text'; - } - (inputType[type] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, $browser, $filter, $parse); } } @@ -25158,21 +25347,19 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; * @name ngValue * * @description - * Binds the given expression to the value of `