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