vendor/assets/javascripts/unstable/angular-scenario.js in angularjs-rails-1.2.25 vs vendor/assets/javascripts/unstable/angular-scenario.js in angularjs-rails-1.2.26

- old
+ new

@@ -9188,11 +9188,11 @@ return jQuery; })); /** - * @license AngularJS v1.3.0-rc.3 + * @license AngularJS v1.3.0-rc.5 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, document){ var _jQuery = window.jQuery.noConflict(true); @@ -9261,11 +9261,11 @@ return arg; } return match; }); - message = message + '\nhttp://errors.angularjs.org/1.3.0-rc.3/' + + message = message + '\nhttp://errors.angularjs.org/1.3.0-rc.5/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + encodeURIComponent(stringify(arguments[i])); } @@ -9277,10 +9277,11 @@ /* global angular: true, msie: true, jqLite: true, jQuery: true, slice: true, + splice: true, push: true, toString: true, ngMinErr: true, angularModule: true, uid: true, @@ -9353,10 +9354,16 @@ assertNotHasOwnProperty: true, getter: true, getBlockNodes: true, hasOwnProperty: true, createMap: true, + + NODE_TYPE_ELEMENT: true, + NODE_TYPE_TEXT: true, + NODE_TYPE_COMMENT: true, + NODE_TYPE_DOCUMENT: true, + NODE_TYPE_DOCUMENT_FRAGMENT: true, */ //////////////////////////////////// /** @@ -9432,27 +9439,25 @@ var /** holds major version number for IE or NaN for real browsers */ msie, jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, + splice = [].splice, push = [].push, toString = Object.prototype.toString, ngMinErr = minErr('ng'), /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, uid = 0; /** - * IE 11 changed the format of the UserAgent string. - * See http://msdn.microsoft.com/en-us/library/ms537503.aspx + * documentMode is an IE-only property + * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ -msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); -if (isNaN(msie)) { - msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); -} +msie = document.documentMode; /** * @private * @param {*} obj @@ -9464,11 +9469,11 @@ return false; } var length = obj.length; - if (obj.nodeType === 1 && length) { + if (obj.nodeType === NODE_TYPE_ELEMENT && length) { return true; } return isString(obj) || isArray(obj) || length === 0 || typeof length === 'number' && length > 0 && (length - 1) in obj; @@ -10300,15 +10305,13 @@ try { // turns out IE does not let you set .html() on elements which // are not allowed to have children. So we just ignore it. element.empty(); } catch(e) {} - // As Per DOM Standards - var TEXT_NODE = 3; var elemHtml = jqLite('<div>').append(element).html(); try { - return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : + return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); } catch(e) { return lowercase(elemHtml); @@ -10883,10 +10886,16 @@ */ function createMap() { return Object.create(null); } +var NODE_TYPE_ELEMENT = 1; +var NODE_TYPE_TEXT = 3; +var NODE_TYPE_COMMENT = 8; +var NODE_TYPE_DOCUMENT = 9; +var NODE_TYPE_DOCUMENT_FRAGMENT = 11; + /** * @ngdoc type * @name angular.Module * @module ng * @description @@ -11302,15 +11311,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-rc.3', // all of these placeholder strings will be replaced by grunt's + full: '1.3.0-rc.5', // all of these placeholder strings will be replaced by grunt's major: 1, // package task minor: 3, dot: 0, - codeName: 'aggressive-pacifism' + codeName: 'impossible-choreography' }; function publishExternalAPI(angular){ extend(angular, { @@ -11340,12 +11349,11 @@ 'uppercase': uppercase, 'callbacks': {counter: 0}, 'getTestability': getTestability, '$$minErr': minErr, '$$csp': csp, - 'reloadWithDebugInfo': reloadWithDebugInfo, - '$$hasClass': jqLiteHasClass + 'reloadWithDebugInfo': reloadWithDebugInfo }); angularModule = setupModuleLoader(window); try { angularModule('ngLocale'); @@ -11487,11 +11495,11 @@ * - [`attr()`](http://api.jquery.com/attr/) * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyles()` * - [`data()`](http://api.jquery.com/data/) * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) * - [`eq()`](http://api.jquery.com/eq/) * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name @@ -11609,11 +11617,11 @@ function jqLiteAcceptsData(node) { // The window object can accept data but has no nodeType // Otherwise we are only interested in elements (1) and documents (9) var nodeType = node.nodeType; - return nodeType === 1 || !nodeType || nodeType === 9; + return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; } function jqLiteBuildFragment(html, context) { var tmp, tag, wrap, fragment = context.createDocumentFragment(), @@ -11862,11 +11870,11 @@ } function jqLiteInheritedData(element, name, value) { // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element.nodeType == 9) { + if(element.nodeType == NODE_TYPE_DOCUMENT) { element = element.documentElement; } var names = isArray(name) ? name : [name]; while (element) { @@ -11875,11 +11883,11 @@ } // 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 = element.parentNode || (element.nodeType === 11 && element.host); + element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); } } function jqLiteEmpty(element) { jqLiteDealoc(element, true); @@ -12053,11 +12061,11 @@ return getText; function getText(element, value) { if (isUndefined(value)) { var nodeType = element.nodeType; - return (nodeType === 1 || nodeType === 3) ? element.textContent : ''; + return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; } element.textContent = value; } })(), @@ -12275,11 +12283,11 @@ }, children: function(element) { var children = []; forEach(element.childNodes, function(element){ - if (element.nodeType === 1) + if (element.nodeType === NODE_TYPE_ELEMENT) children.push(element); }); return children; }, @@ -12287,22 +12295,22 @@ return element.contentDocument || element.childNodes || []; }, append: function(element, node) { var nodeType = element.nodeType; - if (nodeType !== 1 && nodeType !== 11) return; + if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; node = new JQLite(node); for (var i = 0, ii = node.length; i < ii; i++) { var child = node[i]; element.appendChild(child); } }, prepend: function(element, node) { - if (element.nodeType === 1) { + if (element.nodeType === NODE_TYPE_ELEMENT) { var index = element.firstChild; forEach(new JQLite(node), function(child){ element.insertBefore(child, index); }); } @@ -12349,11 +12357,11 @@ } }, parent: function(element) { var parent = element.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; + return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; }, next: function(element) { return element.nextElementSibling; }, @@ -12508,17 +12516,17 @@ * @module ng * @name angular.injector * @kind function * * @description - * Creates an injector function that can be used for retrieving services as well as for + * Creates an injector object that can be used for retrieving services as well as for * dependency injection (see {@link guide/di dependency injection}). * * @param {Array.<string|Function>} modules A list of module functions or their aliases. See * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link auto.$injector $injector}. + * @returns {function()} Injector object. See {@link auto.$injector $injector}. * * @example * Typical usage * ```js * // create an injector @@ -12621,11 +12629,10 @@ /////////////////////////////////////// /** * @ngdoc service * @name $injector - * @kind function * * @description * * `$injector` is used to retrieve object instances as defined by * {@link auto.$provide provider}, instantiate types, invoke methods, @@ -12636,11 +12643,11 @@ * ```js * var $injector = angular.injector(); * expect($injector.get('$injector')).toBe($injector); * expect($injector.invoke(function($injector) { * return $injector; - * }).toBe($injector); + * })).toBe($injector); * ``` * * # Injection Function Annotation * * JavaScript does not have annotations, and annotations are needed for dependency injection. The @@ -12703,12 +12710,12 @@ * @name $injector#has * * @description * Allows the user to query if the particular service exists. * - * @param {string} Name of the service to query. - * @returns {boolean} returns true if injector has given service. + * @param {string} name Name of the service to query. + * @returns {boolean} `true` if injector has given service. */ /** * @ngdoc method * @name $injector#instantiate @@ -13164,19 +13171,33 @@ throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); } return providerCache[name + providerSuffix] = provider_; } - function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } + function enforceReturnValue(name, factory) { + return function enforcedReturnValue() { + var result = instanceInjector.invoke(factory); + if (isUndefined(result)) { + throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name); + } + return result; + }; + } + function factory(name, factoryFn, enforce) { + return provider(name, { + $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn + }); + } + function service(name, constructor) { return factory(name, ['$injector', function($injector) { return $injector.instantiate(constructor); }]); } - function value(name, val) { return factory(name, valueFn(val)); } + function value(name, val) { return factory(name, valueFn(val), false); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); providerCache[name] = value; instanceCache[name] = value; @@ -13423,11 +13444,14 @@ // does not scroll when user clicks on anchor link that is currently on // (no url change, no $location.hash() change), browser native does scroll if (autoScrollingEnabled) { $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, - function autoScrollWatchAction() { + function autoScrollWatchAction(newVal, oldVal) { + // skip the initial scroll if $location.hash is empty + if (newVal === oldVal && newVal === '') return; + $rootScope.$evalAsync(scroll); }); } return scroll; @@ -13513,13 +13537,61 @@ this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; } return this.$$classNameFilter; }; - this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) { + this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) { var currentDefer; + + function runAnimationPostDigest(fn) { + var cancelFn, defer = $$q.defer(); + defer.promise.$$cancelFn = function ngAnimateMaybeCancel() { + cancelFn && cancelFn(); + }; + + $rootScope.$$postDigest(function ngAnimatePostDigest() { + cancelFn = fn(function ngAnimateNotifyComplete() { + defer.resolve(); + }); + }); + + return defer.promise; + } + + function resolveElementClasses(element, cache) { + var toAdd = [], toRemove = []; + + var hasClasses = createMap(); + forEach((element.attr('class') || '').split(/\s+/), function(className) { + hasClasses[className] = true; + }); + + forEach(cache.classes, function(status, className) { + var hasClass = hasClasses[className]; + + // If the most recent class manipulation (via $animate) was to remove the class, and the + // element currently has the class, the class is scheduled for removal. Otherwise, if + // the most recent class manipulation (via $animate) was to add the class, and the + // element does not currently have the class, the class is scheduled to be added. + if (status === false && hasClass) { + toRemove.push(className); + } else if (status === true && !hasClass) { + toAdd.push(className); + } + }); + + return (toAdd.length + toRemove.length) > 0 && [toAdd.length && toAdd, toRemove.length && toRemove]; + } + + function cachedClassManipulation(cache, classes, op) { + for (var i=0, ii = classes.length; i < ii; ++i) { + var className = classes[i]; + cache[className] = op; + } + } + function asyncPromise() { // only serve one instance of a promise in order to save CPU cycles if (!currentDefer) { currentDefer = $$q.defer(); $$asyncCallback(function() { @@ -13619,17 +13691,21 @@ * added to it * @param {string} className the CSS class which will be added to the element * @return {Promise} the animation callback promise */ addClass : function(element, className) { + return this.setClass(element, className, []); + }, + + $$addClassImmediately : function addClassImmediately(element, className) { + element = jqLite(element); className = !isString(className) ? (isArray(className) ? className.join(' ') : '') : className; forEach(element, function (element) { jqLiteAddClass(element, className); }); - return asyncPromise(); }, /** * * @ngdoc method @@ -13641,10 +13717,15 @@ * removed from it * @param {string} className the CSS class which will be removed from the element * @return {Promise} the animation callback promise */ removeClass : function(element, className) { + return this.setClass(element, [], className); + }, + + $$removeClassImmediately : function removeClassImmediately(element, className) { + element = jqLite(element); className = !isString(className) ? (isArray(className) ? className.join(' ') : '') : className; forEach(element, function (element) { jqLiteRemoveClass(element, className); @@ -13663,14 +13744,57 @@ * removed from it * @param {string} add the CSS classes which will be added to the element * @param {string} remove the CSS class which will be removed from the element * @return {Promise} the animation callback promise */ - setClass : function(element, add, remove) { - this.addClass(element, add); - this.removeClass(element, remove); - return asyncPromise(); + setClass : function(element, add, remove, runSynchronously) { + var self = this; + var STORAGE_KEY = '$$animateClasses'; + var createdCache = false; + element = jqLite(element); + + if (runSynchronously) { + // TODO(@caitp/@matsko): Remove undocumented `runSynchronously` parameter, and always + // perform DOM manipulation asynchronously or in postDigest. + self.$$addClassImmediately(element, add); + self.$$removeClassImmediately(element, remove); + return asyncPromise(); + } + + var cache = element.data(STORAGE_KEY); + if (!cache) { + cache = { + classes: {} + }; + createdCache = true; + } + + var classes = cache.classes; + + add = isArray(add) ? add : add.split(' '); + remove = isArray(remove) ? remove : remove.split(' '); + cachedClassManipulation(classes, add, true); + cachedClassManipulation(classes, remove, false); + + if (createdCache) { + cache.promise = runAnimationPostDigest(function(done) { + var cache = element.data(STORAGE_KEY); + element.removeData(STORAGE_KEY); + + var classes = cache && resolveElementClasses(element, cache); + + if (classes) { + if (classes[0]) self.$$addClassImmediately(element, classes[0]); + if (classes[1]) self.$$removeClassImmediately(element, classes[1]); + } + + done(); + }); + element.data(STORAGE_KEY, cache); + } + + return cache.promise; }, enabled : noop, cancel : noop }; @@ -13685,10 +13809,12 @@ return $timeout(fn, 0, false); }; }]; } +/* global stripHash: true */ + /** * ! This is a private undocumented service ! * * @name $browser * @requires $log @@ -13808,12 +13934,13 @@ ////////////////////////////////////////////////////////////// // URL API ////////////////////////////////////////////////////////////// var lastBrowserUrl = location.href, + lastHistoryState = history.state, baseElement = document.find('base'), - newLocation = null; + reloadLocation = null; /** * @name $browser#url * * @description @@ -13828,56 +13955,87 @@ * * NOTE: this api is intended for use only by the $location service. Please use the * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record ? + * @param {boolean=} replace Should new url replace current history record? + * @param {object=} state object to use with pushState/replaceState */ - self.url = function(url, replace) { + self.url = function(url, replace, state) { + // In modern browsers `history.state` is `null` by default; treating it separately + // from `undefined` would cause `$browser.url('/foo')` to change `history.state` + // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. + if (isUndefined(state)) { + state = null; + } + // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; // setter if (url) { - if (lastBrowserUrl == url) return; + // Don't change anything if previous and current URLs and states match. This also prevents + // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. + // See https://github.com/angular/angular.js/commit/ffb2701 + if (lastBrowserUrl === url && (!$sniffer.history || history.state === state)) { + return; + } + var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; - if ($sniffer.history) { - 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')); - } + // Don't use history API if only the hash changed + // due to a bug in IE10/IE11 which leads + // to not firing a `hashchange` nor `popstate` event + // in some cases (see #9143). + if ($sniffer.history && (!sameBase || history.state !== state)) { + history[replace ? 'replaceState' : 'pushState'](state, '', url); + lastHistoryState = history.state; } else { - newLocation = url; + if (!sameBase) { + reloadLocation = url; + } if (replace) { location.replace(url); } else { location.href = url; } } return self; // getter } else { - // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href - // methods not updating location.href synchronously. + // - reloadLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return newLocation || location.href.replace(/%27/g,"'"); + return reloadLocation || location.href.replace(/%27/g,"'"); } }; + /** + * @name $browser#state + * + * @description + * This method is a getter. + * + * Return history.state or null if history.state is undefined. + * + * @returns {object} state + */ + self.state = function() { + return isUndefined(history.state) ? null : history.state; + }; + var urlChangeListeners = [], urlChangeInit = false; function fireUrlChange() { - newLocation = null; - if (lastBrowserUrl == self.url()) return; + if (lastBrowserUrl === self.url() && lastHistoryState === history.state) { + return; + } lastBrowserUrl = self.url(); forEach(urlChangeListeners, function(listener) { - listener(self.url()); + listener(self.url(), history.state); }); } /** * @name $browser#onUrlChange @@ -13908,13 +14066,11 @@ // changed by push/replaceState // html5 history api - popstate event if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); // hashchange event - if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); - // polling - else self.addPollFn(fireUrlChange); + jqLite(window).on('hashchange', fireUrlChange); urlChangeInit = true; } urlChangeListeners.push(callback); @@ -14685,12 +14841,15 @@ * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with: * * * (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` * Controller alias at the directive scope. An alias for the controller so it * can be referenced at the directive template. The directive needs to define a scope for this @@ -14764,27 +14923,23 @@ * There are very few scenarios where element replacement is required for the application function, * the main one being reusable custom components that are used within SVG contexts * (because SVG doesn't work with custom elements in the DOM tree). * * #### `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 - * 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. + * Extract the contents of the element where the directive appears and make it available to the directive. + * The contents are compiled and provided to the directive as a **transclusion function**. See the + * {@link $compile#transclusion Transclusion} section below. * - * * `true` - transclude the content of the directive. - * * `'element'` - transclude the whole element including any directives defined at lower priority. + * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the + * directive's element or the entire element: * - * <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> + * * `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. When used, the `template` + * property is ignored. * + * * #### `compile` * * ```js * function compile(tElement, tAttrs, transclude) { ... } * ``` @@ -14877,11 +15032,125 @@ * compilation and linking has been suspended until that occurs. * * 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> + * + * ### Transclusion + * + * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and + * copying them to another part of the DOM, while maintaining their connection to the original AngularJS + * scope from where they were taken. + * + * Transclusion is used (often with {@link ngTransclude}) to insert the + * original contents of a directive's element into a specified place in the template of the directive. + * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded + * content has access to the properties on the scope from which it was taken, even if the directive + * has isolated scope. + * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. + * + * This makes it possible for the widget to have private state for its template, while the transcluded + * content has access to its originating scope. + * + * <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> + * + * #### Transclusion Functions + * + * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion + * function** to the directive's `link` function and `controller`. This transclusion function is a special + * **linking function** that will return the compiled contents linked to a new transclusion scope. + * + * <div class="alert alert-info"> + * If you are just using {@link ngTransclude} then you don't need to worry about this function, since + * ngTransclude will deal with it for us. + * </div> + * + * If you want to manually control the insertion and removal of the transcluded content in your directive + * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery + * object that contains the compiled DOM, which is linked to the correct transclusion scope. + * + * When you call a transclusion function you can pass in a **clone attach function**. This function is accepts + * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded + * content and the `scope` is the newly created transclusion scope, to which the clone is bound. + * + * <div class="alert alert-info"> + * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function + * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. + * </div> + * + * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone + * attach function**: + * + * ```js + * var transcludedContent, transclusionScope; + * + * $transclude(function(clone, scope) { + * element.append(clone); + * transcludedContent = clone; + * transclusionScope = scope; + * }); + * ``` + * + * Later, if you want to remove the transcluded content from your DOM then you should also destroy the + * associated transclusion scope: + * + * ```js + * transcludedContent.remove(); + * transclusionScope.$destroy(); + * ``` + * + * <div class="alert alert-info"> + * **Best Practice**: if you intend to add and remove transcluded content manually in your directive + * (by calling the transclude function to get the DOM and and calling `element.remove()` to remove it), + * then you are also responsible for calling `$destroy` on the transclusion scope. + * </div> + * + * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} + * automatically destroy their transluded clones as necessary so you do not need to worry about this if + * you are simply using {@link ngTransclude} to inject the transclusion into your directive. + * + * + * #### Transclusion Scopes + * + * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion + * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed + * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it + * was taken. + * + * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look + * like this: + * + * ```html + * <div ng-app> + * <div isolate> + * <div transclusion> + * </div> + * </div> + * </div> + * ``` + * + * The `$parent` scope hierarchy will look like this: + * + * ``` + * - $rootScope + * - isolate + * - transclusion + * ``` + * + * but the scopes will inherit prototypically from different scopes to their `$parent`. + * + * ``` + * - $rootScope + * - transclusion + * - isolate + * ``` + * + * * ### Attributes * * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the * `link()` or `compile()` functions. It has a variety of uses. * @@ -14915,11 +15184,11 @@ * console.log('ngModel has changed value to ' + value); * }); * } * ``` * - * Below is an example using `$compileProvider`. + * ## Example * * <div class="alert alert-warning"> * **Note**: Typically directives are registered with `module.directive`. The example below is * to illustrate how `$compile` works. * </div> @@ -15040,11 +15309,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, 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 // 'on' and be composed of only English letters. var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; @@ -15345,14 +15615,48 @@ } } 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) { if (value === null || value === undefined) { this.$$element.removeAttr(attrName); @@ -15386,16 +15690,16 @@ * changes. * * @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); $rootScope.$evalAsync(function() { if (!listeners.$$inter) { @@ -15467,41 +15771,42 @@ $compileNodes = jqLite($compileNodes); } // We can not compile top level text elements since text nodes can be merged and we will // not be able to attach scope data to them, so we will wrap them in <span> forEach($compileNodes, function(node, index){ - if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { + if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) { $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0]; } }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); compile.$$addScopeClass($compileNodes); var namespace = null; - var namespaceAdaptedCompileNodes = $compileNodes; - var lastCompileNode; return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){ assertArg(scope, 'scope'); if (!namespace) { namespace = detectNamespaceForChildElements(futureParentElement); } - if (namespace !== 'html' && $compileNodes[0] !== lastCompileNode) { - namespaceAdaptedCompileNodes = jqLite( + var $linkNode; + if (namespace !== 'html') { + // When using a directive with replace:true and templateUrl the $compileNodes + // (or a child element inside of them) + // might change, so we need to recreate the namespace adapted compileNodes + // for call to the link function. + // Note: This will already clone the nodes... + $linkNode = jqLite( wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html()) ); + } else if (cloneConnectFn) { + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + $linkNode = JQLitePrototype.clone.call($compileNodes); + } else { + $linkNode = $compileNodes; } - // When using a directive with replace:true and templateUrl the $compileNodes - // might change, so we need to recreate the namespace adapted compileNodes. - lastCompileNode = $compileNodes[0]; - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call(namespaceAdaptedCompileNodes) // IMPORTANT!!! - : namespaceAdaptedCompileNodes; - if (transcludeControllers) { for (var controllerName in transcludeControllers) { $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); } } @@ -15639,24 +15944,18 @@ } } 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; } @@ -15675,11 +15974,11 @@ attrsMap = attrs.$attr, match, className; switch(nodeType) { - case 1: /* Element */ + case NODE_TYPE_ELEMENT: /* Element */ // use the node name: <directive> addDirective(directives, directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); // iterate over the attributes @@ -15687,41 +15986,39 @@ 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); + name = attr.name; + value = trim(attr.value); - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { - name = snake_case(ngAttrName.substr(6), '-'); - } + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + name = snake_case(ngAttrName.substr(6), '-'); + } - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (directiveIsMultiElement(directiveNName)) { - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); - } + var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); + 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; - if (isNgAttr || !attrs.hasOwnProperty(nName)) { - attrs[nName] = value; - if (getBooleanAttrName(node, nName)) { - attrs[nName] = true; // presence means true - } - } - addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); + nName = directiveNormalize(name.toLowerCase()); + attrsMap[nName] = name; + if (isNgAttr || !attrs.hasOwnProperty(nName)) { + attrs[nName] = value; + if (getBooleanAttrName(node, nName)) { + attrs[nName] = true; // presence means true + } } + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); } // use class as directive className = node.className; if (isString(className) && className !== '') { @@ -15732,14 +16029,14 @@ } className = className.substr(match.index + match[0].length); } } break; - case 3: /* Text Node */ + case NODE_TYPE_TEXT: /* Text Node */ addTextInterpolateDirective(directives, node.nodeValue); break; - case 8: /* Comment */ + case NODE_TYPE_COMMENT: /* Comment */ try { match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); if (match) { nName = directiveNormalize(match[1]); if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { @@ -15775,11 +16072,11 @@ if (!node) { throw $compileMinErr('uterdir', "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); } - if (node.nodeType == 1 /** Element **/) { + if (node.nodeType == NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } nodes.push(node); node = node.nextSibling; @@ -15954,15 +16251,15 @@ if (directive.replace) { replaceDirective = directive; if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(wrapTemplate(directive.templateNamespace, trim(directiveValue))); + $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", directiveName, ''); } @@ -16062,26 +16359,38 @@ } 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') { if (value = elementControllers[require]) { value = value.instance; } } - value = value || $element[retrievalMethod]('$' + require + 'Controller'); + value = value || $searchElement[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); @@ -16283,11 +16592,11 @@ transcludeControllers = elementControllers; } if (!futureParentElement) { futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; } - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement); + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } } } function markDirectivesAsIsolate(directives) { @@ -16424,15 +16733,15 @@ if (origAsyncDirective.replace) { if (jqLiteIsTextNode(content)) { $template = []; } else { - $template = jqLite(wrapTemplate(templateNamespace, trim(content))); + $template = removeComments(wrapTemplate(templateNamespace, trim(content))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", origAsyncDirective.name, templateUrl); } @@ -16467,10 +16776,12 @@ beforeTemplateLinkNode = linkQueue.shift(), linkRootElement = linkQueue.shift(), boundTranscludeFn = linkQueue.shift(), linkNode = $compileNode[0]; + if (scope.$$destroyed) continue; + if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { var oldClasses = beforeTemplateLinkNode.className; if (!(previousCompileContext.hasElementTranscludeDirective && origAsyncDirective.replace)) { @@ -16493,10 +16804,11 @@ linkQueue = null; }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { var childBoundTranscludeFn = boundTranscludeFn; + if (scope.$$destroyed) return; if (linkQueue) { linkQueue.push(scope); linkQueue.push(node); linkQueue.push(rootElement); linkQueue.push(childBoundTranscludeFn); @@ -16609,10 +16921,15 @@ throw $compileMinErr('nodomevents', "Interpolations for HTML DOM event attributes are disallowed. Please use the " + "ng- versions (such as ng-click instead of onclick) instead."); } + // If the attribute was removed, then we are done + if (!attr[name]) { + return; + } + // we need to interpolate again, in case the attribute value has been updated // (e.g. by another directive's compile function) interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name), ALL_OR_NOTHING_ATTRS[name] || allOrNothing); @@ -16836,10 +17153,27 @@ values += (values.length > 0 ? ' ' : '') + token; } return values; } +function removeComments(jqNodes) { + jqNodes = jqLite(jqNodes); + var i = jqNodes.length; + + if (i <= 1) { + return jqNodes; + } + + while (i--) { + var node = jqNodes[i]; + if (node.nodeType === NODE_TYPE_COMMENT) { + splice.call(jqNodes, i, 1); + } + } + return jqNodes; +} + /** * @ngdoc provider * @name $controllerProvider * @description * The {@link ng.$controller $controller service} is used by Angular to create new @@ -17139,11 +17473,12 @@ * */ function $HttpProvider() { var JSON_START = /^\s*(\[|\{[^\{])/, JSON_END = /[\}\]]\s*$/, PROTECTION_PREFIX = /^\)\]\}',?\n/, - CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; + APPLICATION_JSON = 'application/json', + CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; /** * @ngdoc property * @name $httpProvider#defaults * @description @@ -17164,16 +17499,19 @@ * - **`defaults.headers.put`** * - **`defaults.headers.patch`** **/ var defaults = this.defaults = { // transform incoming response data - transformResponse: [function(data) { + transformResponse: [function defaultHttpResponseTransform(data, headers) { if (isString(data)) { // strip json vulnerability protection prefix data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) + var contentType = headers('Content-Type'); + if ((contentType && contentType.indexOf(APPLICATION_JSON) === 0) || + (JSON_START.test(data) && JSON_END.test(data))) { data = fromJson(data); + } } return data; }], // transform outgoing request data @@ -18127,22 +18465,12 @@ return url; } }]; } -function createXhr(method) { - //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest - //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest - //if it is available - if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) || - !window.XMLHttpRequest)) { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } else if (window.XMLHttpRequest) { - return new window.XMLHttpRequest(); - } - - throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); +function createXhr() { + return new window.XMLHttpRequest(); } /** * @ngdoc service * @name $httpBackend @@ -18164,15 +18492,12 @@ return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); }]; } function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { - var ABORTED = -1; - // TODO(vojta): fix the signature return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - var status; $browser.$$incOutstandingRequestCount(); url = url || $browser.url(); if (lowercase(method) == 'jsonp') { var callbackId = '_' + (callbacks.counter++).toString(36); @@ -18186,57 +18511,52 @@ completeRequest(callback, status, callbacks[callbackId].data, "", text); callbacks[callbackId] = noop; }); } else { - var xhr = createXhr(method); + var xhr = createXhr(); xhr.open(method, url, true); forEach(headers, function(value, key) { if (isDefined(value)) { xhr.setRequestHeader(key, value); } }); - // In IE6 and 7, this might be called synchronously when xhr.send below is called and the - // response is in the cache. the promise api will ensure that to the app code the api is - // always async - xhr.onreadystatechange = function() { - // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by - // xhrs that are resolved while the app is in the background (see #5426). - // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before - // continuing - // - // 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, - statusText = ''; + xhr.onload = function requestLoaded() { + var statusText = xhr.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) + var response = ('response' in xhr) ? xhr.response : xhr.responseText; - // 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; - } + // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) + var status = xhr.status === 1223 ? 204 : xhr.status; - // 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, - statusText); + // fix status code when it is 0 (0 status is undocumented). + // Occurs when accessing file resources or on Android 4.1 stock browser + // while retrieving files from application cache. + if (status === 0) { + status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; } + + completeRequest(callback, + status, + response, + xhr.getAllResponseHeaders(), + statusText); }; + var requestError = function () { + // The response is always empty + // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error + completeRequest(callback, -1, null, null, ''); + }; + + xhr.onerror = requestError; + xhr.onabort = requestError; + if (withCredentials) { xhr.withCredentials = true; } if (responseType) { @@ -18265,31 +18585,19 @@ timeout.then(timeoutRequest); } function timeoutRequest() { - status = ABORTED; jsonpDone && jsonpDone(); xhr && xhr.abort(); } function completeRequest(callback, status, response, headersString, statusText) { // cancel timeout and subsequent timeout promise resolution timeoutId && $browserDefer.cancel(timeoutId); jsonpDone = xhr = null; - // fix status code when it is 0 (0 status is undocumented). - // Occurs when accessing file resources or on Android 4.1 stock browser - // while retrieving files from application cache. - if (status === 0) { - status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; - } - - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status === 1223 ? 204 : status; - statusText = statusText || ''; - callback(status, response, headersString, statusText); $browser.$$completeOutstandingRequest(noop); } }; @@ -19237,22 +19545,20 @@ }; } -LocationHashbangInHtml5Url.prototype = - LocationHashbangUrl.prototype = - LocationHtml5Url.prototype = { +var locationPrototype = { /** * Are we in html5 mode? * @private */ $$html5: false, /** - * Has any change been replacing ? + * Has any change been replacing? * @private */ $$replace: false, /** @@ -19350,11 +19656,11 @@ * * @param {(string|number)=} path New path * @return {string} path */ path: locationGetterSetter('$$path', function(path) { - path = path ? path.toString() : ''; + path = path !== null ? path.toString() : ''; return path.charAt(0) == '/' ? path : '/' + path; }), /** * @ngdoc method @@ -19447,11 +19753,11 @@ * * @param {(string|number)=} hash New hash fragment * @return {string} hash */ hash: locationGetterSetter('$$hash', function(hash) { - return hash ? hash.toString() : ''; + return hash !== null ? hash.toString() : ''; }), /** * @ngdoc method * @name $location#replace @@ -19464,10 +19770,50 @@ this.$$replace = true; return this; } }; +forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) { + Location.prototype = Object.create(locationPrototype); + + /** + * @ngdoc method + * @name $location#state + * + * @description + * This method is getter / setter. + * + * Return the history state object when called without any parameter. + * + * Change the history state object when called with one parameter and return `$location`. + * The state object is later passed to `pushState` or `replaceState`. + * + * NOTE: This method is supported only in HTML5 mode and only in browsers supporting + * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support + * older browsers (like IE9 or Android < 4.0), don't use this method. + * + * @param {object=} state State object for pushState or replaceState + * @return {object} state + */ + Location.prototype.state = function(state) { + if (!arguments.length) + return this.$$state; + + if (Location !== LocationHtml5Url || !this.$$html5) { + throw $locationMinErr('nostate', 'History API state support is available only ' + + 'in HTML5 mode and only in browsers supporting HTML5 History API'); + } + // The user might modify `stateObject` after invoking `$location.state(stateObject)` + // but we're changing the $$state reference to $browser.state() during the $digest + // so the modification window is narrow. + this.$$state = isUndefined(state) ? null : state; + + return this; + }; +}); + + function locationGetter(property) { return function() { return this[property]; }; } @@ -19520,11 +19866,12 @@ */ function $LocationProvider(){ var hashPrefix = '', html5Mode = { enabled: false, - requireBase: true + requireBase: true, + rewriteLinks: true }; /** * @ngdoc method * @name $locationProvider#hashPrefix @@ -19544,33 +19891,42 @@ /** * @ngdoc method * @name $locationProvider#html5Mode * @description * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. - * If object, sets `enabled` and `requireBase` to respective values. - * - **enabled** – `{boolean}` – Sets `html5Mode.enabled`. If true, will rely on - * `history.pushState` to change urls where supported. Will fall back to hash-prefixed paths - * in browsers that do not support `pushState`. - * - **requireBase** - `{boolean}` - Sets `html5Mode.requireBase` (default: `true`). When - * html5Mode is enabled, specifies whether or not a <base> tag is required to be present. If - * `enabled` and `requireBase` are true, and a base tag is not present, an error will be - * thrown when `$location` is injected. See the - * {@link guide/$location $location guide for more information} + * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported + * properties: + * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to + * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not + * support `pushState`. + * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies + * whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are + * true, and a base tag is not present, an error will be thrown when `$location` is injected. + * See the {@link guide/$location $location guide for more information} + * - **rewriteLinks** - `{boolean}` - (default: `false`) When html5Mode is enabled, disables + * url rewriting for relative linksTurns off url rewriting for relative links. * * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { if (isBoolean(mode)) { html5Mode.enabled = mode; return this; } else if (isObject(mode)) { - html5Mode.enabled = isBoolean(mode.enabled) ? - mode.enabled : - html5Mode.enabled; - html5Mode.requireBase = isBoolean(mode.requireBase) ? - mode.requireBase : - html5Mode.requireBase; + + if (isBoolean(mode.enabled)) { + html5Mode.enabled = mode.enabled; + } + + if (isBoolean(mode.requireBase)) { + html5Mode.requireBase = mode.requireBase; + } + + if (isBoolean(mode.rewriteLinks)) { + html5Mode.rewriteLinks = mode.rewriteLinks; + } + return this; } else { return html5Mode; } }; @@ -19578,30 +19934,42 @@ /** * @ngdoc event * @name $location#$locationChangeStart * @eventType broadcast on root scope * @description - * Broadcasted before a URL will change. This change can be prevented by calling + * Broadcasted before a URL will change. + * + * This change can be prevented by calling * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more * details about event object. Upon successful change * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. + * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ /** * @ngdoc event * @name $location#$locationChangeSuccess * @eventType broadcast on root scope * @description * Broadcasted after a URL was changed. * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. + * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', function( $rootScope, $browser, $sniffer, $rootElement) { var $location, @@ -19622,17 +19990,38 @@ LocationMode = LocationHashbangUrl; } $location = new LocationMode(appBase, '#' + hashPrefix); $location.$$parseLinkUrl(initialUrl, initialUrl); + $location.$$state = $browser.state(); + var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; + function setBrowserUrlWithFallback(url, replace, state) { + var oldUrl = $location.url(); + var oldState = $location.$$state; + try { + $browser.url(url, replace, state); + + // Make sure $location.state() returns referentially identical (not just deeply equal) + // state object; this makes possible quick checking if the state changed in the digest + // loop. Checking deep equality would be too expensive. + $location.$$state = $browser.state(); + } catch (e) { + // Restore old values if pushState fails + $location.url(oldUrl); + $location.$$state = oldState; + + throw e; + } + } + $rootElement.on('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (event.ctrlKey || event.metaKey || event.which == 2) return; + if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.which == 2) return; var elm = jqLite(event.target); // traverse the DOM up to find first A tag while (nodeName_(elm[0]) !== 'a') { @@ -19654,10 +20043,13 @@ // Ignore when url is started with javascript: or mailto: if (IGNORE_URI_REGEXP.test(absHref)) return; if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { if ($location.$$parseLinkUrl(absHref, relHref)) { + // We do a preventDefault for all urls that are part of the angular application, + // in html5mode and also without, so that we are able to abort navigation without + // getting double entries in the location history. event.preventDefault(); // update location manually if ($location.absUrl() != $browser.url()) { $rootScope.$apply(); // hack to work around FF6 bug 684208 when scenario runner clicks on links @@ -19671,56 +20063,67 @@ // rewrite hashbang url <> html5 url if ($location.absUrl() != initialUrl) { $browser.url($location.absUrl(), true); } + var initializing = true; + // update $location when $browser url changes - $browser.onUrlChange(function(newUrl) { - if ($location.absUrl() != newUrl) { - $rootScope.$evalAsync(function() { - var oldUrl = $location.absUrl(); + $browser.onUrlChange(function(newUrl, newState) { + $rootScope.$evalAsync(function() { + var oldUrl = $location.absUrl(); + var oldState = $location.$$state; - $location.$$parse(newUrl); - if ($rootScope.$broadcast('$locationChangeStart', newUrl, - oldUrl).defaultPrevented) { - $location.$$parse(oldUrl); - $browser.url(oldUrl); - } else { - afterLocationChange(oldUrl); - } - }); - if (!$rootScope.$$phase) $rootScope.$digest(); - } + $location.$$parse(newUrl); + $location.$$state = newState; + if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + newState, oldState).defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + setBrowserUrlWithFallback(oldUrl, false, oldState); + } else { + initializing = false; + afterLocationChange(oldUrl, oldState); + } + }); + if (!$rootScope.$$phase) $rootScope.$digest(); }); // update browser - var changeCounter = 0; $rootScope.$watch(function $locationWatch() { var oldUrl = $browser.url(); + var oldState = $browser.state(); var currentReplace = $location.$$replace; - if (!changeCounter || oldUrl != $location.absUrl()) { - changeCounter++; + if (initializing || oldUrl !== $location.absUrl() || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state)) { + initializing = false; + $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). - defaultPrevented) { + if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl, + $location.$$state, oldState).defaultPrevented) { $location.$$parse(oldUrl); + $location.$$state = oldState; } else { - $browser.url($location.absUrl(), currentReplace); - afterLocationChange(oldUrl); + setBrowserUrlWithFallback($location.absUrl(), currentReplace, + oldState === $location.$$state ? null : $location.$$state); + afterLocationChange(oldUrl, oldState); } }); } + $location.$$replace = false; - return changeCounter; + // we don't need to return anything because $evalAsync will make the digest loop dirty when + // there is a change }); return $location; - function afterLocationChange(oldUrl) { - $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); + function afterLocationChange(oldUrl, oldState) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, + $location.$$state, oldState); } }]; } /** @@ -19975,10 +20378,15 @@ }, function(constantGetter, name) { constantGetter.constant = constantGetter.literal = constantGetter.sharedGetter = true; 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 */ '+':function(self, locals, a,b){ a=a(self, locals); b=b(self, locals); @@ -21838,18 +22246,15 @@ function Scope() { this.$id = nextUid(); 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 = []; } /** * @ngdoc property * @name $rootScope.Scope#$id @@ -21894,22 +22299,27 @@ * @param {boolean} isolate If true, then the scope does not prototypically inherit from the * parent scope. The scope is isolated, as it can not see parent scope properties. * 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. if (!this.$$ChildScope) { this.$$ChildScope = function ChildScope() { @@ -21922,20 +22332,31 @@ }; this.$$ChildScope.prototype = this; } 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; + } }, /** * @ngdoc method * @name $rootScope.Scope#$watch @@ -22407,12 +22828,10 @@ * */ $digest: function() { var watch, value, last, watchers, - asyncQueue = this.$$asyncQueue, - postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], logIdx, logMsg, asyncTask; @@ -22573,29 +22992,25 @@ if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; 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. // // see: // - https://code.google.com/p/v8/issues/detail?id=2073#c26 // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 // - 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; }, /** * @ngdoc method * @name $rootScope.Scope#$eval @@ -22658,23 +23073,23 @@ * */ $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); }, /** * @ngdoc method * @name $rootScope.Scope#$apply @@ -22754,11 +23169,11 @@ * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with current `scope` parameter. */ $applyAsync: function(expr) { var scope = this; - expr && $rootScope.$$applyAsyncQueue.push($applyAsyncExpression); + expr && applyAsyncQueue.push($applyAsyncExpression); scheduleApplyAsync(); function $applyAsyncExpression() { scope.$eval(expr); } @@ -22963,10 +23378,15 @@ } }; 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; function beginPhase(phase) { if ($rootScope.$$phase) { @@ -22996,14 +23416,13 @@ * because it's unique we can easily tell it apart from other values */ function initWatchVal() {} function flushApplyAsync() { - var queue = $rootScope.$$applyAsyncQueue; - while (queue.length) { + while (applyAsyncQueue.length) { try { - queue.shift()(); + applyAsyncQueue.shift()(); } catch(e) { $exceptionHandler(e); } } applyAsyncId = null; @@ -23078,16 +23497,13 @@ this.$get = function() { return function sanitizeUri(uri, isImage) { var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; var normalizedVal; - // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. - if (!msie || msie >= 8 ) { - normalizedVal = urlResolve(uri).href; - if (normalizedVal !== '' && !normalizedVal.match(regex)) { - return 'unsafe:'+normalizedVal; - } + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:'+normalizedVal; } return uri; }; }; } @@ -24155,11 +24571,10 @@ * @name $sniffer * @requires $window * @requires $document * * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? * @property {boolean} transitions Does the browser support CSS transition events ? * @property {boolean} animations Does the browser support CSS animation events ? * * @description * This is very simple implementation of testing browser's features. @@ -24212,13 +24627,10 @@ // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined // jshint -W018 history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), // jshint +W018 - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!documentMode || documentMode > 7), hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. if (event == 'input' && msie == 9) return false; @@ -25684,11 +26096,11 @@ * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically * for strings and numerically for numbers. Note: if you notice numbers are not being sorted * correctly, make sure they are actually being saved as numbers and not strings. * * @param {Array} array The array to sort. - * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be + * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be * used by the comparator to determine the order of elements. * * Can be one of: * * - `function`: Getter function. The result of this function will be sorted using the @@ -25697,14 +26109,17 @@ * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by * 3 first characters of a property called `name`). The result of a constant expression * is interpreted as a property name to be used in comparisons (for example `"special name"` * to sort object by the value of their `special name` property). An expression can be * optionally prefixed with `+` or `-` to control ascending or descending sort order - * (for example, `+name` or `-name`). + * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array + * element itself is used to compare where sorting. * - `Array`: An array of function or string predicates. The first predicate in the array * is used for sorting, but when two items are equivalent, the next predicate is used. * + * If the predicate is missing or empty then it defaults to `'+'`. + * * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * @example <example module="orderByExample"> @@ -25789,19 +26204,25 @@ */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse){ return function(array, sortPredicate, reverseOrder) { if (!(isArrayLike(array))) return array; - if (!sortPredicate) return array; sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; + if (sortPredicate.length === 0) { sortPredicate = ['+']; } sortPredicate = sortPredicate.map(function(predicate){ var descending = false, get = predicate || identity; if (isString(predicate)) { if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { descending = predicate.charAt(0) == '-'; predicate = predicate.substring(1); } + if ( predicate === '' ) { + // Effectively no predicate was passed so we compare identity + return reverseComparator(function(a,b) { + return compare(a, b); + }, descending); + } get = $parse(predicate); if (get.constant) { var key = get(); return reverseComparator(function(a,b) { return compare(a[key], b[key]); @@ -25873,26 +26294,10 @@ * `<a href="" ng-click="list.addItem()">Add Item</a>` */ var htmlAnchorDirective = valueFn({ restrict: 'E', compile: function(element, attr) { - - if (msie <= 8) { - - // turn <a href ng-click="..">link</a> into a stylable link in IE - // but only if it doesn't have name attribute, in which case it's an anchor - if (!attr.href && !attr.name) { - attr.$set('href', ''); - } - - // add a comment node to anchors to workaround IE bug that causes element content to be reset - // to new attribute content if attribute is updated with value containing @ and element also - // contains value with @ - // see issue #1949 - element.append(document.createComment('IE fix')); - } - if (!attr.href && !attr.xlinkHref && !attr.name) { return function(scope, element) { // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? 'xlink:href' : 'href'; @@ -26336,15 +26741,13 @@ var nullFormCtrl = { $addControl: noop, $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, - $$setPending: noop, $setDirty: noop, $setPristine: noop, - $setSubmitted: noop, - $$clearControlValidity: noop + $setSubmitted: noop }, SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { control.$name = name; @@ -26405,13 +26808,10 @@ form.$invalid = false; form.$submitted = false; parentForm.$addControl(form); - // Setup initial state of the control - element.addClass(PRISTINE_CLASS); - /** * @ngdoc method * @name form.FormController#$rollbackViewValue * * @description @@ -26780,14 +27180,18 @@ return ['$timeout', function($timeout) { var formDirective = { 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) { - if (!attr.action) { + pre: function ngFormPreLink(scope, formElement, attr, controller) { + // if `action` attr is not present on the form, prevent the default action (submission) + if (!('action' in attr)) { // we can't use jq events because if a form is destroyed during submission the default // action is not prevented. see #1238 // // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler @@ -27937,32 +28341,41 @@ function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var 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()); } return parsedDate; } return undefined; }); 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 ''; }); if (isDefined(attr.min) || attr.ngMin) { @@ -27984,10 +28397,15 @@ attr.$observe('max', function(val) { maxVal = parseObservedDateValue(val); 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; } }; @@ -28298,14 +28716,16 @@ 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, $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); + } } } }; }]; @@ -28602,15 +29022,10 @@ }; 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 * * @description @@ -28899,18 +29314,21 @@ } this.$$parseAndValidate(); }; 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)) { // ctrl.$modelValue has not been touched yet... ctrl.$modelValue = ngModelGet(); @@ -29223,46 +29641,55 @@ var ngModelDirective = function() { return { 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(); + }); }); - }); - } + } + }; } }; }; @@ -29813,12 +30240,13 @@ set = context.set, unset = context.unset, 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) { createAndSet('$pending', validationErrorKey, options); } else { @@ -29964,15 +30392,13 @@ restrict: 'AC', compile: function ngBindCompile(templateElement) { $compile.$$addBindingClass(templateElement); return function ngBindLink(scope, element, attr) { $compile.$$addBindingInfo(element, attr.ngBind); + element = element[0]; scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); + element.textContent = value === undefined ? '' : value; }); }; } }; }]; @@ -30034,12 +30460,13 @@ compile: function ngBindTemplateCompile(templateElement) { $compile.$$addBindingClass(templateElement); return function ngBindTemplateLink(scope, element, attr) { var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); $compile.$$addBindingInfo(element, interpolateFn.expressions); + element = element[0]; attr.$observe('ngBindTemplate', function(value) { - element.text(value); + element.textContent = value === undefined ? '' : value; }); }; } }; }]; @@ -30052,11 +30479,14 @@ * @description * Creates a binding that will innerHTML the result of evaluating the `expression` into the current * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in - * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to + * core Angular). In order to use {@link ngSanitize} in your module's dependencies, you need to + * include "angular-sanitize.js" in your application. + * + * You may also bypass sanitization for values you know are safe. To do so, bind to * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. * * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you * will have an exception (instead of an exploit.) @@ -30814,12 +31244,130 @@ <html ng-app ng-csp> ... ... </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 // the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc /** @@ -31321,11 +31869,11 @@ * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. * * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope * is created when the element is restored. The scope created within `ngIf` inherits from * its parent scope using - * [prototypal inheritance](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance). + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance). * An important implication of this is if `ngModel` is used within `ngIf` to bind to * a javascript primitive defined in the parent scope. In this case any modifications made to the * variable within the child scope will override (hide) the value in the parent scope. * * Also, `ngIf` recreates elements using their compiled state. An example of this behavior @@ -31335,12 +31883,12 @@ * * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` * and `leave` effects. * * @animations - * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container - * leave - happens just before the ngIf contents are removed from the DOM + * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container + * leave - happens just before the `ngIf` contents are removed from the DOM * * @element ANY * @scope * @priority 600 * @param {expression} ngIf If the {@link guide/expression expression} is falsy then @@ -32476,10 +33024,12 @@ }; } }; }]; +var NG_HIDE_CLASS = 'ng-hide'; +var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; /** * @ngdoc directive * @name ngShow * * @description @@ -32637,11 +33187,15 @@ return { restrict: 'A', multiElement: true, link: function(scope, element, attr) { scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - $animate[value ? 'removeClass' : 'addClass'](element, 'ng-hide'); + // we're adding a temporary, animation-specific class for ng-hide since this way + // we can control when the element is actually displayed on screen without having + // to have a global/greedy CSS selector that breaks when other animations are run. + // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 + $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); }); } }; }]; @@ -32792,11 +33346,13 @@ return { restrict: 'A', multiElement: true, link: function(scope, element, attr) { scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - $animate[value ? 'addClass' : 'removeClass'](element, 'ng-hide'); + // The comment inside of the ngShowDirective explains why we add and + // remove a temporary class for the show/hide animation + $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); }); } }; }]; @@ -33214,10 +33770,16 @@ * of {@link ng.directive:ngRepeat ngRepeat} when you want the * `select` model to be bound to a non-string value. This is because an option element can only * be bound to string values at present. * </div> * + * <div class="alert alert-info"> + * **Note:** Using `select as` will bind the result of the `select as` expression to the model, but + * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources) + * or property name (for object data sources) of the value within the collection. + * </div> + * * @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=} required The control is considered valid only if value is entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -33248,12 +33810,30 @@ * element. If not specified, `select` expression will default to `value`. * * `group`: The result of this expression will be used to group options using the `<optgroup>` * DOM element. * * `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`). + * `value` variable (e.g. `value.propertyName`). With this the selection is preserved + * even when the options are recreated (e.g. reloaded from the server). + + * <div class="alert alert-info"> + * **Note:** Using `select as` together with `trackexpr` is not possible (and will throw). + * Reasoning: + * - Example: <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"> + * values: [{id: 1, label: 'aLabel', subItem: {name: 'aSubItem'}}, {id: 2, label: 'bLabel', subItem: {name: 'bSubItemß'}}], + * $scope.selected = {name: 'aSubItem'}; + * - track by is always applied to `value`, with purpose to preserve the selection, + * (to `item` in this case) + * - to calculate whether an item is selected we do the following: + * 1. apply `track by` to the values in the array, e.g. + * In the example: [1,2] + * 2. apply `track by` to the already selected value in `ngModel`: + * In the example: this is not possible, as `track by` refers to `item.id`, but the selected + * value from `ngModel` is `{name: aSubItem}`. * + * </div> + * * @example <example module="selectExample"> <file name="index.html"> <script> angular.module('selectExample', []) @@ -33505,22 +34085,33 @@ optionsExp, startingTag(selectElement)); } var displayFn = $parse(match[2] || match[1]), valueName = match[4] || match[6], + selectAs = / as /.test(match[0]) && match[1], + selectAsFn = selectAs ? $parse(selectAs) : null, keyName = match[5], groupByFn = $parse(match[3] || ''), valueFn = $parse(match[2] ? match[1] : valueName), valuesFn = $parse(match[7]), track = match[8], trackFn = track ? $parse(match[8]) : null, // This is an array of array of existing option groups in DOM. // We try to reuse these if possible // - optionGroupsCache[0] is the options with no option group // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element - optionGroupsCache = [[{element: selectElement, label:''}]]; + optionGroupsCache = [[{element: selectElement, label:''}]], + //re-usable object to represent option's locals + locals = {}; + if (trackFn && selectAsFn) { + throw ngOptionsMinErr('trkslct', + "Comprehension expression cannot contain both selectAs '{0}' " + + "and trackBy '{1}' expressions.", + selectAs, track); + } + if (nullOption) { // compile the element since there might be bindings in it $compile(nullOption)(scope); // remove the class, which is added automatically because we recompile the element and it @@ -33533,187 +34124,180 @@ } // clear contents, we'll add what's needed based on the model selectElement.empty(); - selectElement.on('change', function() { + selectElement.on('change', selectionChanged); + + ctrl.$render = render; + + scope.$watchCollection(valuesFn, scheduleRendering); + scope.$watchCollection(getLabels, scheduleRendering); + + if (multiple) { + scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering); + } + + // ------------------------------------------------------------------ // + + function callExpression(exprFn, key, value) { + locals[valueName] = value; + if (keyName) locals[keyName] = key; + return exprFn(scope, locals); + } + + function selectionChanged() { scope.$apply(function() { var optionGroup, collection = valuesFn(scope) || [], - locals = {}, key, value, optionElement, index, groupIndex, length, groupLength, trackIndex; - + var viewValue; if (multiple) { - value = []; - for (groupIndex = 0, groupLength = optionGroupsCache.length; - groupIndex < groupLength; - groupIndex++) { - // list of options for that group. (first item has the parent) - optionGroup = optionGroupsCache[groupIndex]; - - for(index = 1, length = optionGroup.length; index < length; index++) { - if ((optionElement = optionGroup[index].element)[0].selected) { - key = optionElement.val(); - if (keyName) locals[keyName] = key; - if (trackFn) { - for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { - locals[valueName] = collection[trackIndex]; - if (trackFn(scope, locals) == key) break; - } - } else { - locals[valueName] = collection[key]; - } - value.push(valueFn(scope, locals)); - } - } - } + viewValue = []; + forEach(selectElement.val(), function(selectedKey) { + viewValue.push(getViewValue(selectedKey, collection[selectedKey])); + }); } else { - key = selectElement.val(); - if (key == '?') { - value = undefined; - } else if (key === ''){ - value = null; - } else { - if (trackFn) { - for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { - locals[valueName] = collection[trackIndex]; - if (trackFn(scope, locals) == key) { - value = valueFn(scope, locals); - break; - } - } - } else { - locals[valueName] = collection[key]; - if (keyName) locals[keyName] = key; - value = valueFn(scope, locals); - } - } + var selectedKey = selectElement.val(); + viewValue = getViewValue(selectedKey, collection[selectedKey]); } - ctrl.$setViewValue(value); + ctrl.$setViewValue(viewValue); render(); }); - }); + } - ctrl.$render = render; + function getViewValue(key, value) { + if (key === '?') { + return undefined; + } else if (key === '') { + return null; + } else { + var viewValueFn = selectAsFn ? selectAsFn : valueFn; + return callExpression(viewValueFn, key, value); + } + } - scope.$watchCollection(valuesFn, scheduleRendering); - scope.$watchCollection(function () { - var locals = {}, - values = valuesFn(scope); - if (values) { - var toDisplay = new Array(values.length); + function getLabels() { + var values = valuesFn(scope); + var toDisplay; + if (values && isArray(values)) { + toDisplay = new Array(values.length); for (var i = 0, ii = values.length; i < ii; i++) { - locals[valueName] = values[i]; - toDisplay[i] = displayFn(scope, locals); + toDisplay[i] = callExpression(displayFn, i, values[i]); } return toDisplay; + } else if (values) { + // TODO: Add a test for this case + toDisplay = {}; + for (var prop in values) { + if (values.hasOwnProperty(prop)) { + toDisplay[prop] = callExpression(displayFn, prop, values[prop]); + } + } } - }, scheduleRendering); - - if (multiple) { - scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering); + return toDisplay; } - - function getSelectedSet() { - var selectedSet = false; + function createIsSelectedFn(viewValue) { + var selectedSet; if (multiple) { - var modelValue = ctrl.$modelValue; - if (trackFn && isArray(modelValue)) { + if (!selectAs && trackFn && isArray(viewValue)) { + selectedSet = new HashMap([]); - var locals = {}; - for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { - locals[valueName] = modelValue[trackIndex]; - selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); + for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) { + // tracking by key + selectedSet.put(callExpression(trackFn, null, viewValue[trackIndex]), true); } } else { - selectedSet = new HashMap(modelValue); + selectedSet = new HashMap(viewValue); } + } else if (!selectAsFn && trackFn) { + viewValue = callExpression(trackFn, null, viewValue); } - return selectedSet; + return function isSelected(key, value) { + var compareValueFn; + if (selectAsFn) { + compareValueFn = selectAsFn; + } else if (trackFn) { + compareValueFn = trackFn; + } else { + compareValueFn = valueFn; + } + + if (multiple) { + return isDefined(selectedSet.remove(callExpression(compareValueFn, key, value))); + } else { + return viewValue == callExpression(compareValueFn, key, value); + } + }; } - function scheduleRendering() { if (!renderScheduled) { scope.$$postDigest(render); renderScheduled = true; } } - function render() { renderScheduled = false; - // Temporary location for the option groups before we render them + // Temporary location for the option groups before we render them var optionGroups = {'':[]}, optionGroupNames = [''], optionGroupName, optionGroup, option, existingParent, existingOptions, existingOption, - modelValue = ctrl.$modelValue, + viewValue = ctrl.$viewValue, values = valuesFn(scope) || [], keys = keyName ? sortedKeys(values) : values, key, + value, groupLength, length, groupIndex, index, - locals = {}, selected, - selectedSet = getSelectedSet(), + isSelected = createIsSelectedFn(viewValue), + anySelected = false, lastElement, element, label; - // We now build up the list of options we need (we merge later) for (index = 0; length = keys.length, index < length; index++) { - key = index; if (keyName) { key = keys[index]; if ( key.charAt(0) === '$' ) continue; - locals[keyName] = key; } + value = values[key]; - locals[valueName] = values[key]; - - optionGroupName = groupByFn(scope, locals) || ''; + optionGroupName = callExpression(groupByFn, key, value) || ''; if (!(optionGroup = optionGroups[optionGroupName])) { optionGroup = optionGroups[optionGroupName] = []; optionGroupNames.push(optionGroupName); } - if (multiple) { - selected = isDefined( - selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) - ); - } else { - if (trackFn) { - var modelCast = {}; - modelCast[valueName] = modelValue; - selected = trackFn(scope, modelCast) === trackFn(scope, locals); - } else { - selected = modelValue === valueFn(scope, locals); - } - selectedSet = selectedSet || selected; // see if at least one item is selected - } - label = displayFn(scope, locals); // what will be seen by the user + selected = isSelected(key, value); + anySelected = anySelected || selected; + + label = callExpression(displayFn, key, value); // what will be seen by the user + // doing displayFn(scope, locals) || '' overwrites zero values label = isDefined(label) ? label : ''; optionGroup.push({ // either the index into array or key from object - id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), + id: (keyName ? keys[index] : index), label: label, selected: selected // determine if we should be selected }); } if (!multiple) { - if (nullOption || modelValue === null) { + if (nullOption || viewValue === null) { // insert null option if we have a placeholder, or the model is null - optionGroups[''].unshift({id:'', label:'', selected:!selectedSet}); - } else if (!selectedSet) { + optionGroups[''].unshift({id:'', label:'', selected:!anySelected}); + } else if (!anySelected) { // option could not be found, we have to insert the undefined item optionGroups[''].unshift({id:'?', label:'', selected:true}); } } @@ -33790,10 +34374,11 @@ element: element, label: option.label, id: option.id, selected: option.selected }); + selectCtrl.addOption(option.label, element); if (lastElement) { lastElement.after(element); } else { existingParent.element.append(element); } @@ -33801,11 +34386,13 @@ } } // 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 while(optionGroupsCache.length > groupIndex) { optionGroupsCache.pop()[0].element.remove(); @@ -33837,15 +34424,11 @@ var selectCtrlName = '$selectController', parent = element.parent(), selectCtrl = parent.data(selectCtrlName) || parent.parent().data(selectCtrlName); // in case we are in optgroup - if (selectCtrl && selectCtrl.databound) { - // For some reason Opera defaults to true and if not overridden this messes up the repeater. - // We don't want the view to drive the initialization of the model anyway. - element.prop('selected', false); - } else { + if (!selectCtrl || !selectCtrl.databound) { selectCtrl = nullSelectCtrl; } if (interpolateFn) { scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { @@ -34195,11 +34778,15 @@ }); return result; }; (function() { - var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10); + /** + * documentMode is an IE-only property + * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx + */ + var msie = document.documentMode; /** * Triggers a browser event. Attempts to choose the right event if one is * not specified. * @@ -34246,101 +34833,75 @@ keys = keys || []; function pressed(key) { return keys.indexOf(key) !== -1; } - if (msie < 9) { - if (inputType == 'radio' || inputType == 'checkbox') { - element.checked = !element.checked; + var evnt; + if(/transitionend/.test(eventType)) { + if(window.WebKitTransitionEvent) { + evnt = new WebKitTransitionEvent(eventType, eventData); + evnt.initEvent(eventType, false, true); } - - // WTF!!! Error: Unspecified error. - // Don't know why, but some elements when detached seem to be in inconsistent state and - // calling .fireEvent() on them will result in very unhelpful error (Error: Unspecified error) - // forcing the browser to compute the element position (by reading its CSS) - // puts the element in consistent state. - element.style.posLeft; - - // TODO(vojta): create event objects with pressed keys to get it working on IE<9 - var ret = element.fireEvent('on' + eventType); - if (inputType == 'submit') { - while(element) { - if (element.nodeName.toLowerCase() == 'form') { - element.fireEvent('onsubmit'); - break; - } - element = element.parentNode; + else { + try { + evnt = new TransitionEvent(eventType, eventData); } - } - return ret; - } else { - var evnt; - if(/transitionend/.test(eventType)) { - if(window.WebKitTransitionEvent) { - evnt = new WebKitTransitionEvent(eventType, eventData); - evnt.initEvent(eventType, false, true); + catch(e) { + evnt = document.createEvent('TransitionEvent'); + evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0); } - else { - try { - evnt = new TransitionEvent(eventType, eventData); - } - catch(e) { - evnt = document.createEvent('TransitionEvent'); - evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0); - } - } } - else if(/animationend/.test(eventType)) { - if(window.WebKitAnimationEvent) { - evnt = new WebKitAnimationEvent(eventType, eventData); - evnt.initEvent(eventType, false, true); + } + else if(/animationend/.test(eventType)) { + if(window.WebKitAnimationEvent) { + evnt = new WebKitAnimationEvent(eventType, eventData); + evnt.initEvent(eventType, false, true); + } + else { + try { + evnt = new AnimationEvent(eventType, eventData); } - else { - try { - evnt = new AnimationEvent(eventType, eventData); - } - catch(e) { - evnt = document.createEvent('AnimationEvent'); - evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0); - } + catch(e) { + evnt = document.createEvent('AnimationEvent'); + evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0); } } - else { - evnt = document.createEvent('MouseEvents'); - x = x || 0; - y = y || 0; - evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), - pressed('alt'), pressed('shift'), pressed('meta'), 0, element); - } + } + else { + evnt = document.createEvent('MouseEvents'); + x = x || 0; + y = y || 0; + evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), + pressed('alt'), pressed('shift'), pressed('meta'), 0, element); + } - /* we're unable to change the timeStamp value directly so this - * is only here to allow for testing where the timeStamp value is - * read */ - evnt.$manualTimeStamp = eventData.timeStamp; + /* we're unable to change the timeStamp value directly so this + * is only here to allow for testing where the timeStamp value is + * read */ + evnt.$manualTimeStamp = eventData.timeStamp; - if(!evnt) return; + if(!evnt) return; - var originalPreventDefault = evnt.preventDefault, - appWindow = element.ownerDocument.defaultView, - fakeProcessDefault = true, - finalProcessDefault, - angular = appWindow.angular || {}; + var originalPreventDefault = evnt.preventDefault, + appWindow = element.ownerDocument.defaultView, + fakeProcessDefault = true, + finalProcessDefault, + angular = appWindow.angular || {}; - // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208 - angular['ff-684208-preventDefault'] = false; - evnt.preventDefault = function() { - fakeProcessDefault = false; - return originalPreventDefault.apply(evnt, arguments); - }; + // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208 + angular['ff-684208-preventDefault'] = false; + evnt.preventDefault = function() { + fakeProcessDefault = false; + return originalPreventDefault.apply(evnt, arguments); + }; - element.dispatchEvent(evnt); - finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault); + element.dispatchEvent(evnt); + finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault); - delete angular['ff-684208-preventDefault']; + delete angular['ff-684208-preventDefault']; - return finalProcessDefault; - } + return finalProcessDefault; }; }()); /** * Represents the application currently being tested and abstracts usage @@ -36077,7 +36638,7 @@ }); } })(window, document); -!window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";\n\n[ng\\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],\n.ng-cloak, .x-ng-cloak,\n.ng-hide:not(.ng-animate) {\n display: none !important;\n}\n\nng\\:form {\n display: block;\n}\n</style>'); +!window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";\n\n[ng\\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],\n.ng-cloak, .x-ng-cloak,\n.ng-hide:not(.ng-hide-animate) {\n display: none !important;\n}\n\nng\\:form {\n display: block;\n}\n</style>'); !window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";\n/* CSS Document */\n\n/** Structure */\nbody {\n font-family: Arial, sans-serif;\n margin: 0;\n font-size: 14px;\n}\n\n#system-error {\n font-size: 1.5em;\n text-align: center;\n}\n\n#json, #xml {\n display: none;\n}\n\n#header {\n position: fixed;\n width: 100%;\n}\n\n#specs {\n padding-top: 50px;\n}\n\n#header .angular {\n font-family: Courier New, monospace;\n font-weight: bold;\n}\n\n#header h1 {\n font-weight: normal;\n float: left;\n font-size: 30px;\n line-height: 30px;\n margin: 0;\n padding: 10px 10px;\n height: 30px;\n}\n\n#application h2,\n#specs h2 {\n margin: 0;\n padding: 0.5em;\n font-size: 1.1em;\n}\n\n#status-legend {\n margin-top: 10px;\n margin-right: 10px;\n}\n\n#header,\n#application,\n.test-info,\n.test-actions li {\n overflow: hidden;\n}\n\n#application {\n margin: 10px;\n}\n\n#application iframe {\n width: 100%;\n height: 758px;\n}\n\n#application .popout {\n float: right;\n}\n\n#application iframe {\n border: none;\n}\n\n.tests li,\n.test-actions li,\n.test-it li,\n.test-it ol,\n.status-display {\n list-style-type: none;\n}\n\n.tests,\n.test-it ol,\n.status-display {\n margin: 0;\n padding: 0;\n}\n\n.test-info {\n margin-left: 1em;\n margin-top: 0.5em;\n border-radius: 8px 0 0 8px;\n -webkit-border-radius: 8px 0 0 8px;\n -moz-border-radius: 8px 0 0 8px;\n cursor: pointer;\n}\n\n.test-info:hover .test-name {\n text-decoration: underline;\n}\n\n.test-info .closed:before {\n content: \'\\25b8\\00A0\';\n}\n\n.test-info .open:before {\n content: \'\\25be\\00A0\';\n font-weight: bold;\n}\n\n.test-it ol {\n margin-left: 2.5em;\n}\n\n.status-display,\n.status-display li {\n float: right;\n}\n\n.status-display li {\n padding: 5px 10px;\n}\n\n.timer-result,\n.test-title {\n display: inline-block;\n margin: 0;\n padding: 4px;\n}\n\n.test-actions .test-title,\n.test-actions .test-result {\n display: table-cell;\n padding-left: 0.5em;\n padding-right: 0.5em;\n}\n\n.test-actions {\n display: table;\n}\n\n.test-actions li {\n display: table-row;\n}\n\n.timer-result {\n width: 4em;\n padding: 0 10px;\n text-align: right;\n font-family: monospace;\n}\n\n.test-it pre,\n.test-actions pre {\n clear: left;\n color: black;\n margin-left: 6em;\n}\n\n.test-describe {\n padding-bottom: 0.5em;\n}\n\n.test-describe .test-describe {\n margin: 5px 5px 10px 2em;\n}\n\n.test-actions .status-pending .test-title:before {\n content: \'\\00bb\\00A0\';\n}\n\n.scrollpane {\n max-height: 20em;\n overflow: auto;\n}\n\n/** Colors */\n\n#header {\n background-color: #F2C200;\n}\n\n#specs h2 {\n border-top: 2px solid #BABAD1;\n}\n\n#specs h2,\n#application h2 {\n background-color: #efefef;\n}\n\n#application {\n border: 1px solid #BABAD1;\n}\n\n.test-describe .test-describe {\n border-left: 1px solid #BABAD1;\n border-right: 1px solid #BABAD1;\n border-bottom: 1px solid #BABAD1;\n}\n\n.status-display {\n border: 1px solid #777;\n}\n\n.status-display .status-pending,\n.status-pending .test-info {\n background-color: #F9EEBC;\n}\n\n.status-display .status-success,\n.status-success .test-info {\n background-color: #B1D7A1;\n}\n\n.status-display .status-failure,\n.status-failure .test-info {\n background-color: #FF8286;\n}\n\n.status-display .status-error,\n.status-error .test-info {\n background-color: black;\n color: white;\n}\n\n.test-actions .status-success .test-title {\n color: #30B30A;\n}\n\n.test-actions .status-failure .test-title {\n color: #DF0000;\n}\n\n.test-actions .status-error .test-title {\n color: black;\n}\n\n.test-actions .timer-result {\n color: #888;\n}\n</style>'); \ No newline at end of file