spec/dummy/tmp/cache/assets/development/sprockets/a1f1fbb9115cf49e86ee8970633e5e7d in hyper_admin-0.3.0 vs spec/dummy/tmp/cache/assets/development/sprockets/a1f1fbb9115cf49e86ee8970633e5e7d in hyper_admin-0.4.0

- old
+ new

@@ -1,8 +1,8 @@ {I" class:ETI"BundledAsset;FI"logical_path;TI"hyper_admin/application.js;TI" pathname;TI"a/home/sindre/Code/hyper/hyper_admin/app/assets/javascripts/hyper_admin/application.js.coffee;FI"content_type;TI"application/javascript;TI" -mtime;Tl+\¥TI" length;Ti¦—I" digest;TI"%2a37ae90ca90a2458abe54db2297a760;FI" source;TI"¦—/*! +mtime;Tl+ Ü)TI" length;TiÊ÷I" digest;TI"%2819f2e104dcc78c2b03e11c4974dda5;FI" source;TI"Ê÷/*! * jQuery JavaScript Library v1.11.1 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ @@ -10754,11 +10754,11 @@ }); } })( jQuery ); /** - * @license AngularJS v1.3.0-beta.13 + * @license AngularJS v1.3.0-beta.17 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, document, undefined) {'use strict'; @@ -10824,11 +10824,11 @@ return arg; } return match; }); - message = message + '\nhttp://errors.angularjs.org/1.3.0-beta.13/' + + message = message + '\nhttp://errors.angularjs.org/1.3.0-beta.17/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + encodeURIComponent(stringify(arguments[i])); } @@ -10836,93 +10836,92 @@ return new Error(message); }; } /* We need to tell jshint what variables are being exported */ -/* global - -angular, - -msie, - -jqLite, - -jQuery, - -slice, - -push, - -toString, - -ngMinErr, - -angularModule, - -nodeName_, - -uid, - -REGEX_STRING_REGEXP, +/* global angular: true, + msie: true, + jqLite: true, + jQuery: true, + slice: true, + push: true, + toString: true, + ngMinErr: true, + angularModule: true, + nodeName_: true, + uid: true, + REGEX_STRING_REGEXP: true, + VALIDITY_STATE_PROPERTY: true, - -lowercase, - -uppercase, - -manualLowercase, - -manualUppercase, - -nodeName_, - -isArrayLike, - -forEach, - -sortedKeys, - -forEachSorted, - -reverseParams, - -nextUid, - -setHashKey, - -extend, - -int, - -inherit, - -noop, - -identity, - -valueFn, - -isUndefined, - -isDefined, - -isObject, - -isString, - -isNumber, - -isDate, - -isArray, - -isFunction, - -isRegExp, - -isWindow, - -isScope, - -isFile, - -isBlob, - -isBoolean, - -trim, - -isElement, - -makeMap, - -map, - -size, - -includes, - -indexOf, - -arrayRemove, - -isLeafNode, - -copy, - -shallowCopy, - -equals, - -csp, - -concat, - -sliceArgs, - -bind, - -toJsonReplacer, - -toJson, - -fromJson, - -toBoolean, - -startingTag, - -tryDecodeURIComponent, - -parseKeyValue, - -toKeyValue, - -encodeUriSegment, - -encodeUriQuery, - -angularInit, - -bootstrap, - -snake_case, - -bindJQuery, - -assertArg, - -assertArgFn, - -assertNotHasOwnProperty, - -getter, - -getBlockElements, - -hasOwnProperty, - + lowercase: true, + uppercase: true, + manualLowercase: true, + manualUppercase: true, + nodeName_: true, + isArrayLike: true, + forEach: true, + sortedKeys: true, + forEachSorted: true, + reverseParams: true, + nextUid: true, + setHashKey: true, + extend: true, + int: true, + inherit: true, + noop: true, + identity: true, + valueFn: true, + isUndefined: true, + isDefined: true, + isObject: true, + isString: true, + isNumber: true, + isDate: true, + isArray: true, + isFunction: true, + isRegExp: true, + isWindow: true, + isScope: true, + isFile: true, + isBlob: true, + isBoolean: true, + isPromiseLike: true, + trim: true, + isElement: true, + makeMap: true, + map: true, + size: true, + includes: true, + indexOf: true, + arrayRemove: true, + isLeafNode: true, + copy: true, + shallowCopy: true, + equals: true, + csp: true, + concat: true, + sliceArgs: true, + bind: true, + toJsonReplacer: true, + toJson: true, + fromJson: true, + startingTag: true, + tryDecodeURIComponent: true, + parseKeyValue: true, + toKeyValue: true, + encodeUriSegment: true, + encodeUriQuery: true, + angularInit: true, + bootstrap: true, + snake_case: true, + bindJQuery: true, + assertArg: true, + assertArgFn: true, + assertNotHasOwnProperty: true, + getter: true, + getBlockElements: true, + hasOwnProperty: true, */ //////////////////////////////////// /** @@ -10940,10 +10939,14 @@ * <div doc-module-components="ng"></div> */ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; +// The name of a form control's ValidityState property. +// This is used so that it's possible for internal tests to create mock ValidityStates. +var VALIDITY_STATE_PROPERTY = 'validity'; + /** * @ngdoc function * @name angular.lowercase * @module ng * @kind function @@ -11076,16 +11079,16 @@ // 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))) { iterator.call(context, obj[key], key); } } - } else if (obj.forEach && obj.forEach !== forEach) { - obj.forEach(iterator, context); - } else if (isArrayLike(obj)) { + } else if (isArray(obj) || isArrayLike(obj)) { for (key = 0, length = obj.length; key < length; key++) { iterator.call(context, obj[key], key); } + } else if (obj.forEach && obj.forEach !== forEach) { + obj.forEach(iterator, context); } else { for (key in obj) { if (obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key); } @@ -11402,10 +11405,15 @@ function isBoolean(value) { return typeof value === 'boolean'; } +function isPromiseLike(obj) { + return obj && isFunction(obj.then); +} + + var trim = (function() { // native trim is way faster: http://jsperf.com/angular-trim-test // but IE doesn't have it... :-( // TODO: we should move this into IE/ES5 polyfill if (!String.prototype.trim) { @@ -11450,16 +11458,18 @@ if (msie < 9) { nodeName_ = function(element) { element = element.nodeName ? element : element[0]; - return (element.scopeName && element.scopeName != 'HTML') - ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; + return lowercase( + (element.scopeName && element.scopeName != 'HTML') + ? element.scopeName + ':' + element.nodeName : element.nodeName + ); }; } else { nodeName_ = function(element) { - return element.nodeName ? element.nodeName : element[0].nodeName; + return lowercase(element.nodeName ? element.nodeName : element[0].nodeName); }; } function map(obj, iterator, context) { @@ -11518,14 +11528,14 @@ return value; } function isLeafNode (node) { if (node) { - switch (node.nodeName) { - case "OPTION": - case "PRE": - case "TITLE": + switch (nodeName_(node)) { + case "option": + case "pre": + case "title": return true; } } return false; } @@ -11550,13 +11560,13 @@ * @param {(Object|Array)=} destination Destination into which the source is copied. If * provided, must be of the same type as `source`. * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - <example> + <example module="copyExample"> <file name="index.html"> - <div ng-controller="Controller"> + <div ng-controller="ExampleController"> <form novalidate class="simple-form"> Name: <input type="text" ng-model="user.name" /><br /> E-mail: <input type="email" ng-model="user.email" /><br /> Gender: <input type="radio" ng-model="user.gender" value="male" />male <input type="radio" ng-model="user.gender" value="female" />female<br /> @@ -11566,25 +11576,26 @@ <pre>form = {{user | json}}</pre> <pre>master = {{master | json}}</pre> </div> <script> - function Controller($scope) { - $scope.master= {}; + angular.module('copyExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.master= {}; - $scope.update = function(user) { - // Example with 1 argument - $scope.master= angular.copy(user); - }; + $scope.update = function(user) { + // Example with 1 argument + $scope.master= angular.copy(user); + }; - $scope.reset = function() { - // Example with 2 arguments - angular.copy($scope.master, $scope.user); - }; + $scope.reset = function() { + // Example with 2 arguments + angular.copy($scope.master, $scope.user); + }; - $scope.reset(); - } + $scope.reset(); + }]); </script> </file> </example> */ function copy(source, destination, stackSource, stackDest) { @@ -11599,13 +11610,15 @@ if (isArray(source)) { destination = copy(source, [], stackSource, stackDest); } else if (isDate(source)) { destination = new Date(source.getTime()); } else if (isRegExp(source)) { - destination = new RegExp(source.source); + destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + destination.lastIndex = source.lastIndex; } else if (isObject(source)) { - destination = copy(source, {}, stackSource, stackDest); + var emptyObject = Object.create(Object.getPrototypeOf(source)); + destination = copy(source, emptyObject, stackSource, stackDest); } } } else { if (source === destination) throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); @@ -11636,16 +11649,18 @@ var h = destination.$$hashKey; forEach(destination, function(value, key) { delete destination[key]; }); for ( var key in source) { - result = copy(source[key], null, stackSource, stackDest); - if (isObject(source[key])) { - stackSource.push(source[key]); - stackDest.push(result); + if(source.hasOwnProperty(key)) { + result = copy(source[key], null, stackSource, stackDest); + if (isObject(source[key])) { + stackSource.push(source[key]); + stackDest.push(result); + } + destination[key] = result; } - destination[key] = result; } setHashKey(destination,h); } } @@ -11748,18 +11763,31 @@ } } return false; } +var csp = function() { + if (isDefined(csp.isActive_)) return csp.isActive_; -function csp() { - return (document.securityPolicy && document.securityPolicy.isActive) || - (document.querySelector && - !!(document.querySelector('[ng-csp]') || document.querySelector('[data-ng-csp]'))); -} + var active = !!(document.querySelector('[ng-csp]') || + document.querySelector('[data-ng-csp]')); + if (!active) { + try { + /* jshint -W031, -W054 */ + new Function(''); + /* jshint +W031, +W054 */ + } catch (e) { + active = true; + } + } + return (csp.isActive_ = active); +}; + + + function concat(array1, array2, index) { return array1.concat(slice.call(array2, index)); } function sliceArgs(args, startIndex) { @@ -11861,22 +11889,10 @@ ? JSON.parse(json) : json; } -function toBoolean(value) { - if (typeof value === 'function') { - value = true; - } else if (value && value.length !== 0) { - var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); - } else { - value = false; - } - return value; -} - /** * @returns {string} Returns the string representation of the element. */ function startingTag(element) { element = jqLite(element).clone(); @@ -11925,15 +11941,15 @@ */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; forEach((keyValue || "").split('&'), function(keyValue) { if ( keyValue ) { - key_value = keyValue.split('='); + key_value = keyValue.replace(/\+/g,'%20').split('='); key = tryDecodeURIComponent(key_value[0]); if ( isDefined(key) ) { var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; - if (!obj[key]) { + if (!hasOwnProperty.call(obj, key)) { obj[key] = val; } else if(isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key],val]; @@ -12001,11 +12017,11 @@ } var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; function getNgAttribute(element, ngAttr) { - var attr, i, ii = ngAttrPrefixes.length, j, jj; + var attr, i, ii = ngAttrPrefixes.length; element = jqLite(element); for (i=0; i<ii; ++i) { attr = ngAttrPrefixes[i] + ngAttr; if (isString(attr = element.attr(attr))) { return attr; @@ -12140,50 +12156,30 @@ } </file> </example> */ function angularInit(element, bootstrap) { - var elements = [element], - appElement, + var appElement, module, - config = {}, - names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], - options = { - 'boolean': ['strict-di'] - }, - NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + config = {}; - function append(element) { - element && elements.push(element); - } + // The element `element` has priority over any other element + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; - forEach(names, function(name) { - names[name] = true; - append(document.getElementById(name)); - name = name.replace(':', '\\:'); - if (element.querySelectorAll) { - forEach(element.querySelectorAll('.' + name), append); - forEach(element.querySelectorAll('.' + name + '\\:'), append); - forEach(element.querySelectorAll('[' + name + ']'), append); + if (!appElement && element.hasAttribute && element.hasAttribute(name)) { + appElement = element; + module = element.getAttribute(name); } }); + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + var candidate; - forEach(elements, function(element) { - if (!appElement) { - var className = ' ' + element.className + ' '; - var match = NG_APP_CLASS_REGEXP.exec(className); - if (match) { - appElement = element; - module = (match[2] || '').replace(/\s+/g, ','); - } else { - forEach(element.attributes, function(attr) { - if (!appElement && names[attr.name]) { - appElement = element; - module = attr.value; - } - }); - } + if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { + appElement = candidate; + module = candidate.getAttribute(name); } }); if (appElement) { config.strictDi = getNgAttribute(appElement, "strict-di") !== null; bootstrap(appElement, module ? [module] : [], config); @@ -12202,11 +12198,11 @@ * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. * They must use {@link ng.directive:ngApp ngApp}. * * Angular will detect if it has been loaded into the browser more than once and only allow the * first loaded script to be bootstrapped and will report a warning to the browser console for - * each of the subsequent scripts. This prevents strange results in applications, where otherwise + * each of the subsequent scripts. This prevents strange results in applications, where otherwise * multiple instances of Angular try to work on the DOM. * * ```html * <!doctype html> * <html> @@ -12345,11 +12341,11 @@ if (acceptArrayAnnotation && isArray(arg)) { arg = arg[arg.length - 1]; } assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; } /** * throw error if the name given is hasOwnProperty @@ -12727,92 +12723,92 @@ }; }); } -/* global - angularModule: true, - version: true, +/* global angularModule: true, + version: true, - $LocaleProvider, - $CompileProvider, + $LocaleProvider, + $CompileProvider, - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCspDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - patternDirective, - patternDirective, - requiredDirective, - requiredDirective, - minlengthDirective, - minlengthDirective, - maxlengthDirective, - maxlengthDirective, - ngValueDirective, - ngModelOptionsDirective, - ngAttributeAliasDirectives, - ngEventDirectives, + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + styleDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCspDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + patternDirective, + patternDirective, + requiredDirective, + requiredDirective, + minlengthDirective, + minlengthDirective, + maxlengthDirective, + maxlengthDirective, + ngValueDirective, + ngModelOptionsDirective, + ngAttributeAliasDirectives, + ngEventDirectives, - $AnchorScrollProvider, - $AnimateProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $InterpolateProvider, - $IntervalProvider, - $HttpProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TimeoutProvider, - $$RAFProvider, - $$AsyncCallbackProvider, - $WindowProvider + $AnchorScrollProvider, + $AnimateProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DocumentProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpBackendProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, + $WindowProvider */ /** * @ngdoc object @@ -12827,15 +12823,15 @@ * - `minor` – `{number}` – Minor version number, such as "9". * - `dot` – `{number}` – Dot version number, such as "18". * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.3.0-beta.13', // all of these placeholder strings will be replaced by grunt's + full: '1.3.0-beta.17', // all of these placeholder strings will be replaced by grunt's major: 1, // package task minor: 3, dot: 0, - codeName: 'idiosyncratic-numerification' + codeName: 'turing-autocompletion' }; function publishExternalAPI(angular){ extend(angular, { @@ -12949,10 +12945,11 @@ $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $$q: $$QProvider, $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, @@ -12962,17 +12959,15 @@ }); } ]); } -/* global - - -JQLitePrototype, - -addEventListenerFn, - -removeEventListenerFn, - -BOOLEAN_ATTR, - -ALIASED_ATTR +/* global JQLitePrototype: true, + addEventListenerFn: true, + removeEventListenerFn: true, + BOOLEAN_ATTR: true, + ALIASED_ATTR: true, */ ////////////////////////////////// //JQLite ////////////////////////////////// @@ -13123,10 +13118,16 @@ function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } +function jqLiteAcceptsData(node) { + // The window object can accept data but has no nodeType + // Otherwise we are only interested in elements (1) and documents (9) + return !node.nodeType || node.nodeType === 1 || node.nodeType === 9; +} + function jqLiteBuildFragment(html, context) { var elem, tmp, tag, wrap, fragment = context.createDocumentFragment(), nodes = [], i; @@ -13201,16 +13202,20 @@ function jqLiteClone(element) { return element.cloneNode(true); } -function jqLiteDealoc(element){ - jqLiteRemoveData(element); - var childElement; - for ( var i = 0, children = element.children, l = (children && children.length) || 0; i < l; i++) { - childElement = children[i]; - jqLiteDealoc(childElement); +function jqLiteDealoc(element, onlyDescendants){ + if (!onlyDescendants) jqLiteRemoveData(element); + + if (element.childNodes && element.childNodes.length) { + // we use querySelectorAll because documentFragments don't have getElementsByTagName + var descendants = element.getElementsByTagName ? element.getElementsByTagName('*') : + element.querySelectorAll ? element.querySelectorAll('*') : []; + for (var i = 0, l = descendants.length; i < l; i++) { + jqLiteRemoveData(descendants[i]); + } } } function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); @@ -13270,34 +13275,33 @@ return expandoStore && expandoStore[key]; } } function jqLiteData(element, key, value) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); + if (jqLiteAcceptsData(element)) { + var data = jqLiteExpandoStore(element, 'data'), + isSetter = isDefined(value), + keyDefined = !isSetter && isDefined(key), + isSimpleGetter = keyDefined && !isObject(key); - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } + if (!data && !isSimpleGetter) { + jqLiteExpandoStore(element, 'data', data = {}); + } - if (isSetter) { - // set data only on Elements and Documents - if (element.nodeType === 1 || element.nodeType === 9) { + if (isSetter) { data[key] = value; - } - } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. - return data && data[key]; + } else { + if (keyDefined) { + if (isSimpleGetter) { + // don't create data in this case. + return data && data[key]; + } else { + extend(data, key); + } } else { - extend(data, key); + return data; } - } else { - return data; } } } function jqLiteHasClass(element, selector) { @@ -13347,10 +13351,14 @@ var length = elements.length; // if an Array or NodeList and not a Window if (typeof length === 'number' && elements.window !== elements) { if (length) { + if (elements.item) { + // convert NodeList to an Array to make PhantomJS 1.x happy + elements = slice.call(elements); + } push.apply(root, elements); } } else { root[root.length++] = elements; } @@ -13362,36 +13370,31 @@ function jqLiteController(element, name) { return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); } function jqLiteInheritedData(element, name, value) { - element = jqLite(element); - // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); + if(element.nodeType == 9) { + element = element.documentElement; } var names = isArray(name) ? name : [name]; - while (element.length) { - var node = element[0]; + while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = element.data(names[i])) !== undefined) return value; + if ((value = jqLite.data(element, names[i])) !== undefined) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM // to lookup parent controllers. - element = jqLite(node.parentNode || (node.nodeType === 11 && node.host)); + element = element.parentNode || (element.nodeType === 11 && element.host); } } function jqLiteEmpty(element) { - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); while (element.firstChild) { element.removeChild(element.firstChild); } } @@ -13444,11 +13447,11 @@ forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { BOOLEAN_ATTR[lowercase(value)] = value; }); var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[uppercase(value)] = true; + BOOLEAN_ELEMENTS[value] = true; }); var ALIASED_ATTR = { 'ngMinlength' : 'minlength', 'ngMaxlength' : 'maxlength', 'ngPattern' : 'pattern' @@ -13457,30 +13460,37 @@ function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; + return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; } function getAliasedAttrName(element, name) { var nodeName = element.nodeName; return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; } forEach({ data: jqLiteData, + removeData: jqLiteRemoveData +}, function(fn, name) { + JQLite[name] = fn; +}); + +forEach({ + data: jqLiteData, inheritedData: jqLiteInheritedData, scope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); }, isolateScope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); + return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, controller: jqLiteController, injector: function(element) { @@ -13567,11 +13577,11 @@ } })(), val: function(element, value) { if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { + if (element.multiple && nodeName_(element) === 'select') { var result = []; forEach(element.options, function (option) { if (option.selected) { result.push(option.value || option.text); } @@ -13585,13 +13595,11 @@ html: function(element, value) { if (isUndefined(value)) { return element.innerHTML; } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); element.innerHTML = value; }, empty: jqLiteEmpty }, function(fn, name){ @@ -13707,15 +13715,18 @@ // selector. ////////////////////////////////////////// forEach({ removeData: jqLiteRemoveData, - dealoc: jqLiteDealoc, - on: function onFn(element, type, fn, unsupported){ if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); + // Do not add event handlers to non-elements because they will not be cleaned up. + if (!jqLiteAcceptsData(element)) { + return; + } + var events = jqLiteExpandoStore(element, 'events'), handle = jqLiteExpandoStore(element, 'handle'); if (!events) jqLiteExpandoStore(element, 'events', events = {}); if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); @@ -13896,20 +13907,27 @@ }, clone: jqLiteClone, triggerHandler: function(element, eventName, eventData) { - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + // Copy event handlers in case event handlers array is modified during execution. + var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName], + eventFnsCopy = shallowCopy(eventFns || []); eventData = eventData || []; var event = [{ - preventDefault: noop, + preventDefault: function() { + this.defaultPrevented = true; + }, + isDefaultPrevented: function() { + return this.defaultPrevented === true; + }, stopPropagation: noop }]; - forEach(eventFns, function(fn) { + forEach(eventFnsCopy, function(fn) { fn.apply(element, event.concat(eventData)); }); } }, function(fn, name){ /** @@ -13946,20 +13964,20 @@ * * @param obj * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ -function hashKey(obj) { +function hashKey(obj, nextUidFn) { var objType = typeof obj, key; - if (objType == 'object' && obj !== null) { + if (objType == 'function' || (objType == 'object' && obj !== null)) { if (typeof (key = obj.$$hashKey) == 'function') { // must invoke on object to keep the right this key = obj.$$hashKey(); } else if (key === undefined) { - key = obj.$$hashKey = nextUid(); + key = obj.$$hashKey = (nextUidFn || nextUid)(); } } else { key = obj; } @@ -13967,37 +13985,43 @@ } /** * HashMap which can use objects as keys */ -function HashMap(array){ +function HashMap(array, isolatedUid) { + if (isolatedUid) { + var uid = 0; + this.nextUid = function() { + return ++uid; + }; + } forEach(array, this.put, this); } HashMap.prototype = { /** * Store key value pair * @param key key to store can be any type * @param value value to store can be any type */ put: function(key, value) { - this[hashKey(key)] = value; + this[hashKey(key, this.nextUid)] = value; }, /** * @param key * @returns {Object} the value for the key */ get: function(key) { - return this[hashKey(key)]; + return this[hashKey(key, this.nextUid)]; }, /** * Remove the key/value pair * @param key */ remove: function(key) { - var value = this[key = hashKey(key)]; + var value = this[key = hashKey(key, this.nextUid)]; delete this[key]; return value; } }; @@ -14022,11 +14046,11 @@ * // create an injector * var $injector = angular.injector(['ng']); * * // use the injector to kick off your application * // use the type inference to auto inject arguments, or use implicit injection - * $injector.invoke(function($rootScope, $compile, $document){ + * $injector.invoke(function($rootScope, $compile, $document) { * $compile($document)($rootScope); * $rootScope.$digest(); * }); * ``` * @@ -14083,11 +14107,11 @@ var $inject, fnText, argDecl, last; - if (typeof fn == 'function') { + if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { if (strictDi) { if (!isString(name) || !name) { @@ -14096,12 +14120,12 @@ throw $injectorMinErr('strictdi', '{0} is not using explicit annotation and cannot be invoked in strict mode', name); } fnText = fn.toString().replace(STRIP_COMMENTS, ''); argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { + arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); }); } fn.$inject = $inject; @@ -14132,11 +14156,11 @@ * The following always holds true: * * ```js * var $injector = angular.injector(); * expect($injector.get('$injector')).toBe($injector); - * expect($injector.invoke(function($injector){ + * expect($injector.invoke(function($injector) { * return $injector; * }).toBe($injector); * ``` * * # Injection Function Annotation @@ -14303,11 +14327,11 @@ /** - * @ngdoc object + * @ngdoc service * @name $provide * * @description * * The {@link auto.$provide $provide} service has a number of methods for registering components @@ -14610,11 +14634,11 @@ function createInjector(modulesToLoad, strictDi) { strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap(), + loadedModules = new HashMap([], true), providerCache = { $provide: { provider: supportObject(provider), factory: supportObject(factory), service: supportObject(service), @@ -14768,11 +14792,11 @@ path.shift(); } } } - function invoke(fn, self, locals, serviceName){ + function invoke(fn, self, locals, serviceName) { if (typeof locals === 'string') { serviceName = locals; locals = null; } @@ -14791,12 +14815,11 @@ locals && locals.hasOwnProperty(key) ? locals[key] : getService(key) ); } - if (!fn.$inject) { - // this means that we must be an array. + if (isArray(fn)) { fn = fn[length]; } // http://jsperf.com/angularjs-invoke-apply-vs-switch // #5388 @@ -14845,28 +14868,30 @@ * * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. * * @example - <example> + <example module="anchorScrollExample"> <file name="index.html"> - <div id="scrollArea" ng-controller="ScrollCtrl"> + <div id="scrollArea" ng-controller="ScrollController"> <a ng-click="gotoBottom()">Go to bottom</a> <a id="bottom"></a> You're at the bottom! </div> </file> <file name="script.js"> - function ScrollCtrl($scope, $location, $anchorScroll) { - $scope.gotoBottom = function (){ - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); + angular.module('anchorScrollExample', []) + .controller('ScrollController', ['$scope', '$location', '$anchorScroll', + function ($scope, $location, $anchorScroll) { + $scope.gotoBottom = function() { + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); - // call $anchorScroll() - $anchorScroll(); - }; - } + // call $anchorScroll() + $anchorScroll(); + }; + }]); </file> <file name="style.css"> #scrollArea { height: 350px; overflow: auto; @@ -14895,11 +14920,11 @@ // and IE can't convert NodeList to an array using [].slice // TODO(vojta): use filter if we change it to accept lists as well function getFirstAnchor(list) { var result = null; forEach(list, function(element) { - if (!result && lowercase(element.nodeName) === 'a') result = element; + if (!result && nodeName_(element) === 'a') result = element; }); return result; } function scroll() { @@ -15056,10 +15081,11 @@ enter : function(element, parent, after, done) { after ? after.after(element) : parent.prepend(element); async(done); + return noop; }, /** * * @ngdoc method @@ -15072,10 +15098,11 @@ * removed from the DOM */ leave : function(element, done) { element.remove(); async(done); + return noop; }, /** * * @ngdoc method @@ -15095,11 +15122,11 @@ * element has been moved to its new position */ move : function(element, parent, after, done) { // Do not remove element before insert. Removing will cause data associated with the // element to be dropped. Insert will implicitly do the remove. - this.enter(element, parent, after, done); + return this.enter(element, parent, after, done); }, /** * * @ngdoc method @@ -15112,17 +15139,18 @@ * @param {string} className the CSS class which will be added to the element * @param {Function=} done the callback function (if provided) that will be fired after the * className value has been added to the element */ addClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; + className = !isString(className) + ? (isArray(className) ? className.join(' ') : '') + : className; forEach(element, function (element) { jqLiteAddClass(element, className); }); async(done); + return noop; }, /** * * @ngdoc method @@ -15142,10 +15170,11 @@ isArray(className) ? className.join(' ') : ''; forEach(element, function (element) { jqLiteRemoveClass(element, className); }); async(done); + return noop; }, /** * * @ngdoc method @@ -15164,10 +15193,11 @@ forEach(element, function (element) { jqLiteAddClass(element, add); jqLiteRemoveClass(element, remove); }); async(done); + return noop; }, enabled : noop }; }]; @@ -15461,20 +15491,19 @@ * way) * * @returns {Object} Hash of all cookies (if called without any parameter) */ self.cookies = function(name, value) { - /* global escape: false, unescape: false */ var cookieLength, cookieArray, cookie, i, index; if (name) { if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + + cookieLength = (rawDocument.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + ';path=' + cookiePath).length + 1; // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: // - 300 cookies // - 20 cookies per unique domain @@ -15494,16 +15523,16 @@ for (i = 0; i < cookieArray.length; i++) { cookie = cookieArray[i]; index = cookie.indexOf('='); if (index > 0) { //ignore nameless cookies - name = unescape(cookie.substring(0, index)); + name = decodeURIComponent(cookie.substring(0, index)); // the first value that is seen for a cookie is the most // specific one. values for the same cookie name that // follow are for less specific paths. if (lastCookies[name] === undefined) { - lastCookies[name] = unescape(cookie.substring(index + 1)); + lastCookies[name] = decodeURIComponent(cookie.substring(index + 1)); } } } } return lastCookies; @@ -16072,10 +16101,17 @@ * ### Directive Definition Object * * The directive definition object provides instructions to the {@link ng.$compile * compiler}. The attributes are: * + * ### `multiElement` + * When this property is set to true, the HTML compiler will collect DOM nodes between + * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them + * together as the directive elements. It is recomended that this feature be used on directives + * which are not strictly behavioural (such as {@link api/ng.directive:ngClick ngClick}), and which + * do not manipulate or replace child nodes (such as {@link api/ng.directive:ngInclude ngInclude}). + * * #### `priority` * When there are multiple directives defined on a single DOM element, sometimes it * is necessary to specify the order in which the directives are applied. The `priority` is used * to sort the directives before their `compile` functions get called. Priority is defined as a * number. Directives with greater numerical `priority` are compiled first. Pre-link functions @@ -16123,11 +16159,11 @@ * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. * If no `attr` name is specified then the attribute name is assumed to be the same as the * local name. Given `<widget my-attr="count = count + value">` and widget definition of * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to * a function wrapper for the `count = count + value` expression. Often it's desirable to - * pass data from the isolated scope via an expression and to the parent scope, this can be + * pass data from the isolated scope via an expression to the parent scope, this can be * done by passing a map of local variable names and values into the expression wrapper fn. * For example, if the expression is `increment(amount)` then we can specify the amount value * by calling the `localFn` as `localFn({amount: 22})`. * * @@ -16167,11 +16203,11 @@ * * #### `restrict` * String of subset of `EACM` which restricts the directive to a specific directive * declaration style. If omitted, the default (attributes only) is used. * - * * `E` - Element name: `<my-directive></my-directive>` + * * `E` - Element name (default): `<my-directive></my-directive>` * * `A` - Attribute (default): `<div my-directive="exp"></div>` * * `C` - Class: `<div class="my-directive: exp;"></div>` * * `M` - Comment: `<!-- directive: my-directive exp -->` * * @@ -16187,20 +16223,22 @@ * processing. * * If no `type` is specified, then the type is considered to be html. * * #### `template` - * replace the current element with the contents of the HTML. The replacement process - * migrates all of the attributes / classes from the old element to the new one. See the - * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive - * Directives Guide} for an example. + * HTML markup that may: + * * Replace the contents of the directive's element (defualt). + * * Replace the directive's element itself (if `replace` is true - DEPRECATED). + * * Wrap the contents of the directive's element (if `transclude` is true). * - * You can specify `template` as a string representing the template or as a function which takes - * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and - * returns a string value representing the template. + * Value may be: * + * * A string. For example `<div red-on-hover>{{delete_str}}</div>`. + * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` + * function api below) and returns a string value. * + * * #### `templateUrl` * Same as `template` but the template is loaded from the specified URL. Because * the template loading is asynchronous the compilation/linking is suspended until the template * is loaded. * @@ -16209,15 +16247,18 @@ * a string value representing the url. In either case, the template URL is passed through {@link * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * * * #### `replace` ([*DEPRECATED*!], will be removed in next major release) - * specify where the template should be inserted. Defaults to `false`. + * specify what the template should replace. Defaults to `false`. * - * * `true` - the template will replace the current element. - * * `false` - the template will replace the contents of the current element. + * * `true` - the template will replace the directive's element. + * * `false` - the template will replace the contents of the directive's element. * + * The replacement process migrates all of the attributes / classes from the old element to the new + * one. See the {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive + * Directives Guide} for an example. * * #### `transclude` * compile the content of the element and make it available to the directive. * Typically used with {@link ng.directive:ngTransclude * ngTransclude}. The advantage of transclusion is that the linking function receives a @@ -16227,10 +16268,15 @@ * be bound to the parent (pre-`isolate`) scope. * * * `true` - transclude the content of the directive. * * `'element'` - transclude the whole element including any directives defined at lower priority. * + * <div class="alert alert-warning"> + * **Note:** When testing an element transclude directive you must not place the directive at the root of the + * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives + * Testing Transclusion Directives}. + * </div> * * #### `compile` * * ```js * function compile(tElement, tAttrs, transclude) { ... } @@ -16363,14 +16409,14 @@ * <div class="alert alert-warning"> * **Note**: Typically directives are registered with `module.directive`. The example below is * to illustrate how `$compile` works. * </div> * - <example module="compile"> + <example module="compileExample"> <file name="index.html"> <script> - angular.module('compile', [], function($compileProvider) { + angular.module('compileExample', [], function($compileProvider) { // configure new 'compile' directive by passing a directive // factory function. The factory function injects the '$compile' $compileProvider.directive('compile', function($compile) { // directive factory creates a link function return function(scope, element, attrs) { @@ -16390,19 +16436,18 @@ // we don't get into infinite loop compiling ourselves $compile(element.contents())(scope); } ); }; - }) - }); - - function Ctrl($scope) { + }); + }) + .controller('GreeterController', ['$scope', function($scope) { $scope.name = 'Angular'; $scope.html = 'Hello {{name}}'; - } + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="GreeterController"> <input ng-model="name"> <br> <textarea ng-model="html"></textarea> <br> <div compile="html"></div> </div> </file> @@ -16526,11 +16571,11 @@ } directive.priority = directive.priority || 0; directive.index = index; directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; + directive.restrict = directive.restrict || 'EA'; directives.push(directive); } catch (e) { $exceptionHandler(e); } }); @@ -16720,12 +16765,12 @@ } nodeName = nodeName_(this.$$element); // sanitize a[href] and img[src] values - if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')) { + if ((nodeName === 'a' && key === 'href') || + (nodeName === 'img' && key === 'src')) { this[key] = value = $$sanitizeUri(value, key === 'src'); } if (writeAttr !== false) { if (value === null || value === undefined) { @@ -16826,18 +16871,11 @@ forEach(transcludeControllers, function(instance, name) { $linkNode.data('$' + name + 'Controller', instance); }); - // Attach scope only to non-text nodes. - for(var i = 0, ii = $linkNode.length; i<ii; i++) { - var node = $linkNode[i], - nodeType = node.nodeType; - if (nodeType === 1 /* element */ || nodeType === 9 /* document */) { - $linkNode.eq(i).data('$scope', scope); - } - } + $linkNode.data('$scope', scope); if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); return $linkNode; }; @@ -16883,11 +16921,11 @@ ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext) : null; if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(jqLite(nodeList[i]), 'ng-scope'); + safeAddClass(attrs.$$element, 'ng-scope'); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !(childNodes = nodeList[i].childNodes) || !childNodes.length) @@ -16905,11 +16943,11 @@ // return a linking function if we have found anything, null otherwise return linkFnFound ? compositeLinkFn : null; function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, $node, childScope, i, ii, n, childBoundTranscludeFn; + var nodeLinkFn, childLinkFn, node, childScope, i, ii, n, childBoundTranscludeFn; // copy nodeList so that linking doesn't break due to live list updates. var nodeListLength = nodeList.length, stableNodeList = new Array(nodeListLength); for (i = 0; i < nodeListLength; i++) { @@ -16918,16 +16956,15 @@ for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { node = stableNodeList[n]; nodeLinkFn = linkFns[i++]; childLinkFn = linkFns[i++]; - $node = jqLite(node); if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - $node.data('$scope', childScope); + jqLite.data(node, '$scope', childScope); } else { childScope = scope; } if ( nodeLinkFn.transcludeOnThisElement ) { @@ -16991,39 +17028,45 @@ switch(nodeType) { case 1: /* Element */ // use the node name: <directive> addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, + for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; attr = nAttrs[j]; if (!msie || msie >= 8 || attr.specified) { name = attr.name; + value = trim(attr.value); + // support ngAttr attribute binding ngAttrName = directiveNormalize(name); - if (NG_ATTR_BINDING.test(ngAttrName)) { + if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { name = snake_case(ngAttrName.substr(6), '-'); } var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); + if (directiveIsMultiElement(directiveNName)) { + if (ngAttrName === directiveNName + 'Start') { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } } nName = directiveNormalize(name.toLowerCase()); attrsMap[nName] = name; - attrs[nName] = value = trim(attr.value); - if (getBooleanAttrName(node, nName)) { - attrs[nName] = true; // presence means true + if (isNgAttr || !attrs.hasOwnProperty(nName)) { + attrs[nName] = value; + if (getBooleanAttrName(node, nName)) { + attrs[nName] = true; // presence means true + } } addAttrInterpolateDirective(node, directives, value, nName); addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, attrEndName); } @@ -17219,16 +17262,16 @@ } if (directiveValue == 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; - $template = groupScan(compileNode, attrStart, attrEnd); + $template = $compileNode; $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); + replaceWith(jqCollection, sliceArgs($template), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: // - controllerDirectives - otherwise we'll create duplicates controllers @@ -17401,33 +17444,30 @@ function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; - if (compileNode === linkNode) { - attrs = templateAttrs; - } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); - } + attrs = (compileNode === linkNode) + ? templateAttrs + : shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); $element = attrs.$$element; if (newIsolateScopeDirective) { var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - var $linkNode = jqLite(linkNode); isolateScope = scope.$new(true); if (templateDirective && (templateDirective === newIsolateScopeDirective || templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $linkNode.data('$isolateScope', isolateScope) ; + $element.data('$isolateScope', isolateScope); } else { - $linkNode.data('$isolateScopeNoTemplate', isolateScope); + $element.data('$isolateScopeNoTemplate', isolateScope); } - safeAddClass($linkNode, 'ng-isolate-scope'); + safeAddClass($element, 'ng-isolate-scope'); forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { var match = definition.match(LOCAL_REGEXP) || [], attrName = match[3] || scopeName, optional = (match[2] == '?'), @@ -17467,25 +17507,24 @@ throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!", attrs[attrName], newIsolateScopeDirective.name); }; lastValue = isolateScope[scopeName] = parentGet(scope); - isolateScope.$watch(function parentValueWatch() { - var parentValue = parentGet(scope); + var unwatch = scope.$watch($parse(attrs[attrName], function parentValueWatch(parentValue) { if (!compare(parentValue, isolateScope[scopeName])) { // we are out of sync and need to copy if (!compare(parentValue, lastValue)) { // parent changed and it has precedence isolateScope[scopeName] = parentValue; } else { // if the parent can be assigned then do so parentSet(scope, parentValue = isolateScope[scopeName]); } } - parentValueWatch.$$unwatch = parentGet.$$unwatch; return lastValue = parentValue; - }, null, parentGet.literal); + }), null, parentGet.literal); + isolateScope.$on('$destroy', unwatch); break; case '&': parentGet = $parse(attrs[attrName]); isolateScope[scopeName] = function(locals) { @@ -17627,10 +17666,31 @@ return match; } /** + * looks up the directive and returns true if it is a multi-element directive, + * and therefore requires DOM nodes between -start and -end markers to be grouped + * together. + * + * @param {string} name name of the directive to look up. + * @returns true if directive was registered as multi-element. + */ + function directiveIsMultiElement(name) { + if (hasDirectives.hasOwnProperty(name)) { + for(var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i<ii; i++) { + directive = directives[i]; + if (directive.multiElement) { + return true; + } + } + } + return false; + } + + /** * When the element is replaced with HTML template then the new attributes * on the template need to be merged with the existing attributes in the DOM. * The desired effect is to have both of the attributes present. * * @param {object} dst destination attributes (original DOM) @@ -17817,13 +17877,10 @@ if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding'); return function textInterpolateLinkFn(scope, node) { var parent = node.parent(), bindings = parent.data('$binding') || []; - // Need to interpolate again in case this is using one-time bindings in multiple clones - // of transcluded templates. - interpolateFn = $interpolate(text); bindings.push(interpolateFn); parent.data('$binding', bindings); if (!hasCompileParent) safeAddClass(parent, 'ng-binding'); scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { node[0].nodeValue = value; @@ -17854,12 +17911,12 @@ return $sce.HTML; } var tag = nodeName_(node); // maction[xlink:href] can source SVG. It's not limited to <maction>. if (attrNormalizedName == "xlinkHref" || - (tag == "FORM" && attrNormalizedName == "action") || - (tag != "IMG" && (attrNormalizedName == "src" || + (tag == "form" && attrNormalizedName == "action") || + (tag != "img" && (attrNormalizedName == "src" || attrNormalizedName == "ngSrc"))) { return $sce.RESOURCE_URL; } } @@ -17869,11 +17926,11 @@ // no interpolation found -> ignore if (!interpolateFn) return; - if (name === "multiple" && nodeName_(node) === "SELECT") { + if (name === "multiple" && nodeName_(node) === "select") { throw $compileMinErr("selmulti", "Binding to the 'multiple' attribute is not supported. Element: {0}", startingTag(node)); } @@ -18085,10 +18142,11 @@ * This provider allows controller registration via the * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { var controllers = {}, + globals = false, CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; /** * @ngdoc method @@ -18105,11 +18163,20 @@ } else { controllers[name] = constructor; } }; + /** + * @ngdoc method + * @name $controllerProvider#allowGlobals + * @description If called, allows `$controller` to find controller constructors on `window` + */ + this.allowGlobals = function() { + globals = true; + }; + this.$get = ['$injector', '$window', function($injector, $window) { /** * @ngdoc service * @name $controller @@ -18119,11 +18186,12 @@ * controller constructor function. Otherwise it's considered to be a string which is used * to retrieve the controller constructor using the following steps: * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * check `window[constructor]` on the global `window` object + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) * * @param {Object} locals Injection locals for Controller. * @return {Object} Instance of given controller. * * @description @@ -18139,19 +18207,20 @@ match = expression.match(CNTRL_REG), constructor = match[1], identifier = match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + : getter(locals.$scope, constructor, true) || + (globals ? getter($window, constructor, true) : undefined); assertArgFn(expression, constructor, true); } instance = $injector.instantiate(expression, locals, constructor); if (identifier) { - if (!(locals && typeof locals.$scope == 'object')) { + if (!(locals && typeof locals.$scope === 'object')) { throw minErr('$controller')('noscp', "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", constructor || expression.name, identifier); } @@ -18170,22 +18239,23 @@ * * @description * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - <example> + <example module="documentExample"> <file name="index.html"> - <div ng-controller="MainCtrl"> + <div ng-controller="ExampleController"> <p>$document title: <b ng-bind="title"></b></p> <p>window.document title: <b ng-bind="windowTitle"></b></p> </div> </file> <file name="script.js"> - function MainCtrl($scope, $document) { - $scope.title = $document[0].title; - $scope.windowTitle = angular.element(window.document)[0].title; - } + angular.module('documentExample', []) + .controller('ExampleController', ['$scope', '$document', function($scope, $document) { + $scope.title = $document[0].title; + $scope.windowTitle = angular.element(window.document)[0].title; + }]); </file> </example> */ function $DocumentProvider(){ this.$get = ['$window', function(window){ @@ -18248,15 +18318,11 @@ i = line.indexOf(':'); key = lowercase(trim(line.substr(0, i))); val = trim(line.substr(i + 1)); if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } }); return parsed; } @@ -18314,16 +18380,43 @@ function isSuccess(status) { return 200 <= status && status < 300; } +/** + * @ngdoc provider + * @name $httpProvider + * @description + * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. + * */ function $HttpProvider() { var JSON_START = /^\s*(\[|\{[^\{])/, JSON_END = /[\}\]]\s*$/, PROTECTION_PREFIX = /^\)\]\}',?\n/, CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; + /** + * @ngdoc property + * @name $httpProvider#defaults + * @description + * + * Object containing default values for all {@link ng.$http $http} requests. + * + * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. + * + * - **`defaults.headers`** - {Object} - Default headers for all $http requests. + * Refer to {@link ng.$http#setting-http-headers $http} for documentation on + * setting default headers. + * - **`defaults.headers.common`** + * - **`defaults.headers.post`** + * - **`defaults.headers.put`** + * - **`defaults.headers.patch`** + **/ var defaults = this.defaults = { // transform incoming response data transformResponse: [function(data) { if (isString(data)) { // strip json vulnerability protection prefix @@ -18737,13 +18830,13 @@ * @property {Array.<Object>} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. * * * @example -<example> +<example module="httpExample"> <file name="index.html"> - <div ng-controller="FetchCtrl"> + <div ng-controller="FetchController"> <select ng-model="method"> <option>GET</option> <option>JSONP</option> </select> <input type="text" ng-model="url" size="80"/> @@ -18761,34 +18854,36 @@ <pre>http status code: {{status}}</pre> <pre>http response data: {{data}}</pre> </div> </file> <file name="script.js"> - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; + angular.module('httpExample', []) + .controller('FetchController', ['$scope', '$http', '$templateCache', + function($scope, $http, $templateCache) { + $scope.method = 'GET'; + $scope.url = 'http-hello.html'; - $scope.fetch = function() { - $scope.code = null; - $scope.response = null; + $scope.fetch = function() { + $scope.code = null; + $scope.response = null; - $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - success(function(data, status) { - $scope.status = status; - $scope.data = data; - }). - error(function(data, status) { - $scope.data = data || "Request failed"; - $scope.status = status; - }); - }; + $http({method: $scope.method, url: $scope.url, cache: $templateCache}). + success(function(data, status) { + $scope.status = status; + $scope.data = data; + }). + error(function(data, status) { + $scope.data = data || "Request failed"; + $scope.status = status; + }); + }; - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - } + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + }]); </file> <file name="http-hello.html"> Hello, $http! </file> <file name="protractor.js" type="protractor"> @@ -18838,11 +18933,11 @@ var serverRequest = function(config) { headers = config.headers; var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); // strip content-type if data is undefined - if (isUndefined(config.data)) { + if (isUndefined(reqData)) { forEach(headers, function(value, header) { if (lowercase(header) === 'content-type') { delete headers[header]; } }); @@ -18907,14 +19002,10 @@ reqHeaders = extend({}, config.headers), defHeaderName, lowercaseDefHeaderName, reqHeaderName; defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - // execute if header value is function - execHeaders(defHeaders); - execHeaders(reqHeaders); - // using for-in instead of forEach to avoid unecessary iteration after header has been found defaultHeadersIteration: for (defHeaderName in defHeaders) { lowercaseDefHeaderName = lowercase(defHeaderName); @@ -18925,10 +19016,12 @@ } reqHeaders[defHeaderName] = defHeaders[defHeaderName]; } + // execute if header value is a function for merged headers + execHeaders(reqHeaders); return reqHeaders; function execHeaders(headers) { var headerContent; @@ -18990,11 +19083,11 @@ * * @description * Shortcut method to perform `JSONP` request. * * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. + * The name of the callback should be the string `JSON_CALLBACK`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ createShortMethods('get', 'delete', 'head', 'jsonp'); @@ -19021,12 +19114,25 @@ * @param {string} url Relative or absolute URL specifying the destination of the request * @param {*} data Request content * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ - createShortMethodsWithData('post', 'put'); + /** + * @ngdoc method + * @name $http#patch + * + * @description + * Shortcut method to perform `PATCH` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethodsWithData('post', 'put', 'patch'); + /** * @ngdoc property * @name $http#defaults * * @description @@ -19090,11 +19196,11 @@ } if (cache) { cachedResp = cache.get(url); if (isDefined(cachedResp)) { - if (cachedResp.then) { + if (isPromiseLike(cachedResp)) { // cached request has already been sent, but there is no response yet cachedResp.then(removePendingReq, removePendingReq); return cachedResp; } else { // serving from cache @@ -19172,31 +19278,33 @@ } } function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (!isArray(value)) value = [value]; + if (!params) return url; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value)) return; + if (!isArray(value)) value = [value]; - forEach(value, function(v) { - if (isObject(v)) { - v = toJson(v); - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - if(parts.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + forEach(value, function(v) { + if (isObject(v)) { + if (isDate(v)){ + v = v.toISOString(); + } else if (isObject(v)) { + v = toJson(v); + } } - return url; - } - - + parts.push(encodeUriQuery(key) + '=' + + encodeUriQuery(v)); + }); + }); + if(parts.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + } + return url; + } }]; } function createXhr(method) { //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest @@ -19277,25 +19385,32 @@ // // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and // Safari respectively. if (xhr && xhr.readyState == 4) { var responseHeaders = null, - response = null; + response = null, + statusText = ''; if(status !== ABORTED) { responseHeaders = xhr.getAllResponseHeaders(); // responseText is the old-school way of retrieving response (supported by IE8 & 9) // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) response = ('response' in xhr) ? xhr.response : xhr.responseText; } + // Accessing statusText on an aborted xhr object will + // throw an 'c00c023f error' in IE9 and lower, don't touch it. + if (!(status === ABORTED && msie < 10)) { + statusText = xhr.statusText; + } + completeRequest(callback, status || xhr.status, response, responseHeaders, - xhr.statusText || ''); + statusText); } }; if (withCredentials) { xhr.withCredentials = true; @@ -19321,11 +19436,11 @@ xhr.send(post || null); } if (timeout > 0) { var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (timeout && timeout.then) { + } else if (isPromiseLike(timeout)) { timeout.then(timeoutRequest); } function timeoutRequest() { @@ -19515,13 +19630,13 @@ * var exp = $interpolate('{{greeting}} {{name}}!'); * expect(exp(context)).toEqual('Hello !'); * * // "allOrNothing" mode * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); - * expect(exp(context, true)).toBeUndefined(); + * expect(exp(context)).toBeUndefined(); * context.name = 'Angular'; - * expect(exp(context, true)).toEqual('Hello Angular!'); + * expect(exp(context)).toEqual('Hello Angular!'); * ``` * * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. * * ####Escaped Interpolation @@ -19585,21 +19700,20 @@ parseFns = [], textLength = text.length, hasInterpolation = false, hasText = false, exp, - concat = [], - lastValuesCache = { values: {}, results: {}}; + concat = []; while(index < textLength) { if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { if (index !== startIndex) hasText = true; separators.push(text.substring(index, startIndex)); exp = text.substring(startIndex + startSymbolLength, endIndex); expressions.push(exp); - parseFns.push($parse(exp)); + parseFns.push($parse(exp, parseStringifyInterceptor)); index = endIndex + endSymbolLength; hasInterpolation = true; } else { // we did not find an interpolation, so we have to add the remainder to the separators array if (index !== textLength) { @@ -19636,25 +19750,22 @@ if (!mustHaveExpression || hasInterpolation) { concat.length = separators.length + expressions.length; var compute = function(values) { for(var i = 0, ii = expressions.length; i < ii; i++) { + if (allOrNothing && isUndefined(values[i])) return; concat[2*i] = separators[i]; concat[(2*i)+1] = values[i]; } concat[2*ii] = separators[ii]; return concat.join(''); }; var getValue = function (value) { - if (trustedContext) { - value = $sce.getTrusted(trustedContext, value); - } else { - value = $sce.valueOf(value); - } - - return value; + return trustedContext ? + $sce.getTrusted(trustedContext, value) : + $sce.valueOf(value); }; var stringify = function (value) { if (value == null) { // null || undefined return ''; @@ -19674,68 +19785,53 @@ return value; }; return extend(function interpolationFn(context) { - var scopeId = (context && context.$id) || 'notAScope'; - var lastValues = lastValuesCache.values[scopeId]; - var lastResult = lastValuesCache.results[scopeId]; var i = 0; var ii = expressions.length; var values = new Array(ii); - var val; - var inputsChanged = lastResult === undefined ? true: false; - - // if we haven't seen this context before, initialize the cache and try to setup - // a cleanup routine that purges the cache when the scope goes away. - if (!lastValues) { - lastValues = []; - inputsChanged = true; - if (context && context.$on) { - context.$on('$destroy', function() { - lastValuesCache.values[scopeId] = null; - lastValuesCache.results[scopeId] = null; - }); - } - } - - try { - interpolationFn.$$unwatch = true; for (; i < ii; i++) { - val = getValue(parseFns[i](context)); - if (allOrNothing && isUndefined(val)) { - interpolationFn.$$unwatch = undefined; - return; - } - val = stringify(val); - if (val !== lastValues[i]) { - inputsChanged = true; - } - values[i] = val; - interpolationFn.$$unwatch = interpolationFn.$$unwatch && parseFns[i].$$unwatch; + values[i] = parseFns[i](context); } - if (inputsChanged) { - lastValuesCache.values[scopeId] = values; - lastValuesCache.results[scopeId] = lastResult = compute(values); - } + return compute(values); } catch(err) { var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString()); $exceptionHandler(newErr); } - return lastResult; }, { // all of these properties are undocumented for now exp: text, //just for compatibility with regular watchers created via $watch separators: separators, - expressions: expressions + expressions: expressions, + $$watchDelegate: function (scope, listener, objectEquality, deregisterNotifier) { + var lastValue; + return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { + var currValue = compute(values); + if (isFunction(listener)) { + listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); + } + lastValue = currValue; + }, objectEquality, deregisterNotifier); + } }); } + + function parseStringifyInterceptor(value) { + try { + return stringify(getValue(value)); + } catch(err) { + var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, + err.toString()); + $exceptionHandler(newErr); + } + } } /** * @ngdoc method @@ -19771,12 +19867,12 @@ return $interpolate; }]; } function $IntervalProvider() { - this.$get = ['$rootScope', '$window', '$q', - function($rootScope, $window, $q) { + this.$get = ['$rootScope', '$window', '$q', '$$q', + function($rootScope, $window, $q, $$q) { var intervals = {}; /** * @ngdoc service @@ -19811,29 +19907,31 @@ * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @returns {promise} A promise which will be notified on each iteration. * * @example - * <example module="time"> - * <file name="index.html"> - * <script> - * function Ctrl2($scope,$interval) { - * $scope.format = 'M/d/yy h:mm:ss a'; - * $scope.blood_1 = 100; - * $scope.blood_2 = 120; + * <example module="intervalExample"> + * <file name="index.html"> + * <script> + * angular.module('intervalExample', []) + * .controller('ExampleController', ['$scope', '$interval', + * function($scope, $interval) { + * $scope.format = 'M/d/yy h:mm:ss a'; + * $scope.blood_1 = 100; + * $scope.blood_2 = 120; * - * var stop; - * $scope.fight = function() { - * // Don't start a new fight if we are already fighting - * if ( angular.isDefined(stop) ) return; + * var stop; + * $scope.fight = function() { + * // Don't start a new fight if we are already fighting + * if ( angular.isDefined(stop) ) return; * * stop = $interval(function() { * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { - * $scope.blood_1 = $scope.blood_1 - 3; - * $scope.blood_2 = $scope.blood_2 - 4; + * $scope.blood_1 = $scope.blood_1 - 3; + * $scope.blood_2 = $scope.blood_2 - 4; * } else { - * $scope.stopFight(); + * $scope.stopFight(); * } * }, 100); * }; * * $scope.stopFight = function() { @@ -19844,26 +19942,25 @@ * }; * * $scope.resetFight = function() { * $scope.blood_1 = 100; * $scope.blood_2 = 120; - * } + * }; * * $scope.$on('$destroy', function() { - * // Make sure that the interval is destroyed too + * // Make sure that the interval nis destroyed too * $scope.stopFight(); * }); - * } - * - * angular.module('time', []) - * // Register the 'myCurrentTime' directive factory method. - * // We inject $interval and dateFilter service since the factory method is DI. - * .directive('myCurrentTime', function($interval, dateFilter) { + * }]) + * // Register the 'myCurrentTime' directive factory method. + * // We inject $interval and dateFilter service since the factory method is DI. + * .directive('myCurrentTime', ['$interval', 'dateFilter', + * function($interval, dateFilter) { * // return the directive link function. (compile function not needed) * return function(scope, element, attrs) { * var format, // date format - * stopTime; // so that we can cancel the time updates + * stopTime; // so that we can cancel the time updates * * // used to update the UI * function updateTime() { * element.text(dateFilter(new Date(), format)); * } @@ -19875,41 +19972,41 @@ * }); * * stopTime = $interval(updateTime, 1000); * * // listen on DOM destroy (removal) event, and cancel the next UI update - * // to prevent updating time ofter the DOM element was removed. + * // to prevent updating time after the DOM element was removed. * element.on('$destroy', function() { * $interval.cancel(stopTime); * }); * } - * }); - * </script> + * }]); + * </script> * - * <div> - * <div ng-controller="Ctrl2"> - * Date format: <input ng-model="format"> <hr/> - * Current time is: <span my-current-time="format"></span> - * <hr/> - * Blood 1 : <font color='red'>{{blood_1}}</font> - * Blood 2 : <font color='red'>{{blood_2}}</font> - * <button type="button" data-ng-click="fight()">Fight</button> - * <button type="button" data-ng-click="stopFight()">StopFight</button> - * <button type="button" data-ng-click="resetFight()">resetFight</button> - * </div> + * <div> + * <div ng-controller="ExampleController"> + * Date format: <input ng-model="format"> <hr/> + * Current time is: <span my-current-time="format"></span> + * <hr/> + * Blood 1 : <font color='red'>{{blood_1}}</font> + * Blood 2 : <font color='red'>{{blood_2}}</font> + * <button type="button" data-ng-click="fight()">Fight</button> + * <button type="button" data-ng-click="stopFight()">StopFight</button> + * <button type="button" data-ng-click="resetFight()">resetFight</button> * </div> + * </div> * - * </file> + * </file> * </example> */ function interval(fn, delay, count, invokeApply) { var setInterval = $window.setInterval, clearInterval = $window.clearInterval, - deferred = $q.defer(), - promise = deferred.promise, iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply); + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; count = isDefined(count) ? count : 0; promise.then(null, null, fn); @@ -19943,11 +20040,11 @@ * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { intervals[promise.$$intervalId].reject('canceled'); - clearInterval(promise.$$intervalId); + $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; return true; } return false; }; @@ -20453,18 +20550,21 @@ * 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. * - * @param {(string|Array<string>)=} paramValue If `search` is a string, then `paramValue` will - * override only a single search property. + * @param {(string|Array<string>|boolean)=} paramValue If `search` is a string, then `paramValue` + * will override only a single search property. * * If `paramValue` is an array, it will override the property of the `search` component of * `$location` specified via the first argument. * * If `paramValue` is `null`, the property specified via the first argument will be deleted. * + * If `paramValue` is `true`, the property specified via the first argument will be added with no + * value nor trailing equal sign. + * * @return {Object} If called with no arguments returns the parsed `search` object. If called with * one or more arguments returns `$location` object itself. */ search: function(search, paramValue) { switch (arguments.length) { @@ -20472,10 +20572,15 @@ return this.$$search; case 1: if (isString(search)) { this.$$search = parseKeyValue(search); } else if (isObject(search)) { + // remove object undefined or null properties + forEach(search, function(value, key) { + if (value == null) delete search[key]; + }); + this.$$search = search; } else { throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.'); } @@ -20577,11 +20682,11 @@ function $LocationProvider(){ var hashPrefix = '', html5Mode = false; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#hashPrefix * @description * @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 */ @@ -20593,11 +20698,11 @@ return hashPrefix; } }; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#html5Mode * @description * @param {boolean=} mode Use HTML5 strategy if available. * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -20662,11 +20767,11 @@ if (event.ctrlKey || event.metaKey || event.which == 2) return; var elm = jqLite(event.target); // traverse the DOM up to find first A tag - while (lowercase(elm[0].nodeName) !== 'a') { + while (nodeName_(elm[0]) !== 'a') { // ignore rewriting if no A tag (reached root element, or no parent - removed from document) if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } var absHref = elm.prop('href'); @@ -20793,19 +20898,20 @@ * * The default is to log `debug` messages. You can use * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - <example> + <example module="logExample"> <file name="script.js"> - function LogCtrl($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - } + angular.module('logExample', []) + .controller('LogController', ['$scope', '$log', function($scope, $log) { + $scope.$log = $log; + $scope.message = 'Hello World!'; + }]); </file> <file name="index.html"> - <div ng-controller="LogCtrl"> + <div ng-controller="LogController"> <p>Reload this page with open console, enter text and hit the log button...</p> Message: <input type="text" ng-model="message"/> <button ng-click="$log.log(message)">log</button> <button ng-click="$log.warn(message)">warn</button> @@ -20825,11 +20931,11 @@ function $LogProvider(){ var debug = true, self = this; /** - * @ngdoc property + * @ngdoc method * @name $logProvider#debugEnabled * @description * @param {boolean=} flag enable or disable debug level messages * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -20949,36 +21055,31 @@ // access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by // obtaining a reference to native JS functions such as the Function constructor. // // As an example, consider the following Angular expression: // -// {}.toString.constructor(alert("evil JS code")) +// {}.toString.constructor('alert("evil JS code")') // -// We want to prevent this type of access. For the sake of performance, during the lexing phase we -// disallow any "dotted" access to any member named "constructor". -// -// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor -// while evaluating the expression, which is a stronger but more expensive test. Since reflective -// calls are expensive anyway, this is not such a big deal compared to static dereferencing. -// // 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. // -// A developer could foil the name check by aliasing the Function constructor under a different -// name on the scope. -// // 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. + function ensureSafeMemberName(name, fullExpression) { - if (name === "constructor") { + if (name === "__defineGetter__" || name === "__defineSetter__" + || name === "__lookupGetter__" || name === "__lookupSetter__" + || name === "__proto__") { throw $parseMinErr('isecfld', - 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', - fullExpression); + 'Attempting to access a disallowed field in Angular expressions! ' + +'Expression: {0}', fullExpression); } return name; } function ensureSafeObject(obj, fullExpression) { @@ -20987,24 +21088,47 @@ if (obj.constructor === obj) { throw $parseMinErr('isecfn', 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isWindow(obj) - obj.document && obj.location && obj.alert && obj.setInterval) { + obj.window === obj) { throw $parseMinErr('isecwindow', 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isElement(obj) obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { throw $parseMinErr('isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression); + } else if (// block Object so that we can't get hold of dangerous Object.* methods + obj === Object) { + throw $parseMinErr('isecobj', + 'Referencing Object in Angular expressions is disallowed! Expression: {0}', + fullExpression); } } return obj; } +var CALL = Function.prototype.call; +var APPLY = Function.prototype.apply; +var BIND = Function.prototype.bind; + +function ensureSafeFunction(obj, fullExpression) { + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (obj === CALL || obj === APPLY || obj === BIND) { + throw $parseMinErr('isecff', + 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + } +} + var OPERATORS = { /* jshint bitwise : false */ 'null':function(){return null;}, 'true':function(){return true;}, 'false':function(){return false;}, @@ -21269,15 +21393,11 @@ this.throwError('Invalid unicode escape [\\u' + hex + ']'); this.index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } + string = string + (rep || ch); } escape = false; } else if (ch === '\\') { escape = true; } else if (ch === quote) { @@ -21620,10 +21740,11 @@ return extend(function(self, locals) { var o = obj(self, locals), i = indexFn(self, locals), v; + ensureSafeMemberName(i, parser.text); if (!o) return undefined; v = ensureSafeObject(o[i], parser.text); return v; }, { assign: function(self, value, locals) { @@ -21654,11 +21775,11 @@ args.push(argsFn[i](scope, locals)); } var fnPtr = fn(scope, locals, context) || noop; ensureSafeObject(context, parser.text); - ensureSafeObject(fnPtr, parser.text); + ensureSafeFunction(fnPtr, parser.text); // IE stupidity! (IE doesn't have apply for some native functions) var v = fnPtr.apply ? fnPtr.apply(context, args) : fnPtr(args[0], args[1], args[2], args[3], args[4]); @@ -21749,10 +21870,12 @@ obj[key] = propertyObj; } obj = propertyObj; } key = ensureSafeMemberName(element.shift(), fullExp); + ensureSafeObject(obj, fullExp); + ensureSafeObject(obj[key], fullExp); obj[key] = setValue; return setValue; } var getterFnCache = {}; @@ -21793,30 +21916,10 @@ return pathVal; }; } -function simpleGetterFn1(key0, fullExp) { - ensureSafeMemberName(key0, fullExp); - - return function simpleGetterFn1(scope, locals) { - if (scope == null) return undefined; - return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - }; -} - -function simpleGetterFn2(key0, key1, fullExp) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - - return function simpleGetterFn2(scope, locals) { - if (scope == null) return undefined; - scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - return scope == null ? undefined : scope[key1]; - }; -} - function getterFn(path, options, fullExp) { // Check whether the cache has this getter already. // We can use hasOwnProperty directly on the cache because we ensure, // see below, that the cache never stores a path called 'hasOwnProperty' if (getterFnCache.hasOwnProperty(path)) { @@ -21825,17 +21928,12 @@ var pathKeys = path.split('.'), pathKeysLength = pathKeys.length, fn; - // When we have only 1 or 2 tokens, use optimized special case closures. // http://jsperf.com/angularjs-parse-getter/6 - if (pathKeysLength === 1) { - fn = simpleGetterFn1(pathKeys[0], fullExp); - } else if (pathKeysLength === 2) { - fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); - } else if (options.csp) { + if (options.csp) { if (pathKeysLength < 6) { fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp); } else { fn = function(scope, locals) { var i = 0, val; @@ -21856,11 +21954,11 @@ code += 'if(s == null) return undefined;\n' + 's='+ (index // we simply dereference 's' on any .dot notation ? 's' // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n'; + : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '.' + key + ';\n'; }); code += 'return s;'; /* jshint -W054 */ var evaledFnGetter = new Function('s', 'k', code); // s=scope, k=locals @@ -21938,77 +22036,93 @@ this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { $parseOptions.csp = $sniffer.csp; - return function(exp) { - var parsedExpression, - oneTime; + return function(exp, interceptorFn) { + var parsedExpression, oneTime, + cacheKey = (exp = trim(exp)); switch (typeof exp) { case 'string': + if (cache.hasOwnProperty(cacheKey)) { + parsedExpression = cache[cacheKey]; + } else { + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } - exp = trim(exp); + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + parsedExpression = parser.parse(exp); - if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { - oneTime = true; - exp = exp.substring(2); - } + if (parsedExpression.constant) parsedExpression.$$watchDelegate = constantWatch; + else if (oneTime) parsedExpression.$$watchDelegate = oneTimeWatch; - if (cache.hasOwnProperty(exp)) { - return oneTime ? oneTimeWrapper(cache[exp]) : cache[exp]; + if (cacheKey !== 'hasOwnProperty') { + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + cache[cacheKey] = parsedExpression; + } } + return addInterceptor(parsedExpression, interceptorFn); - var lexer = new Lexer($parseOptions); - var parser = new Parser(lexer, $filter, $parseOptions); - parsedExpression = parser.parse(exp); - - if (exp !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[exp] = parsedExpression; - } - - if (parsedExpression.constant) { - parsedExpression.$$unwatch = true; - } - - return oneTime ? oneTimeWrapper(parsedExpression) : parsedExpression; - case 'function': - return exp; + return addInterceptor(exp, interceptorFn); default: - return noop; + return addInterceptor(noop, interceptorFn); } + }; - function oneTimeWrapper(expression) { - var stable = false, - lastValue; - oneTimeParseFn.literal = expression.literal; - oneTimeParseFn.constant = expression.constant; - oneTimeParseFn.assign = expression.assign; - return oneTimeParseFn; - - function oneTimeParseFn(self, locals) { - if (!stable) { - lastValue = expression(self, locals); - oneTimeParseFn.$$unwatch = isDefined(lastValue); - if (oneTimeParseFn.$$unwatch && self && self.$$postDigestQueue) { - self.$$postDigestQueue.push(function () { - // create a copy if the value is defined and it is not a $sce value - if ((stable = isDefined(lastValue)) && - (lastValue === null || !lastValue.$$unwrapTrustedValue)) { - lastValue = copy(lastValue, null); - } - }); + function oneTimeWatch(scope, listener, objectEquality, deregisterNotifier, parsedExpression) { + var unwatch, lastValue; + return unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener.apply(this, arguments); + } + if (isDefined(value)) { + scope.$$postDigest(function () { + if (isDefined(lastValue)) { + unwatch(); } - } - return lastValue; + }); } + }, objectEquality, deregisterNotifier); + } + + function constantWatch(scope, listener, objectEquality, deregisterNotifier, parsedExpression) { + var unwatch; + return unwatch = scope.$watch(function constantWatch(scope) { + return parsedExpression(scope); + }, function constantListener(value, old, scope) { + if (isFunction(listener)) { + listener.apply(this, arguments); + } + unwatch(); + }, objectEquality, deregisterNotifier); + } + + function addInterceptor(parsedExpression, interceptorFn) { + if (isFunction(interceptorFn)) { + var fn = function interceptedExpression(scope, locals) { + var value = parsedExpression(scope, locals); + var result = interceptorFn(value, scope, locals); + // we only return the interceptor's result if the + // initial value is defined (for bind-once) + return isDefined(value) ? result : value; + }; + fn.$$watchDelegate = parsedExpression.$$watchDelegate; + return fn; + } else { + return parsedExpression; } - }; + } }]; } /** * @ngdoc service @@ -22016,10 +22130,50 @@ * @requires $rootScope * * @description * A promise/deferred implementation 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, the other resembles ES6 promises to some degree. + * + * # $q constructor + * + * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` + * function as the first argument). This is similar to the native Promise implementation from ES6 Harmony, + * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + * + * While the constructor-style use is supported, not all of the supporting methods from Harmony promises are + * available yet. + * + * It can be used like so: + * + * ```js + * return $q(function(resolve, reject) { + * // perform some asynchronous operation, resolve or reject the promise when appropriate. + * setInterval(function() { + * if (pollStatus > 0) { + * resolve(polledValue); + * } else if (pollStatus < 0) { + * reject(polledValue); + * } else { + * pollStatus = pollAgain(function(value) { + * polledValue = value; + * }); + * } + * }, 10000); + * }). + * then(function(value) { + * // handle success + * }, function(reason) { + * // handle failure + * }); + * ``` + * + * Note, progress/notify callbacks are not currently supported via the ES6-style interface. + * + * However, the more traditional CommonJS style usage is still available, and documented below. + * * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an * interface for interacting with an object that represents the result of an action that is * performed asynchronously, and may or may not be finished at any given point in time. * * From the perspective of dealing with error handling, deferred and promise APIs are to @@ -22031,21 +22185,17 @@ * * function asyncGreet(name) { * var deferred = $q.defer(); * * setTimeout(function() { - * // since this fn executes async in a future turn of the event loop, we need to wrap - * // our code into an $apply call so that the model changes are properly observed. - * scope.$apply(function() { - * deferred.notify('About to greet ' + name + '.'); + * deferred.notify('About to greet ' + name + '.'); * - * if (okToGreet(name)) { - * deferred.resolve('Hello, ' + name + '!'); - * } else { - * deferred.reject('Greeting ' + name + ' is not allowed.'); - * } - * }); + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } * }, 1000); * * return deferred.promise; * } * @@ -22066,11 +22216,10 @@ * Additionally the promise api allows for composition that is very hard to do with the * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * - * * # The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. * * The purpose of the deferred object is to expose the associated Promise instance as well as APIs @@ -22175,20 +22324,33 @@ * // Propagate promise resolution to 'then' functions using $apply(). * $rootScope.$apply(); * expect(resolvedValue).toEqual(123); * })); * ``` + * + * @param {function(function, function)} resolver Function which is responsible for resolving or + * rejecting the newly created promise. The first parameteter is a function which resolves the + * promise, the second parameter is a function which rejects the promise. + * + * @returns {Promise} The newly created promise. */ function $QProvider() { this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); }, $exceptionHandler); }]; } +function $$QProvider() { + this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { + return qFactory(function(callback) { + $browser.defer(callback); + }, $exceptionHandler); + }]; +} /** * Constructs a promise manager. * * @param {function(Function)} nextTick Function for executing functions in the next turn. @@ -22315,11 +22477,11 @@ try { callbackOutput = (callback ||defaultCallback)(); } catch(e) { return makePromise(e, false); } - if (callbackOutput && isFunction(callbackOutput.then)) { + if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { return makePromise(value, isResolved); }, function(error) { return makePromise(error, false); }); @@ -22340,11 +22502,11 @@ return deferred; }; var ref = function(value) { - if (value && isFunction(value.then)) return value; + if (isPromiseLike(value)) return value; return { then: function(callback) { var result = defer(); nextTick(function() { result.resolve(callback(value)); @@ -22524,16 +22686,42 @@ } return deferred.promise; } - return { - defer: defer, - reject: reject, - when: when, - all: all + var $Q = function Q(resolver) { + if (!isFunction(resolver)) { + // TODO(@caitp): minErr this + throw new TypeError('Expected resolverFn'); + } + + if (!(this instanceof Q)) { + // More useful when $Q is the Promise itself. + return new Q(resolver); + } + + var deferred = defer(); + + function resolveFn(value) { + deferred.resolve(value); + } + + function rejectFn(reason) { + deferred.reject(reason); + } + + resolver(resolveFn, rejectFn); + + return deferred.promise; }; + + $Q.defer = defer; + $Q.reject = reject; + $Q.when = when; + $Q.all = all; + + return $Q; } function $$RAFProvider(){ //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || @@ -22879,24 +23067,29 @@ * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers * a call to the `listener`. * * - `string`: Evaluated as {@link guide/expression expression} * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. + * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value + * of `watchExpression` changes. * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as - * parameters. - * + * - `newVal` contains the current value of the `watchExpression` + * - `oldVal` contains the previous value of the `watchExpression` + * - `scope` refers to the current scope * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. + * @param {function()=} deregisterNotifier Function to call when the deregistration function + * get called. * @returns {function()} Returns a deregistration function for this listener. */ - $watch: function(watchExp, listener, objectEquality) { + $watch: function(watchExp, listener, objectEquality, deregisterNotifier) { + var get = compileToFn(watchExp, 'watch'); + + if (get.$$watchDelegate) { + return get.$$watchDelegate(this, listener, objectEquality, deregisterNotifier, get); + } var scope = this, - get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, @@ -22904,14 +23097,12 @@ eq: !!objectEquality }; lastDirtyWatch = null; - // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; + watcher.fn = noop; } if (!array) { array = scope.$$watchers = []; } @@ -22920,10 +23111,13 @@ array.unshift(watcher); return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; + if (isFunction(deregisterNotifier)) { + deregisterNotifier(); + } }; }, /** * @ngdoc method @@ -22932,11 +23126,11 @@ * * @description * 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 `watchCollection` array are observed via standard $watch operation and are examined on every + * - 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 `listener` is called whenever any expression in the `watchExpressions` array changes. * * @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually * watched using {@link ng.$rootScope.Scope#$watch $watch()} @@ -22946,50 +23140,55 @@ * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching * those of `watchExpression` * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching * those of `watchExpression` * The `scope` refers to the current scope. - * * @returns {function()} Returns a de-registration function for all listeners. */ $watchGroup: function(watchExpressions, listener) { var oldValues = new Array(watchExpressions.length); var newValues = new Array(watchExpressions.length); var deregisterFns = []; var changeCount = 0; var self = this; - var unwatchFlags = new Array(watchExpressions.length); - var unwatchCount = watchExpressions.length; + var masterUnwatch; + if (watchExpressions.length === 1) { + // Special case size of one + return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { + newValues[0] = value; + oldValues[0] = oldValue; + listener.call(this, newValues, (value === oldValue) ? newValues : oldValues, scope); + }); + } + forEach(watchExpressions, function (expr, i) { - var exprFn = $parse(expr); - deregisterFns.push(self.$watch(exprFn, function (value, oldValue) { + var unwatch = self.$watch(expr, function watchGroupSubAction(value, oldValue) { newValues[i] = value; oldValues[i] = oldValue; changeCount++; - if (unwatchFlags[i] && !exprFn.$$unwatch) unwatchCount++; - if (!unwatchFlags[i] && exprFn.$$unwatch) unwatchCount--; - unwatchFlags[i] = exprFn.$$unwatch; - })); + }, false, function watchGroupDeregNotifier() { + arrayRemove(deregisterFns, unwatch); + if (!deregisterFns.length) { + masterUnwatch(); + } + }); + + deregisterFns.push(unwatch); }, this); - deregisterFns.push(self.$watch(watchGroupFn, function () { - listener(newValues, oldValues, self); - if (unwatchCount === 0) { - watchGroupFn.$$unwatch = true; - } else { - watchGroupFn.$$unwatch = false; - } - })); + masterUnwatch = self.$watch(function watchGroupChangeWatch() { + return changeCount; + }, function watchGroupChangeAction(value, oldValue) { + listener(newValues, (value === oldValue) ? newValues : oldValues, self); + }); return function deregisterWatchGroup() { - forEach(deregisterFns, function (fn) { - fn(); - }); + while (deregisterFns.length) { + deregisterFns[0](); + } }; - - function watchGroupFn() {return changeCount;} }, /** * @ngdoc method @@ -23056,19 +23255,19 @@ // a shallow copy of the newValue from when the last change happened var veryOldValue; // only track veryOldValue if the listener is asking for it var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; - var objGetter = $parse(obj); + var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0; - function $watchCollectionWatch() { - newValue = objGetter(self); - var newLength, key; + function $watchCollectionInterceptor(_value) { + newValue = _value; + var newLength, key, bothNaN; if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { oldValue = newValue; changeDetected++; @@ -23088,11 +23287,11 @@ changeDetected++; oldValue.length = oldLength = newLength; } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - var bothNaN = (oldValue[i] !== oldValue[i]) && + bothNaN = (oldValue[i] !== oldValue[i]) && (newValue[i] !== newValue[i]); if (!bothNaN && (oldValue[i] !== newValue[i])) { changeDetected++; oldValue[i] = newValue[i]; } @@ -23108,11 +23307,13 @@ newLength = 0; for (key in newValue) { if (newValue.hasOwnProperty(key)) { newLength++; if (oldValue.hasOwnProperty(key)) { - if (oldValue[key] !== newValue[key]) { + bothNaN = (oldValue[key] !== oldValue[key]) && + (newValue[key] !== newValue[key]); + if (!bothNaN && (oldValue[key] !== newValue[key])) { changeDetected++; oldValue[key] = newValue[key]; } } else { oldLength++; @@ -23130,11 +23331,10 @@ delete oldValue[key]; } } } } - $watchCollectionWatch.$$unwatch = objGetter.$$unwatch; return changeDetected; } function $watchCollectionAction() { if (initRun) { @@ -23163,11 +23363,11 @@ } } } } - return this.$watch($watchCollectionWatch, $watchCollectionAction); + return this.$watch(changeDetector, $watchCollectionAction); }, /** * @ngdoc method * @name $rootScope.Scope#$digest @@ -23226,11 +23426,10 @@ postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], - stableWatchesCandidates = [], logIdx, logMsg, asyncTask; beginPhase('$digest'); lastDirtyWatch = null; @@ -23262,11 +23461,11 @@ // circuit it with === operator, only when === fails do we use .equals if (watch) { if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) - : (typeof value == 'number' && typeof last == 'number' + : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); @@ -23277,11 +23476,10 @@ ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); watchLog[logIdx].push(logMsg); } - if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers}); } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; @@ -23324,17 +23522,10 @@ postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } - - for (length = stableWatchesCandidates.length - 1; length >= 0; --length) { - var candidate = stableWatchesCandidates[length]; - if (candidate.watch.get.$$unwatch) { - arrayRemove(candidate.array, candidate.watch); - } - } }, /** * @ngdoc event @@ -23970,23 +24161,25 @@ * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. * * Here is what a secure configuration for this scenario might look like: * - * <pre class="prettyprint"> - * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.resourceUrlWhitelist([ - * // Allow same origin resource loads. - * 'self', - * // Allow loading from our assets domain. Notice the difference between * and **. - * 'http://srv*.assets.example.com/**']); + * ``` + * angular.module('myApp', []).config(function($sceDelegateProvider) { + * $sceDelegateProvider.resourceUrlWhitelist([ + * // Allow same origin resource loads. + * 'self', + * // Allow loading from our assets domain. Notice the difference between * and **. + * 'http://srv*.assets.example.com/**' + * ]); * - * // The blacklist overrides the whitelist so the open redirect here is blocked. - * $sceDelegateProvider.resourceUrlBlacklist([ - * 'http://myapp.example.com/clickThru**']); - * }); - * </pre> + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` */ function $SceDelegateProvider() { this.SCE_CONTEXTS = SCE_CONTEXTS; @@ -24277,14 +24470,14 @@ * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. * * Here's an example of a binding in a privileged context: * - * <pre class="prettyprint"> - * <input ng-model="userHtml"> - * <div ng-bind-html="userHtml"> - * </pre> + * ``` + * <input ng-model="userHtml"> + * <div ng-bind-html="userHtml"></div> + * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE * disabled, this application allows the user to render arbitrary HTML into the DIV. * In a more realistic example, one may be rendering user comments, blog articles, etc. via * bindings. (HTML is just one example of a context where rendering user controlled input creates @@ -24320,19 +24513,19 @@ * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly * simplified): * - * <pre class="prettyprint"> - * var ngBindHtmlDirective = ['$sce', function($sce) { - * return function(scope, element, attr) { - * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { - * element.html(value || ''); - * }); - * }; - * }]; - * </pre> + * ``` + * var ngBindHtmlDirective = ['$sce', function($sce) { + * return function(scope, element, attr) { + * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { + * element.html(value || ''); + * }); + * }; + * }]; + * ``` * * ## Impact on loading templates * * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as * `templateUrl`'s specified by {@link guide/directive directives}. @@ -24432,90 +24625,89 @@ * * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. * * ## Show me an example using SCE. * - * @example -<example module="mySceApp" deps="angular-sanitize.js"> -<file name="index.html"> - <div ng-controller="myAppController as myCtrl"> - <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> - <b>User comments</b><br> - By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - $sanitize is available. If $sanitize isn't available, this results in an error instead of an - exploit. - <div class="well"> - <div ng-repeat="userComment in myCtrl.userComments"> - <b>{{userComment.name}}</b>: - <span ng-bind-html="userComment.htmlComment" class="htmlComment"></span> - <br> - </div> - </div> - </div> -</file> - -<file name="script.js"> - var mySceApp = angular.module('mySceApp', ['ngSanitize']); - - mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - var self = this; - $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - self.userComments = userComments; - }); - self.explicitlyTrustedHtml = $sce.trustAsHtml( - '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' + - 'sanitization.&quot;">Hover over this text.</span>'); - }); -</file> - -<file name="test_data.json"> -[ - { "name": "Alice", - "htmlComment": - "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>" - }, - { "name": "Bob", - "htmlComment": "<i>Yes!</i> Am I the only other one?" - } -] -</file> - -<file name="protractor.js" type="protractor"> - describe('SCE doc demo', function() { - it('should sanitize untrusted values', function() { - expect(element(by.css('.htmlComment')).getInnerHtml()) - .toBe('<span>Is <i>anyone</i> reading this?</span>'); - }); - - it('should NOT sanitize explicitly trusted values', function() { - expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( - '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' + - 'sanitization.&quot;">Hover over this text.</span>'); - }); - }); -</file> -</example> + * <example module="mySceApp" deps="angular-sanitize.js"> + * <file name="index.html"> + * <div ng-controller="AppController as myCtrl"> + * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> + * <b>User comments</b><br> + * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + * $sanitize is available. If $sanitize isn't available, this results in an error instead of an + * exploit. + * <div class="well"> + * <div ng-repeat="userComment in myCtrl.userComments"> + * <b>{{userComment.name}}</b>: + * <span ng-bind-html="userComment.htmlComment" class="htmlComment"></span> + * <br> + * </div> + * </div> + * </div> + * </file> * + * <file name="script.js"> + * angular.module('mySceApp', ['ngSanitize']) + * .controller('AppController', ['$http', '$templateCache', '$sce', + * function($http, $templateCache, $sce) { + * var self = this; + * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + * self.userComments = userComments; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' + + * 'sanitization.&quot;">Hover over this text.</span>'); + * }]); + * </file> * + * <file name="test_data.json"> + * [ + * { "name": "Alice", + * "htmlComment": + * "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>" + * }, + * { "name": "Bob", + * "htmlComment": "<i>Yes!</i> Am I the only other one?" + * } + * ] + * </file> * + * <file name="protractor.js" type="protractor"> + * describe('SCE doc demo', function() { + * it('should sanitize untrusted values', function() { + * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * .toBe('<span>Is <i>anyone</i> reading this?</span>'); + * }); + * + * it('should NOT sanitize explicitly trusted values', function() { + * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' + + * 'sanitization.&quot;">Hover over this text.</span>'); + * }); + * }); + * </file> + * </example> + * + * + * * ## Can I disable SCE completely? * * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits * for little coding overhead. It will be much harder to take an SCE disabled application and * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE * for cases where you have a lot of existing code that was written before SCE was introduced and * you're migrating them a module at a time. * * That said, here's how you can completely disable SCE: * - * <pre class="prettyprint"> - * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { - * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. - * $sceProvider.enabled(false); - * }); - * </pre> + * ``` + * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { + * // Completely disable SCE. For demonstration purposes only! + * // Do not use in new projects. + * $sceProvider.enabled(false); + * }); + * ``` * */ /* jshint maxlen: 100 */ function $SceProvider() { @@ -24622,11 +24814,11 @@ sce.valueOf = identity; } /** * @ngdoc method - * @name $sce#parse + * @name $sce#parseAs * * @description * Converts Angular {@link guide/expression expression} into a function. This is like {@link * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, @@ -24644,15 +24836,13 @@ sce.parseAs = function sceParseAs(type, expr) { var parsed = $parse(expr); if (parsed.literal && parsed.constant) { return parsed; } else { - return function sceParseAsTrusted(self, locals) { - var result = sce.getTrusted(type, parsed(self, locals)); - sceParseAsTrusted.$$unwatch = parsed.$$unwatch; - return result; - }; + return $parse(expr, function (value) { + return sce.getTrusted(type, value); + }); } }; /** * @ngdoc method @@ -25009,12 +25199,12 @@ }; }]; } function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { + this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { var deferreds = {}; /** * @ngdoc service @@ -25040,13 +25230,13 @@ * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this * promise will be resolved with is the return value of the `fn` function. * */ function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), + var skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), timeoutId; timeoutId = $browser.defer(function() { try { deferred.resolve(fn()); @@ -25210,21 +25400,22 @@ * below, are evaluated with respect to the current scope. Therefore, there is * no risk of inadvertently coding in a dependency on a global value in such an * expression. * * @example - <example> + <example module="windowExample"> <file name="index.html"> <script> - function Ctrl($scope, $window) { - $scope.greeting = 'Hello, World!'; - $scope.doGreeting = function(greeting) { + angular.module('windowExample', []) + .controller('ExampleController', ['$scope', '$window', function ($scope, $window) { + $scope.greeting = 'Hello, World!'; + $scope.doGreeting = function(greeting) { $window.alert(greeting); - }; - } + }; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <input type="text" ng-model="greeting" /> <button ng-click="doGreeting(greeting)">ALERT</button> </div> </file> <file name="protractor.js" type="protractor"> @@ -25238,10 +25429,21 @@ */ function $WindowProvider(){ this.$get = valueFn(window); } +/* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + /** * @ngdoc provider * @name $filterProvider * @description * @@ -25580,11 +25782,11 @@ // jshint -W086 case "object": // jshint +W086 for (var key in expression) { (function(path) { - if (typeof expression[path] == 'undefined') return; + if (typeof expression[path] === 'undefined') return; predicates.push(function(value) { return search(path == '$' ? value : (value && value[path]), expression[path]); }); })(key); } @@ -25619,18 +25821,19 @@ * @param {string=} symbol Currency symbol or identifier to be displayed. * @returns {string} Formatted number. * * * @example - <example> + <example module="currencyExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.amount = 1234.56; - } + angular.module('currencyExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.amount = 1234.56; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <input type="number" ng-model="amount"> <br> default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br> custom currency identifier (USD$): <span>{{amount | currency:"USD$"}}</span> </div> </file> @@ -25678,18 +25881,19 @@ * If this is not provided then the fraction size is computed from the current locale's number * formatting pattern. In the case of the default locale, it will be 3. * @returns {string} Number rounded to decimalPlaces and places a “,†after each third digit. * * @example - <example> + <example module="numberFilterExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.val = 1234.56789; - } + angular.module('numberFilterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.val = 1234.56789; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Enter number: <input ng-model='val'><br> Default formatting: <span id='number-default'>{{val | number}}</span><br> No fractions: <span>{{val | number:0}}</span><br> Negative number: <span>{{-val | number:4}}</span> </div> @@ -25735,10 +25939,11 @@ var hasExponent = false; if (numStr.indexOf('e') !== -1) { var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); if (match && match[2] == '-' && match[3] > fractionSize + 1) { numStr = '0'; + number = 0; } else { formatedText = numStr; hasExponent = true; } } @@ -25749,12 +25954,15 @@ // determine fractionSize if it is not specified if (isUndefined(fractionSize)) { fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } - var pow = Math.pow(10, fractionSize + 1); - number = Math.floor(number * pow + 5) / pow; + // safely round numbers in JS without hitting imprecisions of floating-point arithmetics + // inspired by: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); + var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; var i, pos = 0, @@ -26023,15 +26231,11 @@ fn, match; format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } + date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); } if (isNumber(date)) { date = new Date(date); } @@ -26135,21 +26339,22 @@ * are copied. The `limit` will be trimmed if it exceeds `array.length` * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array * had less than `limit` elements. * * @example - <example> + <example module="limitToExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.numbers = [1,2,3,4,5,6,7,8,9]; - $scope.letters = "abcdefghi"; - $scope.numLimit = 3; - $scope.letterLimit = 3; - } + angular.module('limitToExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.numbers = [1,2,3,4,5,6,7,8,9]; + $scope.letters = "abcdefghi"; + $scope.numLimit = 3; + $scope.letterLimit = 3; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Limit {{numbers}} to: <input type="integer" ng-model="numLimit"> <p>Output numbers: {{ numbers | limitTo:numLimit }}</p> Limit {{letters}} to: <input type="integer" ng-model="letterLimit"> <p>Output letters: {{ letters | limitTo:letterLimit }}</p> </div> @@ -26257,24 +26462,25 @@ * * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * @example - <example> + <example module="orderByExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.friends = - [{name:'John', phone:'555-1212', age:10}, - {name:'Mary', phone:'555-9876', age:19}, - {name:'Mike', phone:'555-4321', age:21}, - {name:'Adam', phone:'555-5678', age:35}, - {name:'Julie', phone:'555-8765', age:29}] - $scope.predicate = '-age'; - } + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = + [{name:'John', phone:'555-1212', age:10}, + {name:'Mary', phone:'555-9876', age:19}, + {name:'Mike', phone:'555-4321', age:21}, + {name:'Adam', phone:'555-5678', age:35}, + {name:'Julie', phone:'555-8765', age:29}]; + $scope.predicate = '-age'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> <hr/> [ <a href="" ng-click="predicate=''">unsorted</a> ] <table class="friend"> <tr> @@ -26298,13 +26504,13 @@ * desired parameters. * * Example: * * @example - <example> + <example module="orderByExample"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <table class="friend"> <tr> <th><a href="" ng-click="reverse=false;order('name', false)">Name</a> (<a href="" ng-click="order('-name',false)">^</a>)</th> <th><a href="" ng-click="reverse=!reverse;order('phone', reverse)">Phone Number</a></th> @@ -26318,25 +26524,25 @@ </table> </div> </file> <file name="script.js"> - function Ctrl($scope, $filter) { - var orderBy = $filter('orderBy'); - $scope.friends = [ - { name: 'John', phone: '555-1212', age: 10 }, - { name: 'Mary', phone: '555-9876', age: 19 }, - { name: 'Mike', phone: '555-4321', age: 21 }, - { name: 'Adam', phone: '555-5678', age: 35 }, - { name: 'Julie', phone: '555-8765', age: 29 } - ]; - - $scope.order = function(predicate, reverse) { - $scope.friends = orderBy($scope.friends, predicate, reverse); - }; - $scope.order('-age',false); - } + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { + var orderBy = $filter('orderBy'); + $scope.friends = [ + { name: 'John', phone: '555-1212', age: 10 }, + { name: 'Mary', phone: '555-9876', age: 19 }, + { name: 'Mike', phone: '555-4321', age: 21 }, + { name: 'Adam', phone: '555-5678', age: 35 }, + { name: 'Julie', phone: '555-8765', age: 29 } + ]; + $scope.order = function(predicate, reverse) { + $scope.friends = orderBy($scope.friends, predicate, reverse); + }; + $scope.order('-age',false); + }]); </file> </example> */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse){ @@ -26373,18 +26579,22 @@ if (comp !== 0) return comp; } return 0; } function reverseComparator(comp, descending) { - return toBoolean(descending) + return descending ? function(a,b){return comp(b,a);} : comp; } function compare(v1, v2){ var t1 = typeof v1; var t2 = typeof v2; if (t1 == t2) { + if (isDate(v1) && isDate(v2)) { + v1 = v1.valueOf(); + v2 = v2.valueOf(); + } if (t1 == "string") { v1 = v1.toLowerCase(); v2 = v2.toLowerCase(); } if (v1 === v2) return 0; @@ -26518,11 +26728,11 @@ browser.wait(function() { return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/123$/); }); - }, 1000, 'page should navigate to /123'); + }, 5000, 'page should navigate to /123'); }); xit('should execute ng-click but not reload when href empty string and name specified', function() { element(by.id('link-4')).click(); expect(element(by.model('value')).getAttribute('value')).toEqual('4'); @@ -26546,11 +26756,11 @@ // to use browser.driver to get the base webdriver. browser.wait(function() { return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/6$/); }); - }, 1000, 'page should navigate to /6'); + }, 5000, 'page should navigate to /6'); }); </file> </example> */ @@ -26803,10 +27013,11 @@ if (propName == "multiple") return; var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { + restrict: 'A', priority: 100, link: function(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { attr.$set(attrName, !!value); }); @@ -26855,12 +27066,16 @@ attr.$attr[name] = 'xlink:href'; propName = null; } attr.$observe(normalized, function(value) { - if (!value) - return; + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } + return; + } attr.$set(name, value); // 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 @@ -26947,10 +27162,27 @@ $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); } /** * @ngdoc method + * @name form.FormController#$rollbackViewValue + * + * @description + * Rollback all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * 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) { + control.$rollbackViewValue(); + }); + }; + + /** + * @ngdoc method * @name form.FormController#$commitViewValue * * @description * Commit all form controls pending updates to the `$modelValue`. * @@ -27206,16 +27438,17 @@ * color:white; * } * </pre> * * @example - <example deps="angular-animate.js" animations="true" fixBase="true"> + <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.userType = 'guest'; - } + angular.module('formExample', []) + .controller('FormController', ['$scope', function($scope) { + $scope.userType = 'guest'; + }]); </script> <style> .my-form { -webkit-transition:all linear 0.5s; transition:all linear 0.5s; @@ -27223,11 +27456,11 @@ } .my-form.ng-invalid { background: red; } </style> - <form name="myForm" ng-controller="Ctrl" class="my-form"> + <form name="myForm" ng-controller="FormController" class="my-form"> userType: <input name="input" ng-model="userType" required> <span class="error" ng-show="myForm.input.$error.required">Required!</span><br> <tt>userType = {{userType}}</tt><br> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br> @@ -27321,22 +27554,20 @@ }; var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global - - -VALID_CLASS, - -INVALID_CLASS, - -PRISTINE_CLASS, - -DIRTY_CLASS, - -UNTOUCHED_CLASS, - -TOUCHED_CLASS +/* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true, + UNTOUCHED_CLASS: true, + TOUCHED_CLASS: true, */ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i; +var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\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)$/; var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; @@ -27368,19 +27599,20 @@ * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. * * @example - <example name="text-input-directive"> + <example name="text-input-directive" module="textInputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'guest'; - $scope.word = /^\s*\w*\s*$/; - } + angular.module('textInputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'guest'; + $scope.word = /^\s*\w*\s*$/; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Single word: <input type="text" name="input" ng-model="text" ng-pattern="word" required ng-trim="false"> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> <span class="error" ng-show="myForm.input.$error.pattern"> @@ -27427,11 +27659,13 @@ * @name input[date] * * @description * Input with date validation and transformation. In browsers that do not yet support * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 - * date format (yyyy-MM-dd), for example: `2009-01-06`. The model must always be a Date object. + * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many + * modern browsers do not yet support this input type, it is important to provide cues to users on the + * expected input format via a placeholder or label. The model must always be a Date object. * * @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`. This must be a * valid ISO date string (yyyy-MM-dd). @@ -27443,18 +27677,19 @@ * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="date-input-directive"> + <example name="date-input-directive" module="dateInputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = new Date(2013, 9, 22); - } + angular.module('dateInputExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 22); + }]); </script> - <form name="myForm" ng-controller="Ctrl as dateCtrl"> + <form name="myForm" ng-controller="DateController as dateCtrl"> Pick a date between in 2013: <input type="date" id="exampleInput" name="input" ng-model="value" placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required /> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -27498,11 +27733,11 @@ setInput('2015-01-01'); expect(value.getText()).toContain(''); expect(valid.getText()).toContain('myForm.input.$valid = false'); }); </file> - </example>f + </example> */ 'date': createDateInputType('date', DATE_REGEXP, createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), 'yyyy-MM-dd'), @@ -27527,18 +27762,19 @@ * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="datetimelocal-input-directive"> + <example name="datetimelocal-input-directive" module="dateExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = new Date(2010, 11, 28, 14, 57); - } + angular.module('dateExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2010, 11, 28, 14, 57); + }]); </script> - <form name="myForm" ng-controller="Ctrl as dateCtrl"> + <form name="myForm" ng-controller="DateController as dateCtrl"> Pick a date between in 2013: <input type="datetime-local" id="exampleInput" name="input" ng-model="value" placeholder="yyyy-MM-ddTHH:mm" min="2001-01-01T00:00" max="2013-12-31T00:00" required /> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -27612,18 +27848,19 @@ * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="time-input-directive"> + <example name="time-input-directive" module="timeExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = new Date(0, 0, 1, 14, 57); - } + angular.module('timeExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(0, 0, 1, 14, 57); + }]); </script> - <form name="myForm" ng-controller="Ctrl as dateCtrl"> + <form name="myForm" ng-controller="DateController as dateCtrl"> Pick a between 8am and 5pm: <input type="time" id="exampleInput" name="input" ng-model="value" placeholder="HH:mm" min="08:00" max="17:00" required /> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -27696,18 +27933,19 @@ * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="week-input-directive"> + <example name="week-input-directive" module="weekExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = new Date(2013, 0, 3); - } + angular.module('weekExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 0, 3); + }]); </script> - <form name="myForm" ng-controller="Ctrl as dateCtrl"> + <form name="myForm" ng-controller="DateController as dateCtrl"> Pick a date between in 2013: <input id="exampleInput" type="week" name="input" ng-model="value" placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required /> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -27779,18 +28017,19 @@ * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="month-input-directive"> + <example name="month-input-directive" module="monthExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = new Date(2013, 9, 1); - } + angular.module('monthExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 1); + }]); </script> - <form name="myForm" ng-controller="Ctrl as dateCtrl"> + <form name="myForm" ng-controller="DateController as dateCtrl"> Pick a month int 2013: <input id="exampleInput" type="month" name="input" ng-model="value" placeholder="yyyy-MM" min="2013-01" max="2013-12" required /> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -27867,18 +28106,19 @@ * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="number-input-directive"> + <example name="number-input-directive" module="numberExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = 12; - } + angular.module('numberExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value = 12; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Number: <input type="number" name="input" ng-model="value" min="0" max="99" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> <span class="error" ng-show="myForm.input.$error.number"> @@ -27942,18 +28182,19 @@ * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="url-input-directive"> + <example name="url-input-directive" module="urlExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'http://google.com'; - } + angular.module('urlExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'http://google.com'; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> URL: <input type="url" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> <span class="error" ng-show="myForm.input.$error.url"> Not valid url!</span> @@ -28018,18 +28259,19 @@ * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="email-input-directive"> + <example name="email-input-directive" module="emailExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'me@example.com'; - } + angular.module('emailExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'me@example.com'; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Email: <input type="email" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> <span class="error" ng-show="myForm.input.$error.email"> Not valid email!</span> @@ -28084,22 +28326,23 @@ * interaction with the input element. * @param {string} ngValue Angular expression which sets the value to which the expression should * be set when selected. * * @example - <example name="radio-input-directive"> + <example name="radio-input-directive" module="radioExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.color = 'blue'; - $scope.specialValue = { - "id": "12345", - "value": "green" - }; - } + angular.module('radioExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.color = 'blue'; + $scope.specialValue = { + "id": "12345", + "value": "green" + }; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> <input type="radio" ng-model="color" value="red"> Red <br/> <input type="radio" ng-model="color" ng-value="specialValue"> Green <br/> <input type="radio" ng-model="color" value="blue"> Blue <br/> <tt>color = {{color | json}}</tt><br/> </form> @@ -28128,28 +28371,29 @@ * @description * HTML checkbox. * * @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=} ngTrueValue The value to which the expression should be set when selected. - * @param {string=} ngFalseValue The value to which the expression should be set when not selected. + * @param {expression=} ngTrueValue The value to which the expression should be set when selected. + * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="checkbox-input-directive"> + <example name="checkbox-input-directive" module="checkboxExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value1 = true; - $scope.value2 = 'YES' - } + angular.module('checkboxExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value1 = true; + $scope.value2 = 'YES' + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Value1: <input type="checkbox" ng-model="value1"> <br/> Value2: <input type="checkbox" ng-model="value2" - ng-true-value="YES" ng-false-value="NO"> <br/> + ng-true-value="'YES'" ng-false-value="'NO'"> <br/> <tt>value1 = {{value1}}</tt><br/> <tt>value2 = {{value2}}</tt><br/> </form> </file> <file name="protractor.js" type="protractor"> @@ -28183,31 +28427,46 @@ function validate(ctrl, validatorName, validity, value){ ctrl.$setValidity(validatorName, validity); return validity ? value : undefined; } +function testFlags(validity, flags) { + var i, flag; + if (flags) { + for (i=0; i<flags.length; ++i) { + flag = flags[i]; + if (validity[flag]) { + return true; + } + } + } + return false; +} -function addNativeHtml5Validators(ctrl, validatorName, element) { - var validity = element.prop('validity'); +// Pass validity so that behaviour can be mocked easier. +function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) { if (isObject(validity)) { + ctrl.$$hasNativeValidators = true; var validator = function(value) { // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can // perform the required validation) - if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError || - validity.typeMismatch) && !validity.valueMissing) { + if (!ctrl.$error[validatorName] && + !testFlags(validity, ignoreFlags) && + testFlags(validity, badFlags)) { ctrl.$setValidity(validatorName, false); return; } return value; }; ctrl.$parsers.push(validator); } } function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - var validity = element.prop('validity'); + var validity = element.prop(VALIDITY_STATE_PROPERTY); var placeholder = element[0].placeholder, noevent = {}; + ctrl.$$validityState = validity; // In composition mode, users are still inputing 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) { @@ -28238,24 +28497,24 @@ } // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming // e.g. <input ng-model="foo" ng-trim="false"> - if (toBoolean(attr.ngTrim || 'T')) { + if (!attr.ngTrim || attr.ngTrim !== 'false') { value = trim(value); } - if (ctrl.$viewValue !== value || - // If the value is still empty/falsy, and there is no `required` error, run validators - // again. This enables HTML5 constraint validation errors to affect Angular validation - // even when the first character entered causes an error. - (validity && value === '' && !validity.valueMissing)) { + // If a control is suffering from bad input, browsers discard its value, so it may be + // necessary to revalidate even if the control's value is the same empty value twice in + // a row. + var revalidate = validity && ctrl.$$hasNativeValidators; + if (ctrl.$viewValue !== value || (value === '' && revalidate)) { if (scope.$$phase) { - ctrl.$setViewValue(value, event); + ctrl.$setViewValue(value, event, revalidate); } else { scope.$apply(function() { - ctrl.$setViewValue(value, event); + ctrl.$setViewValue(value, event, revalidate); }); } } }; @@ -28400,10 +28659,12 @@ ctrl.$formatters.push(maxValidator); } }; } +var numberBadFlags = ['badInput']; + function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { var empty = ctrl.$isEmpty(value); @@ -28414,11 +28675,11 @@ ctrl.$setValidity('number', false); return undefined; } }); - addNativeHtml5Validators(ctrl, 'number', element); + addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState); ctrl.$formatters.push(function(value) { return ctrl.$isEmpty(value) ? '' : '' + value; }); @@ -28448,27 +28709,23 @@ } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); - var urlValidator = function(value) { - return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; - - ctrl.$formatters.push(urlValidator); - ctrl.$parsers.push(urlValidator); } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); - var emailValidator = function(value) { - return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; - - ctrl.$formatters.push(emailValidator); - ctrl.$parsers.push(emailValidator); } function radioInputType(scope, element, attr, ctrl) { // make the name unique, if not defined if (isUndefined(attr.name)) { @@ -28491,16 +28748,26 @@ }; attr.$observe('value', ctrl.$render); } -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; +function parseConstantExpr($parse, context, name, expression, fallback) { + var parseFn; + if (isDefined(expression)) { + parseFn = $parse(expression); + if (!parseFn.constant) { + throw new minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + + '`{1}`.', name, expression); + } + return parseFn(context); + } + return fallback; +} - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; +function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); + var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); var listener = function(ev) { scope.$apply(function() { ctrl.$setViewValue(element[0].checked, ev && ev.type); }); @@ -28516,11 +28783,11 @@ ctrl.$isEmpty = function(value) { return value !== trueValue; }; ctrl.$formatters.push(function(value) { - return value === trueValue; + return equals(value, trueValue); }); ctrl.$parsers.push(function(value) { return value ? trueValue : falseValue; }); @@ -28578,18 +28845,19 @@ * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="input-directive"> + <example name="input-directive" module="inputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.user = {name: 'guest', last: 'visitor'}; - } + angular.module('inputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = {name: 'guest', last: 'visitor'}; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <form name="myForm"> User name: <input type="text" name="userName" ng-model="user.name" required> <span class="error" ng-show="myForm.userName.$error.required"> Required!</span><br> Last name: <input type="text" name="lastName" ng-model="user.last" @@ -28664,18 +28932,19 @@ expect(formValid.getText()).toContain('false'); }); </file> </example> */ -var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sniffer, $filter) { +var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', + function($browser, $sniffer, $filter, $parse) { return { restrict: 'E', require: ['?ngModel'], link: function(scope, element, attr, ctrls) { if (ctrls[0]) { (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, - $browser, $filter); + $browser, $filter, $parse); } } }; }]; @@ -28865,10 +29134,22 @@ * @name ngModel.NgModelController#$render * * @description * Called when the view needs to be updated. It is expected that the user of the ng-model * directive will implement this method. + * + * The `$render()` method is invoked in the following situations: + * + * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last + * committed value then `$render()` is called to update the input control. + * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and + * the `$viewValue` are different to last time. + * + * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of + * `$modelValue` and `$viewValue` are actually different to their previous value. If `$modelValue` + * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be + * invoked if you only change a property on the objects. */ this.$render = noop; /** * @ngdoc method @@ -28919,11 +29200,11 @@ * * This method can be called within $parsers/$formatters. However, if possible, please use the * `ngModel.$validators` pipeline which is designed to handle validations with true/false values. * * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * to `$error[validationErrorKey]=!isValid` so that it is available for data-binding. * The `validationErrorKey` should be in camelCase and will get converted into dash-case * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` * class and can be bound to as `{{someForm.someControl.$error.myError}}` . * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). */ @@ -29030,11 +29311,11 @@ * * <example name="ng-model-cancel-update" module="cancel-update-example"> * <file name="app.js"> * angular.module('cancel-update-example', []) * - * .controller('CancelUpdateCtrl', function($scope) { + * .controller('CancelUpdateController', ['$scope', function($scope) { * $scope.resetWithCancel = function (e) { * if (e.keyCode == 27) { * $scope.myForm.myInput1.$rollbackViewValue(); * $scope.myValue = ''; * } @@ -29042,14 +29323,14 @@ * $scope.resetWithoutCancel = function (e) { * if (e.keyCode == 27) { * $scope.myValue = ''; * } * }; - * }); + * }]); * </file> * <file name="index.html"> - * <div ng-controller="CancelUpdateCtrl"> + * <div ng-controller="CancelUpdateController"> * <p>Try typing something in each input. See that the model only updates when you * blur off the input. * </p> * <p>Now see what happens if you start typing then press the Escape key</p> * @@ -29078,17 +29359,28 @@ * * @description * Runs each of the registered validations set on the $validators object. */ this.$validate = function() { - this.$$runValidators(ctrl.$modelValue, ctrl.$viewValue); + // ignore $validate before model initialized + if (ctrl.$modelValue !== ctrl.$modelValue) { + return; + } + + var prev = ctrl.$modelValue; + ctrl.$$runValidators(ctrl.$$invalidModelValue || ctrl.$modelValue, ctrl.$viewValue); + if (prev !== ctrl.$modelValue) { + ctrl.$$writeModelToScope(); + } }; this.$$runValidators = function(modelValue, viewValue) { forEach(ctrl.$validators, function(fn, name) { ctrl.$setValidity(name, fn(modelValue, viewValue)); }); + ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue; }; /** * @ngdoc method * @name ngModel.NgModelController#$commitViewValue @@ -29098,15 +29390,15 @@ * * Updates may be pending by a debounced event or because the input is waiting for a some future * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - this.$commitViewValue = function() { + this.$commitViewValue = function(revalidate) { var viewValue = ctrl.$viewValue; $timeout.cancel(pendingDebounce); - if (ctrl.$$lastCommittedViewValue === viewValue) { + if (!revalidate && ctrl.$$lastCommittedViewValue === viewValue) { return; } ctrl.$$lastCommittedViewValue = viewValue; // change to dirty @@ -29123,41 +29415,61 @@ modelValue = fn(modelValue); }); if (ctrl.$modelValue !== modelValue && (isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) { - ctrl.$$runValidators(modelValue, viewValue); - ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; - ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue; + ctrl.$$writeModelToScope(); + } + }; + this.$$writeModelToScope = function() { + var getterSetter; + + if (ctrl.$options && ctrl.$options.getterSetter && + isFunction(getterSetter = ngModelGet($scope))) { + + getterSetter(ctrl.$modelValue); + } else { ngModelSet($scope, ctrl.$modelValue); - forEach(ctrl.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } - }); } + forEach(ctrl.$viewChangeListeners, function(listener) { + try { + listener(); + } catch(e) { + $exceptionHandler(e); + } + }); }; /** * @ngdoc method * @name ngModel.NgModelController#$setViewValue * * @description * Update the view value. * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. + * This method should be called when an input directive want to change the view value; typically, + * this is done from within a DOM event handler. * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * For example {@link ng.directive:input input} calls it when the value of the input changes and + * {@link ng.directive:select select} calls it when an option is selected. * + * If the new `value` is an object (rather than a string or a number), we should make a copy of the + * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep + * watch of objects, it only looks for a change of identity. If you only change the property of + * the object then ngModel will not realise that the object has changed and will not invoke the + * `$parsers` and `$validators` pipelines. + * + * For this reason, you should not change properties of the copy once it has been passed to + * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly. + * + * When this method is called, the new `value` will be staged for committing through the `$parsers` + * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged + * value sent directly for processing, finally to be applied to `$modelValue` and then the + * **expression** specified in the `ng-model` attribute. + * * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. * * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` * and the `default` trigger is not listed, all those actions will remain pending until one of the * `updateOn` events is triggered on the DOM element. @@ -29167,18 +29479,18 @@ * Note that calling this function does not trigger a `$digest`. * * @param {string} value Value from the view. * @param {string} trigger Event that triggered the update. */ - this.$setViewValue = function(value, trigger) { + this.$setViewValue = function(value, trigger, revalidate) { ctrl.$viewValue = value; if (!ctrl.$options || ctrl.$options.updateOnDefault) { - ctrl.$$debounceViewValueCommit(trigger); + ctrl.$$debounceViewValueCommit(trigger, revalidate); } }; - this.$$debounceViewValueCommit = function(trigger) { + this.$$debounceViewValueCommit = function(trigger, revalidate) { var debounceDelay = 0, options = ctrl.$options, debounce; if(options && isDefined(options.debounce)) { @@ -29193,21 +29505,25 @@ } $timeout.cancel(pendingDebounce); if (debounceDelay) { pendingDebounce = $timeout(function() { - ctrl.$commitViewValue(); + ctrl.$commitViewValue(revalidate); }, debounceDelay); } else { - ctrl.$commitViewValue(); + ctrl.$commitViewValue(revalidate); } }; // model -> value $scope.$watch(function ngModelWatch() { var modelValue = ngModelGet($scope); + if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) { + modelValue = modelValue(); + } + // if scope model value and ngModel value are out of sync if (ctrl.$modelValue !== modelValue && (isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) { var formatters = ctrl.$formatters, @@ -29217,12 +29533,10 @@ while(idx--) { viewValue = formatters[idx](viewValue); } ctrl.$$runValidators(modelValue, viewValue); - ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; - ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue; if (ctrl.$viewValue !== viewValue) { ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); } @@ -29312,16 +29626,17 @@ * color:white; * } * </pre> * * @example - * <example deps="angular-animate.js" animations="true" fixBase="true"> + * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.val = '1'; - } + angular.module('inputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.val = '1'; + }]); </script> <style> .my-input { -webkit-transition:all linear 0.5s; transition:all linear 0.5s; @@ -29332,18 +29647,68 @@ background: red; } </style> Update input to see transitions when valid/invalid. Integer is a valid value. - <form name="testForm" ng-controller="Ctrl"> + <form name="testForm" ng-controller="ExampleController"> <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" /> </form> </file> * </example> + * + * ## Binding to a getter/setter + * + * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a + * function that returns a representation of the model when called with zero arguments, and sets + * the internal state of a model when called with an argument. It's sometimes useful to use this + * for models that have an internal representation that's different than what the model exposes + * to the view. + * + * <div class="alert alert-success"> + * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more + * frequently than other parts of your code. + * </div> + * + * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that + * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to + * a `<form>`, which will enable this behavior for all `<input>`s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: + * + * @example + * <example name="ngModel-getter-setter" module="getterSetterExample"> + <file name="index.html"> + <div ng-controller="ExampleController"> + <form name="userForm"> + Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ getterSetter: true }" /> + </form> + <pre>user.name = <span ng-bind="user.name()"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function (newName) { + if (angular.isDefined(newName)) { + _name = newName; + } + return _name; + } + }; + }]); + </file> + * </example> */ var ngModelDirective = function() { return { + restrict: 'A', require: ['ngModel', '^?form', '^?ngModelOptions'], controller: NgModelController, link: { pre: function(scope, element, attr, ctrls) { // Pass the ng-model-options to the ng-model controller @@ -29399,21 +29764,22 @@ * @element input * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change * in input value. * * @example - * <example name="ngChange-directive"> + * <example name="ngChange-directive" module="changeExample"> * <file name="index.html"> * <script> - * function Controller($scope) { - * $scope.counter = 0; - * $scope.change = function() { - * $scope.counter++; - * }; - * } + * angular.module('changeExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.counter = 0; + * $scope.change = function() { + * $scope.counter++; + * }; + * }]); * </script> - * <div ng-controller="Controller"> + * <div ng-controller="ExampleController"> * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" /> * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" /> * <label for="ng-change-example2">Confirmed</label><br /> * <tt>debug = {{confirmed}}</tt><br/> * <tt>counter = {{counter}}</tt><br/> @@ -29440,10 +29806,11 @@ * }); * </file> * </example> */ var ngChangeDirective = valueFn({ + restrict: 'A', require: 'ngModel', link: function(scope, element, attr, ctrl) { ctrl.$viewChangeListeners.push(function() { scope.$eval(attr.ngChange); }); @@ -29451,10 +29818,11 @@ }); var requiredDirective = function() { return { + restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; attr.required = true; // force truthy in case we are on non input element @@ -29470,10 +29838,11 @@ }; var patternDirective = function() { return { + restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; var regexp, patternExp = attr.ngPattern || attr.pattern; @@ -29500,10 +29869,11 @@ }; var maxlengthDirective = function() { return { + restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; var maxlength = 0; @@ -29518,10 +29888,11 @@ }; }; var minlengthDirective = function() { return { + restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; var minlength = 0; @@ -29540,85 +29911,117 @@ /** * @ngdoc directive * @name ngList * * @description - * Text input that converts between a delimited string and an array of strings. The delimiter - * can be a fixed string (by default a comma) or a regular expression. + * Text input that converts between a delimited string and an array of strings. The default + * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom + * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. If - * specified in form `/something/` then the value will be converted into a regular expression. + * The behaviour of the directive is affected by the use of the `ngTrim` attribute. + * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each + * list item is respected. This implies that the user of the directive is responsible for + * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a + * tab or newline character. + * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected + * when joining the list items back together) and whitespace around each list item is stripped + * before it is added to the model. * - * @example - <example name="ngList-directive"> - <file name="index.html"> - <script> - function Ctrl($scope) { - $scope.names = ['igor', 'misko', 'vojta']; - } - </script> - <form name="myForm" ng-controller="Ctrl"> - List: <input name="namesInput" ng-model="names" ng-list required> - <span class="error" ng-show="myForm.namesInput.$error.required"> - Required!</span> - <br> - <tt>names = {{names}}</tt><br/> - <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> - <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> - <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> - <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> - </form> - </file> - <file name="protractor.js" type="protractor"> - var listInput = element(by.model('names')); - var names = element(by.binding('{{names}}')); - var valid = element(by.binding('myForm.namesInput.$valid')); - var error = element(by.css('span.error')); - - it('should initialize to model', function() { - expect(names.getText()).toContain('["igor","misko","vojta"]'); - expect(valid.getText()).toContain('true'); - expect(error.getCssValue('display')).toBe('none'); - }); - - it('should be invalid if empty', function() { - listInput.clear(); - listInput.sendKeys(''); - - expect(names.getText()).toContain(''); - expect(valid.getText()).toContain('false'); - expect(error.getCssValue('display')).not.toBe('none'); }); - </file> - </example> + * ### Example with Validation + * + * <example name="ngList-directive" module="listExample"> + * <file name="app.js"> + * angular.module('listExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.names = ['morpheus', 'neo', 'trinity']; + * }]); + * </file> + * <file name="index.html"> + * <form name="myForm" ng-controller="ExampleController"> + * List: <input name="namesInput" ng-model="names" ng-list required> + * <span class="error" ng-show="myForm.namesInput.$error.required"> + * Required!</span> + * <br> + * <tt>names = {{names}}</tt><br/> + * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> + * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> + * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + * </form> + * </file> + * <file name="protractor.js" type="protractor"> + * var listInput = element(by.model('names')); + * var names = element(by.binding('{{names}}')); + * var valid = element(by.binding('myForm.namesInput.$valid')); + * var error = element(by.css('span.error')); + * + * it('should initialize to model', function() { + * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); + * expect(valid.getText()).toContain('true'); + * expect(error.getCssValue('display')).toBe('none'); + * }); + * + * it('should be invalid if empty', function() { + * listInput.clear(); + * listInput.sendKeys(''); + * + * expect(names.getText()).toContain(''); + * expect(valid.getText()).toContain('false'); + * expect(error.getCssValue('display')).not.toBe('none'); + * }); + * </file> + * </example> + * + * ### Example - splitting on whitespace + * <example name="ngList-directive-newlines"> + * <file name="index.html"> + * <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea> + * <pre>{{ list | json }}</pre> + * </file> + * <file name="protractor.js" type="protractor"> + * it("should split the text by newlines", function() { + * var listInput = element(by.model('list')); + * var output = element(by.binding('{{ list | json }}')); + * listInput.sendKeys('abc\ndef\nghi'); + * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); + * }); + * </file> + * </example> + * + * @element input + * @param {string=} ngList optional delimiter that should be used to split the value. */ var ngListDirective = function() { return { + restrict: 'A', require: 'ngModel', link: function(scope, element, attr, ctrl) { - var match = /\/(.*)\//.exec(attr.ngList), - separator = match && new RegExp(match[1]) || attr.ngList || ','; + // We want to control whitespace trimming so we use this convoluted approach + // to access the ngList attribute, which doesn't pre-trim the attribute + var ngList = element.attr(attr.$attr.ngList) || ', '; + var trimValues = attr.ngTrim !== 'false'; + var separator = trimValues ? trim(ngList) : ngList; var parse = function(viewValue) { // If the viewValue is invalid (say required but empty) it will be `undefined` if (isUndefined(viewValue)) return; var list = []; if (viewValue) { forEach(viewValue.split(separator), function(value) { - if (value) list.push(trim(value)); + if (value) list.push(trimValues ? trim(value) : value); }); } return list; }; ctrl.$parsers.push(parse); ctrl.$formatters.push(function(value) { if (isArray(value)) { - return value.join(', '); + return value.join(ngList); } return undefined; }); @@ -29647,19 +30050,20 @@ * @element input * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute * of the `input` element * * @example - <example name="ngValue-directive"> + <example name="ngValue-directive" module="valueExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.names = ['pizza', 'unicorns', 'robots']; - $scope.my = { favorite: 'unicorns' }; - } + angular.module('valueExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.names = ['pizza', 'unicorns', 'robots']; + $scope.my = { favorite: 'unicorns' }; + }]); </script> - <form ng-controller="Ctrl"> + <form ng-controller="ExampleController"> <h2>Which is your favorite?</h2> <label ng-repeat="name in names" for="{{name}}"> {{name}} <input type="radio" ng-model="my.favorite" @@ -29683,10 +30087,11 @@ </file> </example> */ var ngValueDirective = function() { return { + restrict: 'A', priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { return function ngValueConstantLink(scope, elm, attr) { attr.$set('value', scope.$eval(attr.ngValue)); @@ -29731,20 +30136,22 @@ * matches the default events belonging of the control. * - `debounce`: integer value which contains the debounce model update value in milliseconds. A * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a * custom value for each event. For example: * `ngModelOptions="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"` + * - `getterSetter`: boolean value which determines whether or not to treat functions bound to + `ngModel` as getters/setters. * * @example The following example shows how to override immediate updates. Changes on the inputs within the form will update the model only when the control loses focus (blur event). If `escape` key is pressed while the input field is focused, the value is reset to the value in the current model. - <example name="ngModelOptions-directive-blur"> + <example name="ngModelOptions-directive-blur" module="optionsExample"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <form name="userForm"> Name: <input type="text" name="userName" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" @@ -29755,19 +30162,20 @@ </form> <pre>user.name = <span ng-bind="user.name"></span></pre> </div> </file> <file name="app.js"> - function Ctrl($scope) { - $scope.user = { name: 'say', data: '' }; + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say', data: '' }; - $scope.cancel = function (e) { - if (e.keyCode == 27) { - $scope.userForm.userName.$rollbackViewValue(); - } - }; - } + $scope.cancel = function (e) { + if (e.keyCode == 27) { + $scope.userForm.userName.$rollbackViewValue(); + } + }; + }]); </file> <file name="protractor.js" type="protractor"> var model = element(by.binding('user.name')); var input = element(by.model('user.name')); var other = element(by.model('user.data')); @@ -29792,13 +30200,13 @@ </example> This one shows how to debounce model changes. Model will be updated only 1 sec after last change. If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. - <example name="ngModelOptions-directive-debounce"> + <example name="ngModelOptions-directive-debounce" module="optionsExample"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <form name="userForm"> Name: <input type="text" name="userName" ng-model="user.name" ng-model-options="{ debounce: 1000 }" /> @@ -29806,18 +30214,47 @@ </form> <pre>user.name = <span ng-bind="user.name"></span></pre> </div> </file> <file name="app.js"> - function Ctrl($scope) { - $scope.user = { name: 'say' }; - } + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say' }; + }]); </file> </example> + + This one shows how to bind to getter/setters: + + <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> + <file name="index.html"> + <div ng-controller="ExampleController"> + <form name="userForm"> + Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ getterSetter: true }" /> + </form> + <pre>user.name = <span ng-bind="user.name()"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function (newName) { + return angular.isDefined(newName) ? (_name = newName) : _name; + } + }; + }]); + </file> + </example> */ var ngModelOptionsDirective = function() { return { + restrict: 'A', controller: ['$scope', '$attrs', function($scope, $attrs) { var that = this; this.$options = $scope.$eval($attrs.ngModelOptions); // Allow adding/overriding bound events if (this.$options.updateOn !== undefined) { @@ -29845,11 +30282,11 @@ * expression changes. * * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily + * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an * element attribute, it makes the bindings invisible to the user while the page is loading. * * An alternative solution to this problem would be using the * {@link ng.directive:ngCloak ngCloak} directive. @@ -29858,18 +30295,19 @@ * @element ANY * @param {expression} ngBind {@link guide/expression Expression} to evaluate. * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - <example> + <example module="bindExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.name = 'Whirled'; - } + angular.module('bindExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.name = 'Whirled'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Enter name: <input type="text" ng-model="name"><br> Hello <span ng-bind="name"></span>! </div> </file> <file name="protractor.js" type="protractor"> @@ -29916,19 +30354,20 @@ * @param {string} ngBindTemplate template of form * <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval. * * @example * Try it here: enter text in text box and watch the greeting change. - <example> + <example module="bindExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.salutation = 'Hello'; - $scope.name = 'World'; - } + angular.module('bindExample', []) + .controller('ExampleController', ['$scope', function ($scope) { + $scope.salutation = 'Hello'; + $scope.name = 'World'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Salutation: <input type="text" ng-model="salutation"><br> Name: <input type="text" ng-model="name"><br> <pre ng-bind-template="{{salutation}} {{name}}!"></pre> </div> </file> @@ -29982,24 +30421,24 @@ * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. * * @example Try it here: enter text in text box and watch the greeting change. - <example module="ngBindHtmlExample" deps="angular-sanitize.js"> + <example module="bindHtmlExample" deps="angular-sanitize.js"> <file name="index.html"> - <div ng-controller="ngBindHtmlCtrl"> + <div ng-controller="ExampleController"> <p ng-bind-html="myHTML"></p> </div> </file> <file name="script.js"> - angular.module('ngBindHtmlExample', ['ngSanitize']) - - .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) { - $scope.myHTML = - 'I am an <code>HTML</code>string with <a href="#">links!</a> and other <em>stuff</em>'; - }]); + angular.module('bindHtmlExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.myHTML = + 'I am an <code>HTML</code>string with ' + + '<a href="#">links!</a> and other <em>stuff</em>'; + }]); </file> <file name="protractor.js" type="protractor"> it('should check ng-bind-html', function() { expect(element(by.binding('myHTML')).getText()).toBe( @@ -30007,23 +30446,29 @@ }); </file> </example> */ var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); + return { + restrict: 'A', + compile: function (tElement, tAttrs) { + tElement.addClass('ng-binding'); - var parsed = $parse(attr.ngBindHtml); - function getStringValue() { - var value = parsed(scope); - getStringValue.$$unwatch = parsed.$$unwatch; - return (value || '').toString(); - } + return function (scope, element, attr) { + element.data('$binding', attr.ngBindHtml); + var parsed = $parse(attr.ngBindHtml); + var changeDetector = $parse(attr.ngBindHtml, function getStringValue(value) { + return (value || '').toString(); + }); - scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); + scope.$watch(changeDetector, function ngBindHtmlWatchAction() { + // we re-evaluate the expr because we want a TrustedValueHolderType + // for $sce, not a string + element.html($sce.getTrustedHtml(parsed(scope)) || ''); + }); + }; + } }; }]; function classDirective(name, selector) { name = 'ngClass' + name; @@ -30498,11 +30943,11 @@ * * Since there is always a `.` in the bindings, you don't have to worry about prototypal * inheritance masking primitives. * * This example demonstrates the `controller as` syntax. * - * <example name="ngControllerAs"> + * <example name="ngControllerAs" module="controllerAsExample"> * <file name="index.html"> * <div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings"> * Name: <input type="text" ng-model="settings.name"/> * [ <a href="" ng-click="settings.greet()">greet</a> ]<br/> * Contact: @@ -30519,10 +30964,13 @@ * <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> * </ul> * </div> * </file> * <file name="app.js"> + * angular.module('controllerAsExample', []) + * .controller('SettingsController1', SettingsController1); + * * function SettingsController1() { * this.name = "John Smith"; * this.contacts = [ * {type: 'phone', value: '408 555 1212'}, * {type: 'email', value: 'john.smith@example.org'} ]; @@ -30547,42 +30995,42 @@ * }; * </file> * <file name="protractor.js" type="protractor"> * it('should check controller as', function() { * var container = element(by.id('ctrl-as-exmpl')); - * expect(container.findElement(by.model('settings.name')) + * expect(container.element(by.model('settings.name')) * .getAttribute('value')).toBe('John Smith'); * * var firstRepeat = - * container.findElement(by.repeater('contact in settings.contacts').row(0)); + * container.element(by.repeater('contact in settings.contacts').row(0)); * var secondRepeat = - * container.findElement(by.repeater('contact in settings.contacts').row(1)); + * container.element(by.repeater('contact in settings.contacts').row(1)); * - * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('408 555 1212'); * - * expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('john.smith@example.org'); * - * firstRepeat.findElement(by.linkText('clear')).click(); + * firstRepeat.element(by.linkText('clear')).click(); * - * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe(''); * - * container.findElement(by.linkText('add')).click(); + * container.element(by.linkText('add')).click(); * - * expect(container.findElement(by.repeater('contact in settings.contacts').row(2)) - * .findElement(by.model('contact.value')) + * expect(container.element(by.repeater('contact in settings.contacts').row(2)) + * .element(by.model('contact.value')) * .getAttribute('value')) * .toBe('yourname@example.org'); * }); * </file> * </example> * * This example demonstrates the "attach to `$scope`" style of controller. * - * <example name="ngController"> + * <example name="ngController" module="controllerExample"> * <file name="index.html"> * <div id="ctrl-exmpl" ng-controller="SettingsController2"> * Name: <input type="text" ng-model="name"/> * [ <a href="" ng-click="greet()">greet</a> ]<br/> * Contact: @@ -30599,10 +31047,13 @@ * <li>[ <a href="" ng-click="addContact()">add</a> ]</li> * </ul> * </div> * </file> * <file name="app.js"> + * angular.module('controllerExample', []) + * .controller('SettingsController2', ['$scope', SettingsController2]); + * * function SettingsController2($scope) { * $scope.name = "John Smith"; * $scope.contacts = [ * {type:'phone', value:'408 555 1212'}, * {type:'email', value:'john.smith@example.org'} ]; @@ -30628,41 +31079,42 @@ * </file> * <file name="protractor.js" type="protractor"> * it('should check controller', function() { * var container = element(by.id('ctrl-exmpl')); * - * expect(container.findElement(by.model('name')) + * expect(container.element(by.model('name')) * .getAttribute('value')).toBe('John Smith'); * * var firstRepeat = - * container.findElement(by.repeater('contact in contacts').row(0)); + * container.element(by.repeater('contact in contacts').row(0)); * var secondRepeat = - * container.findElement(by.repeater('contact in contacts').row(1)); + * container.element(by.repeater('contact in contacts').row(1)); * - * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('408 555 1212'); - * expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('john.smith@example.org'); * - * firstRepeat.findElement(by.linkText('clear')).click(); + * firstRepeat.element(by.linkText('clear')).click(); * - * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe(''); * - * container.findElement(by.linkText('add')).click(); + * container.element(by.linkText('add')).click(); * - * expect(container.findElement(by.repeater('contact in contacts').row(2)) - * .findElement(by.model('contact.value')) + * expect(container.element(by.repeater('contact in contacts').row(2)) + * .element(by.model('contact.value')) * .getAttribute('value')) * .toBe('yourname@example.org'); * }); * </file> *</example> */ var ngControllerDirective = [function() { return { + restrict: 'A', scope: true, controller: '@', priority: 500 }; }]; @@ -30676,24 +31128,37 @@ * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. * * This is necessary when developing things like Google Chrome Extensions. * * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). - * For us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. + * For Angular to be CSP compatible there are only two things that we need to do differently: * + * - don't use `Function` constructor to generate optimized value getters + * - don't inject custom stylesheet into the document + * * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will * be raised. * * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). * To make those directives work in CSP mode, include the `angular-csp.css` manually. * - * In order to use this feature put the `ngCsp` directive on the root element of the application. + * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This + * autodetection however triggers a CSP error to be logged in the console: * + * ``` + * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of + * script in the following Content Security Policy directive: "default-src 'self'". Note that + * 'script-src' was not explicitly set, so 'default-src' is used as a fallback. + * ``` + * + * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp` + * directive on the root element of the application or on the `angular.js` script tag, whichever + * appears first in the html document. + * * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* * * @example * This example shows how to apply the `ngCsp` directive to the `html` tag. ```html @@ -30703,13 +31168,13 @@ ... </html> ``` */ -// ngCsp is not implemented as a proper directive any more, because we need it be processed while we bootstrap -// the system (before $parse is instantiated), for this reason we just have a csp() fn that looks for ng-csp attribute -// anywhere in the current doc +// ngCsp is not implemented as a proper directive any more, because we need it be processed while we +// bootstrap the system (before $parse is instantiated), for this reason we just have +// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc /** * @ngdoc directive * @name ngClick * @@ -30750,10 +31215,11 @@ 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), function(name) { var directiveName = directiveNormalize('ng-' + name); ngEventDirectives[directiveName] = ['$parse', function($parse) { return { + restrict: 'A', compile: function($element, attr) { var fn = $parse(attr[directiveName]); return function ngEventHandler(scope, element) { element.on(lowercase(name), function(event) { scope.$apply(function() { @@ -31020,25 +31486,26 @@ * @priority 0 * @param {expression} ngSubmit {@link guide/expression Expression} to eval. * ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example module="submitExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.list = []; - $scope.text = 'hello'; - $scope.submit = function() { - if ($scope.text) { - $scope.list.push(this.text); - $scope.text = ''; - } - }; - } + angular.module('submitExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.list = []; + $scope.text = 'hello'; + $scope.submit = function() { + if ($scope.text) { + $scope.list.push(this.text); + $scope.text = ''; + } + }; + }]); </script> - <form ng-submit="submit()" ng-controller="Ctrl"> + <form ng-submit="submit()" ng-controller="ExampleController"> Enter text and hit enter: <input type="text" ng-model="text" name="text" /> <input type="submit" id="submit" value="Submit" /> <pre>list={{list}}</pre> </form> @@ -31046,11 +31513,11 @@ <file name="protractor.js" type="protractor"> it('should check ng-submit', function() { expect(element(by.binding('list')).getText()).toBe('list=[]'); element(by.css('#submit')).click(); expect(element(by.binding('list')).getText()).toContain('hello'); - expect(element(by.input('text')).getAttribute('value')).toBe(''); + expect(element(by.model('text')).getAttribute('value')).toBe(''); }); it('should ignore empty strings', function() { expect(element(by.binding('list')).getText()).toBe('list=[]'); element(by.css('#submit')).click(); element(by.css('#submit')).click(); @@ -31231,20 +31698,21 @@ </file> </example> */ var ngIfDirective = ['$animate', function($animate) { return { + multiElement: true, transclude: 'element', priority: 600, terminal: true, restrict: 'A', $$tlb: true, link: function ($scope, $element, $attr, ctrl, $transclude) { var block, childScope, previousElements; $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { - if (toBoolean(value)) { + if (value) { if (!childScope) { $transclude(function (clone, newScope) { childScope = newScope; clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); // Note: We only need the first/last node of the cloned nodes. @@ -31319,13 +31787,13 @@ * - If the attribute is not set, disable scrolling. * - If the attribute is set without value, enable scrolling. * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="includeExample" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <select ng-model="template" ng-options="t.name for t in templates"> <option value="">(blank)</option> </select> url of the template: <tt>{{template.url}}</tt> <hr/> @@ -31333,16 +31801,17 @@ <div class="slide-animate" ng-include="template.url"></div> </div> </div> </file> <file name="script.js"> - function Ctrl($scope) { - $scope.templates = - [ { name: 'template1.html', url: 'template1.html'}, - { name: 'template2.html', url: 'template2.html'} ]; - $scope.template = $scope.templates[0]; - } + angular.module('includeExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.templates = + [ { name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'} ]; + $scope.template = $scope.templates[0]; + }]); </file> <file name="template1.html"> Content of template1.html </file> <file name="template2.html"> @@ -31401,21 +31870,21 @@ // Firefox can't handle using selects // See https://github.com/angular/protractor/issues/480 return; } templateSelect.click(); - templateSelect.element.all(by.css('option')).get(2).click(); + templateSelect.all(by.css('option')).get(2).click(); expect(includeElem.getText()).toMatch(/Content of template2.html/); }); it('should change to blank', function() { if (browser.params.browser == 'firefox') { // Firefox can't handle using selects return; } templateSelect.click(); - templateSelect.element.all(by.css('option')).get(0).click(); + templateSelect.all(by.css('option')).get(0).click(); expect(includeElem.isPresent()).toBe(false); }); </file> </example> */ @@ -31439,12 +31908,11 @@ */ /** * @ngdoc event - * @name ng.directive:ngInclude#$includeContentError - * @eventOf ng.directive:ngInclude + * @name ngInclude#$includeContentError * @eventType emit on the scope ngInclude was declared in * @description * Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299) */ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', @@ -31576,18 +32044,19 @@ * * @element ANY * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - <example> + <example module="initExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.list = [['a', 'b'], ['c', 'd']]; - } + angular.module('initExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.list = [['a', 'b'], ['c', 'd']]; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <div ng-repeat="innerList in list" ng-init="outerIndex = $index"> <div ng-repeat="value in innerList" ng-init="innerIndex = $index"> <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span> </div> </div> @@ -31723,11 +32192,11 @@ * Notice that we are still using two plural categories(one, other), but we added * three explicit number rules 0, 1 and 2. * When one person, perhaps John, views the document, "John is viewing" will be shown. * When three people view the document, no explicit number rule is found, so * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. - * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * In this case, plural category 'one' is matched and "John, Mary and one other person are viewing" * is shown. * * Note that when you specify offsets, you must provide explicit number rules for * numbers from 0 up to and including the offset. If you use an offset of 3, for example, * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for @@ -31736,20 +32205,21 @@ * @param {string|expression} count The variable to be bound to. * @param {string} when The mapping between plural category to its corresponding strings. * @param {number=} offset Offset to deduct from the total number. * * @example - <example> + <example module="pluralizeExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.person1 = 'Igor'; - $scope.person2 = 'Misko'; - $scope.personCount = 1; - } + angular.module('pluralizeExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.person1 = 'Igor'; + $scope.person2 = 'Misko'; + $scope.personCount = 1; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Person 1:<input type="text" ng-model="person1" value="Igor" /><br/> Person 2:<input type="text" ng-model="person2" value="Misko" /><br/> Number of People:<input type="text" ng-model="personCount" value="1" /><br/> <!--- Example with simple pluralization rules for en locale ---> @@ -31962,10 +32432,17 @@ * before specifying a tracking expression. * * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements * will be associated by item identity in the array. * + * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the + * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message + * when a filter is active on the repeater, but the filtered result set is empty. + * + * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after + * the items have been processed through the filter. + * * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements * with the corresponding item in the array by identity. Moving the same object in array would move the DOM * element in the same way in the DOM. * @@ -31994,13 +32471,16 @@ {name:'Samantha', age:60, gender:'girl'} ]"> I have {{friends.length}} friends. They are: <input type="search" ng-model="q" placeholder="filter friends..." /> <ul class="example-animate-container"> - <li class="animate-repeat" ng-repeat="friend in friends | filter:q"> + <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. </li> + <li class="animate-repeat" ng-if="results.length == 0"> + <strong>No results found...</strong> + </li> </ul> </div> </file> <file name="animations.css"> .example-animate-container { @@ -32064,29 +32544,32 @@ */ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); return { + restrict: 'A', + multiElement: true, transclude: 'element', priority: 1000, terminal: true, $$tlb: true, link: function($scope, $element, $attr, ctrl, $transclude){ var expression = $attr.ngRepeat; - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), - trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), + trackByExp, trackByExpGetter, aliasAs, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, lhs, rhs, valueIdentifier, keyIdentifier, hashFnLocals = {$id: hashKey}; if (!match) { throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); } lhs = match[1]; rhs = match[2]; - trackByExp = match[3]; + aliasAs = match[3]; + trackByExp = match[4]; if (trackByExp) { trackByExpGetter = $parse(trackByExp); trackByIdExpFn = function(key, value, index) { // assign key, value, and $index to the locals so that they can be used in hash functions @@ -32134,10 +32617,14 @@ collectionKeys, block, // last object information {scope, element, id} nextBlockOrder = [], elementsToRemove; + if (aliasAs) { + $scope[aliasAs] = collection; + } + var updateScope = function(scope, index) { scope[valueIdentifier] = value; if (keyIdentifier) scope[keyIdentifier] = key; scope.$index = index; scope.$first = (index === 0); @@ -32272,19 +32759,14 @@ * * <!-- when $scope.myValue is falsy (element is hidden) --> * <div ng-show="myValue" class="ng-hide"></div> * ``` * - * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When true, the ng-hide CSS class is removed + * When the ngShow expression evaluates to a falsy value then the ng-hide CSS class is added to the class + * attribute on the element causing it to become hidden. When truthy, the ng-hide CSS class is removed * from the element causing the element not to appear hidden. * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> - * * ## Why is !important used? * * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector * can be easily overridden by heavier selectors. For example, something as simple * as changing the display style on a HTML list item would make hidden elements appear visible. @@ -32414,14 +32896,18 @@ }); </file> </example> */ var ngShowDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngShow, function ngShowWatchAction(value){ + $animate[value ? 'removeClass' : 'addClass'](element, 'ng-hide'); + }); + } }; }]; /** @@ -32441,19 +32927,14 @@ * * <!-- when $scope.myValue is falsy (element is visible) --> * <div ng-hide="myValue"></div> * ``` * - * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When false, the ng-hide CSS class is removed + * When the ngHide expression evaluates to a truthy value then the .ng-hide CSS class is added to the class + * attribute on the element causing it to become hidden. When falsy, the ng-hide CSS class is removed * from the element causing the element not to appear hidden. * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> - * * ## Why is !important used? * * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector * can be easily overridden by heavier selectors. For example, something as simple * as changing the display style on a HTML list item would make hidden elements appear visible. @@ -32570,14 +33051,18 @@ }); </file> </example> */ var ngHideDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngHide, function ngHideWatchAction(value){ + $animate[value ? 'addClass' : 'removeClass'](element, 'ng-hide'); + }); + } }; }]; /** * @ngdoc directive @@ -32687,13 +33172,13 @@ * are multiple default cases, all of them will be displayed when no other * case match. * * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="switchExample" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <select ng-model="selection" ng-options="item for item in items"> </select> <tt>selection={{selection}}</tt> <hr/> <div class="animate-switch-container" @@ -32703,14 +33188,15 @@ <div class="animate-switch" ng-switch-default>default</div> </div> </div> </file> <file name="script.js"> - function Ctrl($scope) { - $scope.items = ['settings', 'home', 'other']; - $scope.selection = $scope.items[0]; - } + angular.module('switchExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.items = ['settings', 'home', 'other']; + $scope.selection = $scope.items[0]; + }]); </file> <file name="animations.css"> .animate-switch-container { position:relative; background:white; @@ -32749,15 +33235,15 @@ it('should start in settings', function() { expect(switchElem.getText()).toMatch(/Settings Div/); }); it('should change to home', function() { - select.element.all(by.css('option')).get(1).click(); + select.all(by.css('option')).get(1).click(); expect(switchElem.getText()).toMatch(/Home Span/); }); it('should select default', function() { - select.element.all(by.css('option')).get(2).click(); + select.all(by.css('option')).get(2).click(); expect(switchElem.getText()).toMatch(/default/); }); </file> </example> */ @@ -32783,11 +33269,11 @@ previousElements[i].remove(); } previousElements.length = 0; for (i = 0, ii = selectedScopes.length; i < ii; ++i) { - var selected = selectedElements[i]; + var selected = getBlockElements(selectedElements[i].clone); selectedScopes[i].$destroy(); previousElements[i] = selected; $animate.leave(selected, function() { previousElements.splice(i, 1); }); @@ -32797,16 +33283,17 @@ selectedScopes.length = 0; if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { scope.$eval(attr.change); forEach(selectedTranscludes, function(selectedTransclude) { - var selectedScope = scope.$new(); - selectedScopes.push(selectedScope); - selectedTransclude.transclude(selectedScope, function(caseElement) { + selectedTransclude.transclude(function(caseElement, selectedScope) { + selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; + caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); + var block = { clone: caseElement }; - selectedElements.push(caseElement); + selectedElements.push(block); $animate.enter(caseElement, anchor.parent(), anchor); }); }); } }); @@ -32814,63 +33301,64 @@ }; }]; var ngSwitchWhenDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, require: '^ngSwitch', + multiElement: true, link: function(scope, element, attrs, ctrl, $transclude) { ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); } }); var ngSwitchDefaultDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, require: '^ngSwitch', + multiElement: true, link: function(scope, element, attr, ctrl, $transclude) { ctrl.cases['?'] = (ctrl.cases['?'] || []); ctrl.cases['?'].push({ transclude: $transclude, element: element }); } }); /** * @ngdoc directive * @name ngTransclude - * @restrict AC + * @restrict EAC * * @description * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. * * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted. * * @element ANY * * @example - <example module="transclude"> + <example module="transcludeExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.title = 'Lorem Ipsum'; - $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; - } - - angular.module('transclude', []) + angular.module('transcludeExample', []) .directive('pane', function(){ return { restrict: 'E', transclude: true, scope: { title:'@' }, template: '<div style="border: 1px solid black;">' + '<div style="background-color: gray">{{title}}</div>' + - '<div ng-transclude></div>' + + '<ng-transclude></ng-transclude>' + '</div>' }; - }); + }) + .controller('ExampleController', ['$scope', function($scope) { + $scope.title = 'Lorem Ipsum'; + $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <input ng-model="title"><br> <textarea ng-model="text"></textarea> <br/> <pane title="{{title}}">{{text}}</pane> </div> </file> @@ -32888,10 +33376,11 @@ </file> </example> * */ var ngTranscludeDirective = ngDirective({ + restrict: 'EAC', link: function($scope, $element, $attrs, controller, $transclude) { if (!$transclude) { throw minErr('ngTransclude')('orphan', 'Illegal use of ngTransclude directive in the template! ' + 'No parent directive that requires a transclusion found. ' + @@ -33025,25 +33514,26 @@ * * `trackexpr`: Used when working with an array of objects. The result of this expression will be * used to identify the objects in the array. The `trackexpr` will most likely refer to the * `value` variable (e.g. `value.propertyName`). * * @example - <example> + <example module="selectExample"> <file name="index.html"> <script> - function MyCntrl($scope) { - $scope.colors = [ - {name:'black', shade:'dark'}, - {name:'white', shade:'light'}, - {name:'red', shade:'dark'}, - {name:'blue', shade:'dark'}, - {name:'yellow', shade:'light'} - ]; - $scope.myColor = $scope.colors[2]; // red - } + angular.module('selectExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.colors = [ + {name:'black', shade:'dark'}, + {name:'white', shade:'light'}, + {name:'red', shade:'dark'}, + {name:'blue', shade:'dark'}, + {name:'yellow', shade:'light'} + ]; + $scope.myColor = $scope.colors[2]; // red + }]); </script> - <div ng-controller="MyCntrl"> + <div ng-controller="ExampleController"> <ul> <li ng-repeat="color in colors"> Name: <input ng-model="color.name"> [<a href ng-click="colors.splice($index, 1)">X</a>] </li> @@ -33076,22 +33566,26 @@ </div> </file> <file name="protractor.js" type="protractor"> it('should check ng-options', function() { expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); - element.all(by.select('myColor')).first().click(); + element.all(by.model('myColor')).first().click(); element.all(by.css('select[ng-model="myColor"] option')).first().click(); expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); element(by.css('.nullable select[ng-model="myColor"]')).click(); element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); }); </file> </example> */ -var ngOptionsDirective = valueFn({ terminal: true }); +var ngOptionsDirective = valueFn({ + restrict: 'A', + terminal: true +}); + // jshint maxlen: false var selectDirective = ['$compile', '$parse', function($compile, $parse) { //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, nullModelCtrl = {$setViewValue: noop}; @@ -33116,18 +33610,24 @@ nullOption = nullOption_; unknownOption = unknownOption_; }; - self.addOption = function(value) { + self.addOption = function(value, element) { assertNotHasOwnProperty(value, '"option value"'); optionsMap[value] = true; if (ngModelCtrl.$viewValue == value) { $element.val(value); if (unknownOption.parent()) unknownOption.remove(); } + // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459 + // Adding an <option selected="selected"> element to a <select required="required"> should + // automatically select the new element + if (element[0].hasAttribute('selected')) { + element[0].selected = true; + } }; self.removeOption = function(value) { if (this.hasOption(value)) { @@ -33492,10 +33992,16 @@ lastElement.val(existingOption.id = option.id); } // lastElement.prop('selected') provided by jQuery has side-effects if (existingOption.selected !== option.selected) { lastElement.prop('selected', (existingOption.selected = option.selected)); + if (msie) { + // See #7692 + // The selected item wouldn't visually update on IE without this. + // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well + lastElement.prop('selected', existingOption.selected); + } } } else { // grow elements // if it's a null option @@ -33577,14 +34083,14 @@ scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { attr.$set('value', newVal); if (oldVal !== newVal) { selectCtrl.removeOption(oldVal); } - selectCtrl.addOption(newVal); + selectCtrl.addOption(newVal, element); }); } else { - selectCtrl.addOption(attr.value); + selectCtrl.addOption(attr.value, element); } element.on('$destroy', function() { selectCtrl.removeOption(attr.value); }); @@ -45387,12 +45893,14 @@ return this; }); }).call(this); (function() { + var __hasProp = {}.hasOwnProperty; + angular.module("hyperadmin").controller("FormCtrl", function($scope, $state, Restangular, Flash) { - var method, mode, successMessage, target; + var method, mode, prettifyErrors, successMessage, target; this.resource = {}; mode = $state.current.data.mode; if (mode === "new") { method = "post"; target = "admin/" + $scope.resourceClass.plural; @@ -45402,30 +45910,44 @@ target = "admin/" + $scope.resourceClass.plural + "/" + $state.params.id; successMessage = "" + $scope.resourceClass.singular_human + " updated successfully."; Restangular.one(target).get().then((function(_this) { return function(resource) { _this.resource = resource; - return $scope.resourceClass.attributes.forEach(function(attr) { + return $scope.resourceClass.form_attributes.forEach(function(attr) { if (attr.type === "date" || attr.type === "datetime") { if (_this.resource[attr.key]) { return _this.resource[attr.key] = new Date(_this.resource[attr.key]); } } }); }; })(this)); } ["id", "created_at", "updated_at"].forEach(function(key) { - return _.remove($scope.resourceClass.attributes, function(attr) { + return _.remove($scope.resourceClass.form_attributes, function(attr) { return attr.key === key; }); }); + prettifyErrors = function(errors) { + var attr, attrSchema, result; + result = {}; + for (attr in errors) { + if (!__hasProp.call(errors, attr)) continue; + attrSchema = _.find($scope.resourceClass.form_attributes, (function(_this) { + return function(attribute) { + return attribute.key === attr; + }; + })(this)); + result[attrSchema.human] = errors[attr]; + } + return result; + }; this.submit = (function(_this) { return function() { var onError, onSuccess; onError = function(response) { - return _this.errors = response.data; + return _this.errors = prettifyErrors(response.data); }; onSuccess = function(resource) { $state.go("^.show", { id: resource.id }); @@ -45589,18 +46111,18 @@ }).call(this); (function() { angular.module("hyperadmin").directive("resourceForm", function() { return { - template: "<div class=\"row\">\n <div class=\"col-xs-12\" ng-controller=\"FormCtrl as formCtrl\">\n <form-errors errors=\"formCtrl.errors\"></form-errors> \n\n <form class=\"form-horizontal\" name=\"form\" ng-submit=\"formCtrl.submit()\" novalidate>\n <form-input-group ng-repeat=\"attribute in resourceClass.attributes\"\n resource=\"formCtrl.resource\"\n attr=\"attribute.key\"\n errors=\"formCtrl.errors[attribute.key]\"\n human=\"attribute.human\"\n type=\"attribute.type\">\n </form-input-group>\n\n <form-actions form=\"form\" cancel-state=\"resourceClass.plural\"\n </form>\n </div>\n</div>", + template: "<div class=\"row\">\n <div class=\"col-xs-12\" ng-controller=\"FormCtrl as formCtrl\">\n <form-errors errors=\"formCtrl.errors\"></form-errors> \n\n <form class=\"form-horizontal\" name=\"form\" ng-submit=\"formCtrl.submit()\" novalidate>\n <form-input-group ng-repeat=\"attribute in resourceClass.form_attributes\"\n resource=\"formCtrl.resource\"\n attr=\"attribute.key\"\n errors=\"formCtrl.errors[attribute.key]\"\n human=\"attribute.human\"\n type=\"attribute.type\">\n </form-input-group>\n\n <form-actions form=\"form\" cancel-state=\"resourceClass.plural\"\n </form>\n </div>\n</div>", restrict: "E", scope: { resourceClass: "=resourceClass" }, controller: function($scope) { return ["id", "created_at", "updated_at"].forEach(function(attr) { - return delete $scope.resourceClass.attributes[attr]; + return delete $scope.resourceClass.form_attributes[attr]; }); } }; }); @@ -45616,11 +46138,11 @@ }).call(this); (function() { angular.module("hyperadmin").directive("resourceTable", function() { return { - template: "<table class=\"table table-striped\">\n <thead>\n <tr>\n <th ng-repeat=\"attribute in resourceClass.attributes\">\n {{ attribute.human }}\n </th>\n <th>Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr table-row attributes=\"resourceClass.attributes\"\n ng-repeat=\"resource in resources\" resource=\"resource\">\n </tr>\n </tbody>\n</table>", + template: "<table class=\"table table-striped\">\n <thead>\n <tr>\n <th ng-repeat=\"attribute in resourceClass.index_attributes\">\n {{ attribute.human }}\n </th>\n <th>Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr table-row attributes=\"resourceClass.index_attributes\"\n ng-repeat=\"resource in resources\" resource=\"resource\">\n </tr>\n </tbody>\n</table>", scope: { resourceClass: "=", resources: "=" }, restrict: "E" @@ -45738,6 +46260,6 @@ }).call(this); (function() { }).call(this); -;TI"required_assets_digest;TI"%0d2556e7e7b17366a23787b368ed656a;FI" _version;TI"%6584930913c75ff69ba506e1cfbae2c9;F +;TI"required_assets_digest;TI"%814a129ed32ec64c6316ae0c2346df0e;FI" _version;TI"%6584930913c75ff69ba506e1cfbae2c9;F \ No newline at end of file