diff options
Diffstat (limited to 'js/vendor/angular/angular.js')
-rw-r--r-- | js/vendor/angular/angular.js | 474 |
1 files changed, 341 insertions, 133 deletions
diff --git a/js/vendor/angular/angular.js b/js/vendor/angular/angular.js index ff96d85ea..f91161493 100644 --- a/js/vendor/angular/angular.js +++ b/js/vendor/angular/angular.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.3.0-rc.3 + * @license AngularJS v1.3.0-rc.4 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -71,7 +71,7 @@ function minErr(module, ErrorConstructor) { return match; }); - message = message + '\nhttp://errors.angularjs.org/1.3.0-rc.3/' + + message = message + '\nhttp://errors.angularjs.org/1.3.0-rc.4/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + @@ -2112,11 +2112,11 @@ function setupModuleLoader(window) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.3.0-rc.3', // all of these placeholder strings will be replaced by grunt's + full: '1.3.0-rc.4', // all of these placeholder strings will be replaced by grunt's major: 1, // package task minor: 3, dot: 0, - codeName: 'aggressive-pacifism' + codeName: 'unicorn-hydrafication' }; @@ -4653,8 +4653,6 @@ function Browser(window, document, $log, $sniffer) { if (replace) history.replaceState(null, '', url); else { history.pushState(null, '', url); - // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 - baseElement.attr('href', baseElement.attr('href')); } } else { newLocation = url; @@ -5495,8 +5493,11 @@ function $TemplateCacheProvider() { * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. + * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass * `null` to the `link` fn if not found. + * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass + * `null` to the `link` fn if not found. * * * #### `controllerAs` @@ -5577,13 +5578,20 @@ function $TemplateCacheProvider() { * compile the content of the element and make it available to the directive. * Typically used with {@link ng.directive:ngTransclude * ngTransclude}. The advantage of transclusion is that the linking function receives a - * transclusion function which is pre-bound to the correct scope. In a typical setup the widget - * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` - * scope. This makes it possible for the widget to have private state, and the transclusion to - * be bound to the parent (pre-`isolate`) scope. + * transclusion function which is pre-bound to the scope of the position in the DOM from where + * it was taken. + * + * In a typical setup the widget creates an `isolate` scope, but the transcluded + * content has its own **transclusion scope**. While the **transclusion scope** is owned as a child, + * by the **isolate scope**, it prototypically inherits from the original scope from where the + * transcluded content was taken. * - * * `true` - transclude the content of the directive. - * * `'element'` - transclude the whole element including any directives defined at lower priority. + * This makes it possible for the widget to have private state, and the transclusion to + * be bound to the original (pre-`isolate`) scope. + * + * * `true` - transclude the content (i.e. the child nodes) of the directive's element. + * * `'element'` - transclude the whole of the directive's element including any directives on this + * element that defined at a lower priority than this directive. * * <div class="alert alert-warning"> * **Note:** When testing an element transclude directive you must not place the directive at the root of the @@ -5687,7 +5695,6 @@ function $TemplateCacheProvider() { * It is safe to do DOM transformation in the post-linking function on elements that are not waiting * for their async templates to be resolved. * - * <a name="Attributes"></a> * ### Attributes * * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the @@ -5725,7 +5732,7 @@ function $TemplateCacheProvider() { * } * ``` * - * Below is an example using `$compileProvider`. + * ## Example * * <div class="alert alert-warning"> * **Note**: Typically directives are registered with `module.directive`. The example below is @@ -5850,7 +5857,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/, CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/, - ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'); + ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), + REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // The assumption is that future DOM event attribute names will begin with @@ -6155,10 +6163,44 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); - // sanitize a[href] and img[src] values if ((nodeName === 'a' && key === 'href') || (nodeName === 'img' && key === 'src')) { + // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); + } else if (nodeName === 'img' && key === 'srcset') { + // sanitize img[srcset] values + var result = ""; + + // first check if there are spaces because it's not the same pattern + var trimmedSrcset = trim(value); + // ( 999x ,| 999w ,| ,|, ) + var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; + var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; + + // split srcset into tuple of uri and descriptor except for the last item + var rawUris = trimmedSrcset.split(pattern); + + // for each tuples + var nbrUrisWith2parts = Math.floor(rawUris.length / 2); + for (var i=0; i<nbrUrisWith2parts; i++) { + var innerIdx = i*2; + // sanitize the uri + result += $$sanitizeUri(trim( rawUris[innerIdx]), true); + // add the descriptor + result += ( " " + trim(rawUris[innerIdx+1])); + } + + // split the last item into uri and descriptor + var lastTuple = trim(rawUris[i*2]).split(/\s/); + + // sanitize the last uri + result += $$sanitizeUri(trim(lastTuple[0]), true); + + // and add the last descriptor if any + if( lastTuple.length === 2) { + result += (" " + trim(lastTuple[1])); + } + this[key] = value = result; } if (writeAttr !== false) { @@ -6196,12 +6238,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string} key Normalized key. (ie ngAttribute) . * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. - * See the {@link guide/directive#Attributes Directives} guide for more info. + * See {@link ng.$compile#attributes $compile} for more info. * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, - $$observers = (attrs.$$observers || (attrs.$$observers = {})), + $$observers = (attrs.$$observers || (attrs.$$observers = Object.create(null))), listeners = ($$observers[key] || ($$observers[key] = [])); listeners.push(fn); @@ -6449,20 +6491,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { - var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement) { - var scopeCreated = false; + var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { - transcludedScope = scope.$new(); + transcludedScope = scope.$new(false, containingScope); transcludedScope.$$transcluded = true; - scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement); - if (scopeCreated && !elementTransclusion) { - clone.on('$destroy', function() { transcludedScope.$destroy(); }); - } - return clone; + return transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement); }; return boundTranscludeFn; @@ -6872,14 +6908,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function getControllers(directiveName, require, $element, elementControllers) { var value, retrievalMethod = 'data', optional = false; + var $searchElement = $element; + var match; if (isString(require)) { - while((value = require.charAt(0)) == '^' || value == '?') { - require = require.substr(1); - if (value == '^') { - retrievalMethod = 'inheritedData'; - } - optional = optional || value == '?'; + match = require.match(REQUIRE_PREFIX_REGEXP); + require = require.substring(match[0].length); + + if (match[3]) { + if (match[1]) match[3] = null; + else match[1] = match[3]; } + if (match[1] === '^') { + retrievalMethod = 'inheritedData'; + } else if (match[1] === '^^') { + retrievalMethod = 'inheritedData'; + $searchElement = $element.parent(); + } + if (match[2] === '?') { + optional = true; + } + value = null; if (elementControllers && retrievalMethod === 'data') { @@ -6887,7 +6935,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { value = value.instance; } } - value = value || $element[retrievalMethod]('$' + require + 'Controller'); + value = value || $searchElement[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { throw $compileMinErr('ctreq', @@ -7093,7 +7141,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!futureParentElement) { futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; } - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement); + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } } } @@ -7277,6 +7325,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { boundTranscludeFn = linkQueue.shift(), linkNode = $compileNode[0]; + if (scope.$$destroyed) continue; + if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { var oldClasses = beforeTemplateLinkNode.className; @@ -7303,6 +7353,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { var childBoundTranscludeFn = boundTranscludeFn; + if (scope.$$destroyed) return; if (linkQueue) { linkQueue.push(scope); linkQueue.push(node); @@ -10785,6 +10836,11 @@ forEach({ CONSTANTS[name] = constantGetter; }); +//Not quite a constant, but can be lex/parsed the same +CONSTANTS['this'] = function(self) { return self; }; +CONSTANTS['this'].sharedGetter = true; + + //Operators - will be wrapped by binaryFn/unaryFn/assignment/filter var OPERATORS = extend(createMap(), { /* jshint bitwise : false */ @@ -12648,14 +12704,11 @@ function $RootScopeProvider(){ this.$$phase = this.$parent = this.$$watchers = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = null; - this['this'] = this.$root = this; + this.$root = this; this.$$destroyed = false; - this.$$asyncQueue = []; - this.$$postDigestQueue = []; this.$$listeners = {}; this.$$listenerCount = {}; this.$$isolateBindings = null; - this.$$applyAsyncQueue = []; } /** @@ -12704,18 +12757,23 @@ function $RootScopeProvider(){ * When creating widgets, it is useful for the widget to not accidentally read parent * state. * + * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` + * of the newly created scope. Defaults to `this` scope if not provided. + * This is used when creating a transclude scope to correctly place it + * in the scope hierarchy while maintaining the correct prototypical + * inheritance. + * * @returns {Object} The newly created child scope. * */ - $new: function(isolate) { + $new: function(isolate, parent) { var child; + parent = parent || this; + if (isolate) { child = new Scope(); child.$root = this.$root; - // ensure that there is just one async queue per $rootScope and its children - child.$$asyncQueue = this.$$asyncQueue; - child.$$postDigestQueue = this.$$postDigestQueue; } else { // Only create a child scope class if somebody asks for one, // but cache it to allow the VM to optimize lookups. @@ -12732,16 +12790,27 @@ function $RootScopeProvider(){ } child = new this.$$ChildScope(); } - child['this'] = child; - child.$parent = this; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; + child.$parent = parent; + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; } else { - this.$$childHead = this.$$childTail = child; + parent.$$childHead = parent.$$childTail = child; } + + // When the new scope is not isolated or we inherit from `this`, and + // the parent scope is destroyed, the property `$$destroyed` is inherited + // prototypically. In all other cases, this property needs to be set + // when the parent scope is destroyed. + // The listener needs to be added after the parent is set + if (isolate || parent != this) child.$on('$destroy', destroyChild); + return child; + + function destroyChild() { + child.$$destroyed = true; + } }, /** @@ -13217,8 +13286,6 @@ function $RootScopeProvider(){ $digest: function() { var watch, value, last, watchers, - asyncQueue = this.$$asyncQueue, - postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, @@ -13383,6 +13450,10 @@ function $RootScopeProvider(){ if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + // Disable listeners, watchers and apply/digest methods + this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; + this.$on = this.$watch = this.$watchGroup = function() { return noop; }; + this.$$listeners = {}; // All of the code below is bogus code that works around V8's memory leak via optimized code // and inline caches. @@ -13393,15 +13464,7 @@ function $RootScopeProvider(){ // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = this.$root = null; - - // don't reset these to null in case some async task tries to register a listener/watch/task - this.$$listeners = {}; - this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; - - // prevent NPEs since these methods have references to properties we nulled out - this.$destroy = this.$digest = this.$apply = noop; - this.$on = this.$watch = this.$watchGroup = function() { return noop; }; + this.$$childTail = this.$root = this.$$watchers = null; }, /** @@ -13468,19 +13531,19 @@ function $RootScopeProvider(){ $evalAsync: function(expr) { // if we are outside of an $digest loop and this is the first time we are scheduling async // task also schedule async auto-flush - if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + if (!$rootScope.$$phase && !asyncQueue.length) { $browser.defer(function() { - if ($rootScope.$$asyncQueue.length) { + if (asyncQueue.length) { $rootScope.$digest(); } }); } - this.$$asyncQueue.push({scope: this, expression: expr}); + asyncQueue.push({scope: this, expression: expr}); }, $$postDigest : function(fn) { - this.$$postDigestQueue.push(fn); + postDigestQueue.push(fn); }, /** @@ -13564,7 +13627,7 @@ function $RootScopeProvider(){ */ $applyAsync: function(expr) { var scope = this; - expr && $rootScope.$$applyAsyncQueue.push($applyAsyncExpression); + expr && applyAsyncQueue.push($applyAsyncExpression); scheduleApplyAsync(); function $applyAsyncExpression() { @@ -13773,6 +13836,11 @@ function $RootScopeProvider(){ var $rootScope = new Scope(); + //The internal queues. Expose them on the $rootScope for debugging/testing purposes. + var asyncQueue = $rootScope.$$asyncQueue = []; + var postDigestQueue = $rootScope.$$postDigestQueue = []; + var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + return $rootScope; @@ -13806,10 +13874,9 @@ function $RootScopeProvider(){ function initWatchVal() {} function flushApplyAsync() { - var queue = $rootScope.$$applyAsyncQueue; - while (queue.length) { + while (applyAsyncQueue.length) { try { - queue.shift()(); + applyAsyncQueue.shift()(); } catch(e) { $exceptionHandler(e); } @@ -17215,9 +17282,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { parentForm.$addControl(form); - // Setup initial state of the control - element.addClass(PRISTINE_CLASS); - /** * @ngdoc method * @name form.FormController#$rollbackViewValue @@ -17590,9 +17654,12 @@ var formDirectiveFactory = function(isNgForm) { name: 'form', restrict: isNgForm ? 'EAC' : 'E', controller: FormController, - compile: function() { + compile: function ngFormCompile(formElement) { + // Setup initial state of the control + formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); + return { - pre: function(scope, formElement, attr, controller) { + pre: function ngFormPreLink(scope, formElement, attr, controller) { if (!attr.action) { // we can't use jq events because if a form is destroyed during submission the default // action is not prevented. see #1238 @@ -18747,16 +18814,15 @@ function createDateInputType(type, regexp, parseDate, format) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var previousDate; ctrl.$$parserName = type; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; if (regexp.test(value)) { - var previousDate = ctrl.$modelValue; - if (previousDate && timezone === 'UTC') { - var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); - previousDate = new Date(previousDate.getTime() + timezoneOffset); - } + // Note: We cannot read ctrl.$modelValue, as there might be a different + // parser/formatter in the processing chain so that the model + // contains some different data format! var parsedDate = parseDate(value, previousDate); if (timezone === 'UTC') { parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); @@ -18767,8 +18833,18 @@ function createDateInputType(type, regexp, parseDate, format) { }); ctrl.$formatters.push(function(value) { - if (isDate(value)) { + if (!ctrl.$isEmpty(value)) { + if (!isDate(value)) { + throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + } + previousDate = value; + if (previousDate && timezone === 'UTC') { + var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); + previousDate = new Date(previousDate.getTime() + timezoneOffset); + } return $filter('date')(value, format, timezone); + } else { + previousDate = null; } return ''; }); @@ -18794,6 +18870,11 @@ function createDateInputType(type, regexp, parseDate, format) { ctrl.$validate(); }); } + // Override the standard $isEmpty to detect invalid dates as well + ctrl.$isEmpty = function(value) { + // Invalid Date: getTime() returns NaN + return !value || (value.getTime && value.getTime() !== value.getTime()); + }; function parseObservedDateValue(val) { return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; @@ -19108,10 +19189,12 @@ var inputDirective = ['$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, $parse); + link: { + pre: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + $browser, $filter, $parse); + } } } }; @@ -19412,11 +19495,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ var parentForm = $element.inheritedData('$formController') || nullFormCtrl, currentValidationRunId = 0; - // Setup initial state of the control - $element - .addClass(PRISTINE_CLASS) - .addClass(UNTOUCHED_CLASS); - /** * @ngdoc method * @name ngModel.NgModelController#$setValidity @@ -19709,14 +19787,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ }; this.$$parseAndValidate = function() { - var parserValid = true, - viewValue = ctrl.$$lastCommittedViewValue, - modelValue = viewValue; - for(var i = 0; i < ctrl.$parsers.length; i++) { - modelValue = ctrl.$parsers[i](modelValue); - if (isUndefined(modelValue)) { - parserValid = false; - break; + var viewValue = ctrl.$$lastCommittedViewValue; + var modelValue = viewValue; + var parserValid = isUndefined(modelValue) ? undefined : true; + + if (parserValid) { + for(var i = 0; i < ctrl.$parsers.length; i++) { + modelValue = ctrl.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + parserValid = false; + break; + } } } if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { @@ -20033,42 +20114,51 @@ var ngModelDirective = function() { restrict: 'A', require: ['ngModel', '^?form', '^?ngModelOptions'], controller: NgModelController, - link: { - pre: function(scope, element, attr, ctrls) { - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; + // Prelink needs to run before any input directive + // so that we can set the NgModelOptions in NgModelController + // before anyone else uses it. + priority: 1, + compile: function ngModelCompile(element) { + // Setup initial state of the control + element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); - modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); + return { + pre: function ngModelPreLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || nullFormCtrl; - // notify others, especially parent forms - formCtrl.$addControl(modelCtrl); + modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); - attr.$observe('name', function(newValue) { - if (modelCtrl.$name !== newValue) { - formCtrl.$$renameControl(modelCtrl, newValue); - } - }); + // notify others, especially parent forms + formCtrl.$addControl(modelCtrl); - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); - }, - post: function(scope, element, attr, ctrls) { - var modelCtrl = ctrls[0]; - if (modelCtrl.$options && modelCtrl.$options.updateOn) { - element.on(modelCtrl.$options.updateOn, function(ev) { - modelCtrl.$$debounceViewValueCommit(ev && ev.type); + attr.$observe('name', function(newValue) { + if (modelCtrl.$name !== newValue) { + formCtrl.$$renameControl(modelCtrl, newValue); + } }); - } - element.on('blur', function(ev) { - if (modelCtrl.$touched) return; + scope.$on('$destroy', function() { + formCtrl.$removeControl(modelCtrl); + }); + }, + post: function ngModelPostLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + if (modelCtrl.$options && modelCtrl.$options.updateOn) { + element.on(modelCtrl.$options.updateOn, function(ev) { + modelCtrl.$$debounceViewValueCommit(ev && ev.type); + }); + } - scope.$apply(function() { - modelCtrl.$setTouched(); + element.on('blur', function(ev) { + if (modelCtrl.$touched) return; + + scope.$apply(function() { + modelCtrl.$setTouched(); + }); }); - }); - } + } + }; } }; }; @@ -20623,8 +20713,9 @@ function addSetValidityMethod(context) { parentForm = context.parentForm, $animate = context.$animate; + classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); + ctrl.$setValidity = setValidity; - toggleValidationCss('', true); function setValidity(validationErrorKey, state, options) { if (state === undefined) { @@ -21624,7 +21715,125 @@ var ngControllerDirective = [function() { ... </html> ``` - */ + * @example + // Note: the suffix `.csp` in the example name triggers + // csp mode in our http server! + <example name="example.csp" module="cspExample" ng-csp="true"> + <file name="index.html"> + <div ng-controller="MainController as ctrl"> + <div> + <button ng-click="ctrl.inc()" id="inc">Increment</button> + <span id="counter"> + {{ctrl.counter}} + </span> + </div> + + <div> + <button ng-click="ctrl.evil()" id="evil">Evil</button> + <span id="evilError"> + {{ctrl.evilError}} + </span> + </div> + </div> + </file> + <file name="script.js"> + angular.module('cspExample', []) + .controller('MainController', function() { + this.counter = 0; + this.inc = function() { + this.counter++; + }; + this.evil = function() { + // jshint evil:true + try { + eval('1+2'); + } catch (e) { + this.evilError = e.message; + } + }; + }); + </file> + <file name="protractor.js" type="protractor"> + var util, webdriver; + + var incBtn = element(by.id('inc')); + var counter = element(by.id('counter')); + var evilBtn = element(by.id('evil')); + var evilError = element(by.id('evilError')); + + function getAndClearSevereErrors() { + return browser.manage().logs().get('browser').then(function(browserLog) { + return browserLog.filter(function(logEntry) { + return logEntry.level.value > webdriver.logging.Level.WARNING.value; + }); + }); + } + + function clearErrors() { + getAndClearSevereErrors(); + } + + function expectNoErrors() { + getAndClearSevereErrors().then(function(filteredLog) { + expect(filteredLog.length).toEqual(0); + if (filteredLog.length) { + console.log('browser console errors: ' + util.inspect(filteredLog)); + } + }); + } + + function expectError(regex) { + getAndClearSevereErrors().then(function(filteredLog) { + var found = false; + filteredLog.forEach(function(log) { + if (log.message.match(regex)) { + found = true; + } + }); + if (!found) { + throw new Error('expected an error that matches ' + regex); + } + }); + } + + beforeEach(function() { + util = require('util'); + webdriver = require('protractor/node_modules/selenium-webdriver'); + }); + + // For now, we only test on Chrome, + // as Safari does not load the page with Protractor's injected scripts, + // and Firefox webdriver always disables content security policy (#6358) + if (browser.params.browser !== 'chrome') { + return; + } + + it('should not report errors when the page is loaded', function() { + // clear errors so we are not dependent on previous tests + clearErrors(); + // Need to reload the page as the page is already loaded when + // we come here + browser.driver.getCurrentUrl().then(function(url) { + browser.get(url); + }); + expectNoErrors(); + }); + + it('should evaluate expressions', function() { + expect(counter.getText()).toEqual('0'); + incBtn.click(); + expect(counter.getText()).toEqual('1'); + expectNoErrors(); + }); + + it('should throw and report an error when using "eval"', function() { + evilBtn.click(); + expect(evilError.getText()).toMatch(/Content Security Policy/); + expectError(/Content Security Policy/); + }); + </file> + </example> + */ // 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 @@ -24600,6 +24809,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { id: option.id, selected: option.selected }); + selectCtrl.addOption(option.label, element); if (lastElement) { lastElement.after(element); } else { @@ -24611,7 +24821,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // remove any excessive OPTIONs in a group index++; // increment since the existingOptions[0] is parent element not OPTION while(existingOptions.length > index) { - existingOptions.pop().element.remove(); + option = existingOptions.pop(); + selectCtrl.removeOption(option.label); + option.element.remove(); } } // remove any excessive OPTGROUPs from select |