summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormanchandavishal <manchandavishal143@gmail.com>2021-05-08 00:17:46 +0530
committermanchandavishal <manchandavishal143@gmail.com>2021-05-10 19:34:56 +0530
commit030dabe17221db7b760c3cea40740a506369f97d (patch)
tree0e55db9ceb3d26e0659691fed6457eb38a25d763
parent8c1eb54d8358bd578639ae039aa2048a744c1839 (diff)
downloadxstatic-angular-030dabe17221db7b760c3cea40740a506369f97d.tar.gz
Update XStatic-Angular to 1.8.2
Related-Bug: #1927261 Change-Id: I8238e020df05825f6731499d027c0fd12cc2c00d
-rw-r--r--xstatic/pkg/angular/__init__.py4
-rw-r--r--xstatic/pkg/angular/data/angular-animate.js650
-rw-r--r--xstatic/pkg/angular/data/angular-aria.js131
-rw-r--r--xstatic/pkg/angular/data/angular-cookies.js117
-rw-r--r--xstatic/pkg/angular/data/angular-loader.js136
-rw-r--r--xstatic/pkg/angular/data/angular-message-format.js156
-rw-r--r--xstatic/pkg/angular/data/angular-messages.js226
-rw-r--r--xstatic/pkg/angular/data/angular-mocks.js1130
-rw-r--r--xstatic/pkg/angular/data/angular-parse-ext.js24
-rw-r--r--xstatic/pkg/angular/data/angular-resource.js586
-rw-r--r--xstatic/pkg/angular/data/angular-route.js416
-rw-r--r--xstatic/pkg/angular/data/angular-sanitize.js347
-rw-r--r--xstatic/pkg/angular/data/angular-scenario.js23771
-rw-r--r--xstatic/pkg/angular/data/angular-touch.js459
-rw-r--r--xstatic/pkg/angular/data/angular.js18362
-rw-r--r--xstatic/pkg/angular/data/errors.json2
-rw-r--r--xstatic/pkg/angular/data/version.json2
-rw-r--r--xstatic/pkg/angular/data/version.txt2
18 files changed, 14783 insertions, 31738 deletions
diff --git a/xstatic/pkg/angular/__init__.py b/xstatic/pkg/angular/__init__.py
index 8fb08de..612dfdf 100644
--- a/xstatic/pkg/angular/__init__.py
+++ b/xstatic/pkg/angular/__init__.py
@@ -11,9 +11,9 @@ NAME = __name__.split('.')[-1] # package name (e.g. 'foo' or 'foo_bar')
# please use a all-lowercase valid python
# package name
-VERSION = '1.5.8' # version of the packaged files, please use the upstream
+VERSION = '1.8.2' # version of the packaged files, please use the upstream
# version number
-BUILD = '0' # our package build number, so we can release new builds
+BUILD = '1' # our package build number, so we can release new builds
# with fixes for xstatic stuff.
PACKAGE_VERSION = VERSION + '.' + BUILD # version used for PyPi
diff --git a/xstatic/pkg/angular/data/angular-animate.js b/xstatic/pkg/angular/data/angular-animate.js
index b18b828..7bae3b2 100644
--- a/xstatic/pkg/angular/data/angular-animate.js
+++ b/xstatic/pkg/angular/data/angular-animate.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -29,7 +29,7 @@ var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMA
// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
// therefore there is no reason to test anymore for other vendor prefixes:
// http://caniuse.com/#search=transition
-if ((window.ontransitionend === void 0) && (window.onwebkittransitionend !== void 0)) {
+if ((window.ontransitionend === undefined) && (window.onwebkittransitionend !== undefined)) {
CSS_PREFIX = '-webkit-';
TRANSITION_PROP = 'WebkitTransition';
TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
@@ -38,7 +38,7 @@ if ((window.ontransitionend === void 0) && (window.onwebkittransitionend !== voi
TRANSITIONEND_EVENT = 'transitionend';
}
-if ((window.onanimationend === void 0) && (window.onwebkitanimationend !== void 0)) {
+if ((window.onanimationend === undefined) && (window.onwebkitanimationend !== undefined)) {
CSS_PREFIX = '-webkit-';
ANIMATION_PROP = 'WebkitAnimation';
ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
@@ -63,7 +63,7 @@ var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY;
var ngMinErr = angular.$$minErr('ng');
function assertArg(arg, name, reason) {
if (!arg) {
- throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
+ throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required'));
}
return arg;
}
@@ -139,7 +139,7 @@ function extractElementNode(element) {
if (!element[0]) return element;
for (var i = 0; i < element.length; i++) {
var elm = element[i];
- if (elm.nodeType == ELEMENT_NODE) {
+ if (elm.nodeType === ELEMENT_NODE) {
return elm;
}
}
@@ -306,7 +306,7 @@ function getDomNode(element) {
return (element instanceof jqLite) ? element[0] : element;
}
-function applyGeneratedPreparationClasses(element, event, options) {
+function applyGeneratedPreparationClasses($$jqLite, element, event, options) {
var classes = '';
if (event) {
classes = pendClasses(event, EVENT_CLASS_PREFIX, true);
@@ -334,15 +334,6 @@ function clearGeneratedClasses(element, options) {
}
}
-function blockTransitions(node, duration) {
- // we use a negative delay value since it performs blocking
- // yet it doesn't kill any existing transitions running on the
- // same element which makes this safe for class-based animations
- var value = duration ? '-' + duration + 's' : '';
- applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
- return [TRANSITION_DELAY_PROP, value];
-}
-
function blockKeyframeAnimations(node, applyBlock) {
var value = applyBlock ? 'paused' : '';
var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY;
@@ -423,8 +414,8 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) {
* of the children's parents are currently animating. By default, when an element has an active `enter`, `leave`, or `move`
* (structural) animation, child elements that also have an active structural animation are not animated.
*
- * Note that even if `ngAnimteChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation).
- *
+ * Note that even if `ngAnimateChildren` is set, no child animations will run when the parent element is removed
+ * from the DOM (`leave` animation).
*
* @param {string} ngAnimateChildren If the value is empty, `true` or `on`,
* then child animations are allowed. If the value is `false`, child animations are not allowed.
@@ -432,7 +423,7 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) {
* @example
* <example module="ngAnimateChildren" name="ngAnimateChildren" deps="angular-animate.js" animations="true">
<file name="index.html">
- <div ng-controller="mainController as main">
+ <div ng-controller="MainController as main">
<label>Show container? <input type="checkbox" ng-model="main.enterElement" /></label>
<label>Animate children? <input type="checkbox" ng-model="main.animateChildren" /></label>
<hr>
@@ -482,7 +473,7 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) {
</file>
<file name="script.js">
angular.module('ngAnimateChildren', ['ngAnimate'])
- .controller('mainController', function() {
+ .controller('MainController', function MainController() {
this.animateChildren = false;
this.enterElement = false;
});
@@ -526,11 +517,11 @@ var ANIMATE_TIMER_KEY = '$$animateCss';
* Note that only browsers that support CSS transitions and/or keyframe animations are capable of
* rendering animations triggered via `$animateCss` (bad news for IE9 and lower).
*
- * ## Usage
+ * ## General Use
* Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that
* is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however,
* any automatic control over cancelling animations and/or preventing animations from being run on
- * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to
+ * child elements will not be handled by AngularJS. For this to work as expected, please use `$animate` to
* trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger
* the CSS animation.
*
@@ -727,7 +718,6 @@ var ANIMATE_TIMER_KEY = '$$animateCss';
* * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
*/
var ONE_SECOND = 1000;
-var BASE_TEN = 10;
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
var CLOSING_TIME_BUFFER = 1.5;
@@ -789,7 +779,7 @@ function parseMaxTime(str) {
forEach(values, function(value) {
// it's always safe to consider only second values and omit `ms` values since
// getComputedStyle will always handle the conversion for us
- if (value.charAt(value.length - 1) == 's') {
+ if (value.charAt(value.length - 1) === 's') {
value = value.substring(0, value.length - 1);
}
value = parseFloat(value) || 0;
@@ -813,33 +803,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
return [style, value];
}
-function createLocalCacheLookup() {
- var cache = Object.create(null);
- return {
- flush: function() {
- cache = Object.create(null);
- },
-
- count: function(key) {
- var entry = cache[key];
- return entry ? entry.total : 0;
- },
-
- get: function(key) {
- var entry = cache[key];
- return entry && entry.value;
- },
-
- put: function(key, value) {
- if (!cache[key]) {
- cache[key] = { total: 1, value: value };
- } else {
- cache[key].total++;
- }
- }
- };
-}
-
// we do not reassign an already present style value since
// if we detect the style property value again we may be
// detecting styles that were added via the `from` styles.
@@ -857,27 +820,17 @@ function registerRestorableStyles(backup, node, properties) {
});
}
-var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
- var gcsLookup = createLocalCacheLookup();
- var gcsStaggerLookup = createLocalCacheLookup();
+var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) {
- this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
+ this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$animateCache',
'$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue',
- function($window, $$jqLite, $$AnimateRunner, $timeout,
+ function($window, $$jqLite, $$AnimateRunner, $timeout, $$animateCache,
$$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) {
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
- var parentCounter = 0;
- function gcsHashFn(node, extraClasses) {
- var KEY = "$$ngAnimateParentKey";
- var parentNode = node.parentNode;
- var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
- return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
- }
-
- function computeCachedCssStyles(node, className, cacheKey, properties) {
- var timings = gcsLookup.get(cacheKey);
+ function computeCachedCssStyles(node, className, cacheKey, allowNoDuration, properties) {
+ var timings = $$animateCache.get(cacheKey);
if (!timings) {
timings = computeCssStyles($window, node, properties);
@@ -885,21 +838,27 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
timings.animationIterationCount = 1;
}
}
+
+ // if a css animation has no duration we
+ // should mark that so that repeated addClass/removeClass calls are skipped
+ var hasDuration = allowNoDuration || (timings.transitionDuration > 0 || timings.animationDuration > 0);
// we keep putting this in multiple times even though the value and the cacheKey are the same
// because we're keeping an internal tally of how many duplicate animations are detected.
- gcsLookup.put(cacheKey, timings);
+ $$animateCache.put(cacheKey, timings, hasDuration);
+
return timings;
}
function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
var stagger;
+ var staggerCacheKey = 'stagger-' + cacheKey;
// if we have one or more existing matches of matching elements
// containing the same parent + CSS styles (which is how cacheKey works)
// then staggering is possible
- if (gcsLookup.count(cacheKey) > 0) {
- stagger = gcsStaggerLookup.get(cacheKey);
+ if ($$animateCache.count(cacheKey) > 0) {
+ stagger = $$animateCache.get(staggerCacheKey);
if (!stagger) {
var staggerClassName = pendClasses(className, '-stagger');
@@ -914,20 +873,18 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
$$jqLite.removeClass(node, staggerClassName);
- gcsStaggerLookup.put(cacheKey, stagger);
+ $$animateCache.put(staggerCacheKey, stagger, true);
}
}
return stagger || {};
}
- var cancelLastRAFRequest;
var rafWaitQueue = [];
function waitUntilQuiet(callback) {
rafWaitQueue.push(callback);
$$rAFScheduler.waitUntilQuiet(function() {
- gcsLookup.flush();
- gcsStaggerLookup.flush();
+ $$animateCache.flush();
// DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
// PLEASE EXAMINE THE `$$forceReflow` service to understand why.
@@ -942,8 +899,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
});
}
- function computeTimings(node, className, cacheKey) {
- var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
+ function computeTimings(node, className, cacheKey, allowNoDuration) {
+ var timings = computeCachedCssStyles(node, className, cacheKey, allowNoDuration, DETECT_CSS_PROPERTIES);
var aD = timings.animationDelay;
var tD = timings.transitionDelay;
timings.maxDelay = aD && tD
@@ -1030,7 +987,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
var fullClassName = classes + ' ' + preparationClasses;
- var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;
@@ -1043,7 +999,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
return closeAndReturnNoopAnimator();
}
- var cacheKey, stagger;
+ var stagger, cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
+ if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
+ preparationClasses = null;
+ return closeAndReturnNoopAnimator();
+ }
+
if (options.stagger > 0) {
var staggerVal = parseFloat(options.stagger);
stagger = {
@@ -1053,7 +1014,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
animationDuration: 0
};
} else {
- cacheKey = gcsHashFn(node, fullClassName);
stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
}
@@ -1087,7 +1047,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var itemIndex = stagger
? options.staggerIndex >= 0
? options.staggerIndex
- : gcsLookup.count(cacheKey)
+ : $$animateCache.count(cacheKey)
: 0;
var isFirst = itemIndex === 0;
@@ -1099,10 +1059,10 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
// that if there is no transition defined then nothing will happen and this will also allow
// other transitions to be stacked on top of each other without any chopping them out.
if (isFirst && !options.skipBlocking) {
- blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
+ helpers.blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
}
- var timings = computeTimings(node, fullClassName, cacheKey);
+ var timings = computeTimings(node, fullClassName, cacheKey, !isStructural);
var relativeDelay = timings.maxDelay;
maxDelay = Math.max(relativeDelay, 0);
maxDuration = timings.maxDuration;
@@ -1110,7 +1070,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var flags = {};
flags.hasTransitions = timings.transitionDuration > 0;
flags.hasAnimations = timings.animationDuration > 0;
- flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all';
+ flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty === 'all';
flags.applyTransitionDuration = hasToStyles && (
(flags.hasTransitions && !flags.hasTransitionAll)
|| (flags.hasAnimations && !flags.hasTransitions));
@@ -1140,9 +1100,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
return closeAndReturnNoopAnimator();
}
+ var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
+
if (options.delay != null) {
var delayStyle;
- if (typeof options.delay !== "boolean") {
+ if (typeof options.delay !== 'boolean') {
delayStyle = parseFloat(options.delay);
// number in options.delay means we have to recalculate the delay for the closing timeout
maxDelay = Math.max(delayStyle, 0);
@@ -1183,7 +1145,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
if (flags.blockTransition || flags.blockKeyframeAnimation) {
applyBlocking(maxDuration);
} else if (!options.skipBlocking) {
- blockTransitions(node, false);
+ helpers.blockTransitions(node, false);
}
// TODO(matsko): for 1.5 change this code to have an animator object for better debugging
@@ -1220,20 +1182,23 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
close(true);
}
- function close(rejected) { // jshint ignore:line
+ function close(rejected) {
// if the promise has been called already then we shouldn't close
// the animation again
if (animationClosed || (animationCompleted && animationPaused)) return;
animationClosed = true;
animationPaused = false;
- if (!options.$$skipPreparationClasses) {
+ if (preparationClasses && !options.$$skipPreparationClasses) {
$$jqLite.removeClass(element, preparationClasses);
}
- $$jqLite.removeClass(element, activeClasses);
+
+ if (activeClasses) {
+ $$jqLite.removeClass(element, activeClasses);
+ }
blockKeyframeAnimations(node, false);
- blockTransitions(node, false);
+ helpers.blockTransitions(node, false);
forEach(temporaryStyles, function(entry) {
// There is only one way to remove inline style properties entirely from elements.
@@ -1247,8 +1212,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
if (Object.keys(restoreStyles).length) {
forEach(restoreStyles, function(value, prop) {
- value ? node.style.setProperty(prop, value)
- : node.style.removeProperty(prop);
+ if (value) {
+ node.style.setProperty(prop, value);
+ } else {
+ node.style.removeProperty(prop);
+ }
});
}
@@ -1281,7 +1249,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
function applyBlocking(duration) {
if (flags.blockTransition) {
- blockTransitions(node, duration);
+ helpers.blockTransitions(node, duration);
}
if (flags.blockKeyframeAnimation) {
@@ -1312,6 +1280,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
event.stopPropagation();
var ev = event.originalEvent || event;
+ if (ev.target !== node) {
+ // Since TransitionEvent / AnimationEvent bubble up,
+ // we have to ignore events by finished child animations
+ return;
+ }
+
// we now always use `Date.now()` due to the recent changes with
// event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info)
var timeStamp = ev.$manualTimeStamp || Date.now();
@@ -1351,9 +1325,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
animationPaused = !playAnimation;
if (timings.animationDuration) {
var value = blockKeyframeAnimations(node, animationPaused);
- animationPaused
- ? temporaryStyles.push(value)
- : removeFromArray(temporaryStyles, value);
+ if (animationPaused) {
+ temporaryStyles.push(value);
+ } else {
+ removeFromArray(temporaryStyles, value);
+ }
}
} else if (animationPaused && playAnimation) {
animationPaused = false;
@@ -1402,10 +1378,10 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
$$jqLite.addClass(element, activeClasses);
if (flags.recalculateTimingStyles) {
- fullClassName = node.className + ' ' + preparationClasses;
- cacheKey = gcsHashFn(node, fullClassName);
+ fullClassName = node.getAttribute('class') + ' ' + preparationClasses;
+ cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
- timings = computeTimings(node, fullClassName, cacheKey);
+ timings = computeTimings(node, fullClassName, cacheKey, false);
relativeDelay = timings.maxDelay;
maxDelay = Math.max(relativeDelay, 0);
maxDuration = timings.maxDuration;
@@ -1420,7 +1396,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
}
if (flags.applyAnimationDelay) {
- relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay)
+ relativeDelay = typeof options.delay !== 'boolean' && truthyTimingValue(options.delay)
? parseFloat(options.delay)
: relativeDelay;
@@ -1512,7 +1488,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
}];
}];
-var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) {
+var $$AnimateCssDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) {
$$animationProvider.drivers.push('$$animateCssDriver');
var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim';
@@ -1541,8 +1517,6 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode
);
- var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
-
return function initDriverFn(animationDetails) {
return animationDetails.from && animationDetails.to
? prepareFromToAnchorAnimation(animationDetails.from,
@@ -1784,7 +1758,7 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
// TODO(matsko): add documentation
// by the time...
-var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
+var $$AnimateJsProvider = ['$animateProvider', /** @this */ function($animateProvider) {
this.$get = ['$injector', '$$AnimateRunner', '$$jqLite',
function($injector, $$AnimateRunner, $$jqLite) {
@@ -1823,7 +1797,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
var before, after;
if (animations.length) {
var afterFn, beforeFn;
- if (event == 'leave') {
+ if (event === 'leave') {
beforeFn = 'leave';
afterFn = 'afterLeave'; // TODO(matsko): get rid of this
} else {
@@ -2008,7 +1982,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
function packageAnimations(element, event, options, animations, fnName) {
var operations = groupEventedAnimations(element, event, options, animations, fnName);
if (operations.length === 0) {
- var a,b;
+ var a, b;
if (fnName === 'beforeSetClass') {
a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass');
b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass');
@@ -2036,11 +2010,19 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
});
}
- runners.length ? $$AnimateRunner.all(runners, callback) : callback();
+ if (runners.length) {
+ $$AnimateRunner.all(runners, callback);
+ } else {
+ callback();
+ }
return function endFn(reject) {
forEach(runners, function(runner) {
- reject ? runner.cancel() : runner.end();
+ if (reject) {
+ runner.cancel();
+ } else {
+ runner.end();
+ }
});
};
};
@@ -2050,7 +2032,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
function lookupAnimations(classes) {
classes = isArray(classes) ? classes : classes.split(' ');
var matches = [], flagMap = {};
- for (var i=0; i < classes.length; i++) {
+ for (var i = 0; i < classes.length; i++) {
var klass = classes[i],
animationFactory = $animateProvider.$$registeredAnimations[klass];
if (animationFactory && !flagMap[klass]) {
@@ -2063,7 +2045,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
}];
}];
-var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) {
+var $$AnimateJsDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) {
$$animationProvider.drivers.push('$$animateJsDriver');
this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) {
return function initDriverFn(animationDetails) {
@@ -2125,7 +2107,7 @@ var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProv
var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
-var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
+var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animateProvider) {
var PRE_DIGEST_STATE = 1;
var RUNNING_STATE = 2;
var ONE_SPACE = ' ';
@@ -2136,6 +2118,15 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
join: []
};
+ function getEventData(options) {
+ return {
+ addClass: options.addClass,
+ removeClass: options.removeClass,
+ from: options.from,
+ to: options.to
+ };
+ }
+
function makeTruthyCssClassMap(classString) {
if (!classString) {
return null;
@@ -2159,9 +2150,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
}
- function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
+ function isAllowed(ruleType, currentAnimation, previousAnimation) {
return rules[ruleType].some(function(fn) {
- return fn(element, currentAnimation, previousAnimation);
+ return fn(currentAnimation, previousAnimation);
});
}
@@ -2171,40 +2162,40 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return and ? a && b : a || b;
}
- rules.join.push(function(element, newAnimation, currentAnimation) {
+ rules.join.push(function(newAnimation, currentAnimation) {
// if the new animation is class-based then we can just tack that on
return !newAnimation.structural && hasAnimationClasses(newAnimation);
});
- rules.skip.push(function(element, newAnimation, currentAnimation) {
+ rules.skip.push(function(newAnimation, currentAnimation) {
// there is no need to animate anything if no classes are being added and
// there is no structural animation that will be triggered
return !newAnimation.structural && !hasAnimationClasses(newAnimation);
});
- rules.skip.push(function(element, newAnimation, currentAnimation) {
+ rules.skip.push(function(newAnimation, currentAnimation) {
// why should we trigger a new structural animation if the element will
// be removed from the DOM anyway?
- return currentAnimation.event == 'leave' && newAnimation.structural;
+ return currentAnimation.event === 'leave' && newAnimation.structural;
});
- rules.skip.push(function(element, newAnimation, currentAnimation) {
+ rules.skip.push(function(newAnimation, currentAnimation) {
// if there is an ongoing current animation then don't even bother running the class-based animation
return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural;
});
- rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ rules.cancel.push(function(newAnimation, currentAnimation) {
// there can never be two structural animations running at the same time
return currentAnimation.structural && newAnimation.structural;
});
- rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ rules.cancel.push(function(newAnimation, currentAnimation) {
// if the previous animation is already running, but the new animation will
// be triggered, but the new animation is structural
return currentAnimation.state === RUNNING_STATE && newAnimation.structural;
});
- rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ rules.cancel.push(function(newAnimation, currentAnimation) {
// cancel the animation if classes added / removed in both animation cancel each other out,
// but only if the current animation isn't structural
@@ -2223,15 +2214,21 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA);
});
- this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
+ this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$Map',
'$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
- function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
- $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) {
+ '$$isDocumentHidden',
+ function($$rAF, $rootScope, $rootElement, $document, $$Map,
+ $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow,
+ $$isDocumentHidden) {
- var activeAnimationsLookup = new $$HashMap();
- var disabledElementsLookup = new $$HashMap();
+ var activeAnimationsLookup = new $$Map();
+ var disabledElementsLookup = new $$Map();
var animationsEnabled = null;
+ function removeFromDisabledElementsLookup(evt) {
+ disabledElementsLookup.delete(evt.target);
+ }
+
function postDigestTaskFactory() {
var postDigestCalled = false;
return function(fn) {
@@ -2281,14 +2278,17 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var callbackRegistry = Object.create(null);
- // remember that the classNameFilter is set during the provider/config
- // stage therefore we can optimize here and setup a helper function
+ // remember that the `customFilter`/`classNameFilter` are set during the
+ // provider/config stage therefore we can optimize here and setup helper functions
+ var customFilter = $animateProvider.customFilter();
var classNameFilter = $animateProvider.classNameFilter();
- var isAnimatableClassName = !classNameFilter
- ? function() { return true; }
- : function(className) {
- return classNameFilter.test(className);
- };
+ var returnTrue = function() { return true; };
+
+ var isAnimatableByFilter = customFilter || returnTrue;
+ var isAnimatableClassName = !classNameFilter ? returnTrue : function(node, options) {
+ var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' ');
+ return classNameFilter.test(className);
+ };
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
@@ -2297,16 +2297,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
- var contains = window.Node.prototype.contains || function(arg) {
- // jshint bitwise: false
+ var contains = window.Node.prototype.contains || /** @this */ function(arg) {
+ // eslint-disable-next-line no-bitwise
return this === arg || !!(this.compareDocumentPosition(arg) & 16);
- // jshint bitwise: true
};
- function findCallbacks(parent, element, event) {
- var targetNode = getDomNode(element);
- var targetParentNode = getDomNode(parent);
-
+ function findCallbacks(targetParentNode, targetNode, event) {
var matches = [];
var entries = callbackRegistry[event];
if (entries) {
@@ -2331,11 +2327,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
});
}
- function cleanupEventListeners(phase, element) {
- if (phase === 'close' && !element[0].parentNode) {
+ function cleanupEventListeners(phase, node) {
+ if (phase === 'close' && !node.parentNode) {
// If the element is not attached to a parentNode, it has been removed by
// the domOperation, and we can safely remove the event callbacks
- $animate.off(element);
+ $animate.off(node);
}
}
@@ -2416,7 +2412,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
bool = !disabledElementsLookup.get(node);
} else {
// (element, bool) - Element setter
- disabledElementsLookup.put(node, !bool);
+ if (!disabledElementsLookup.has(node)) {
+ // The element is added to the map for the first time.
+ // Create a listener to remove it on `$destroy` (to avoid memory leak).
+ jqLite(element).on('$destroy', removeFromDisabledElementsLookup);
+ }
+ disabledElementsLookup.set(node, !bool);
}
}
}
@@ -2427,18 +2428,15 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return $animate;
- function queueAnimation(element, event, initialOptions) {
+ function queueAnimation(originalElement, event, initialOptions) {
// we always make a copy of the options since
// there should never be any side effects on
// the input data when running `$animateCss`.
var options = copy(initialOptions);
- var node, parent;
- element = stripCommentsFromElement(element);
- if (element) {
- node = getDomNode(element);
- parent = element.parent();
- }
+ var element = stripCommentsFromElement(originalElement);
+ var node = getDomNode(element);
+ var parentNode = node && node.parentNode;
options = prepareAnimationOptions(options);
@@ -2473,49 +2471,45 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
options.to = null;
}
- // there are situations where a directive issues an animation for
- // a jqLite wrapper that contains only comment nodes... If this
- // happens then there is no way we can perform an animation
- if (!node) {
- close();
- return runner;
- }
-
- var className = [node.className, options.addClass, options.removeClass].join(' ');
- if (!isAnimatableClassName(className)) {
+ // If animations are hard-disabled for the whole application there is no need to continue.
+ // There are also situations where a directive issues an animation for a jqLite wrapper that
+ // contains only comment nodes. In this case, there is no way we can perform an animation.
+ if (!animationsEnabled ||
+ !node ||
+ !isAnimatableByFilter(node, event, initialOptions) ||
+ !isAnimatableClassName(node, options)) {
close();
return runner;
}
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
- var documentHidden = $document[0].hidden;
+ var documentHidden = $$isDocumentHidden();
- // this is a hard disable of all animations for the application or on
- // the element itself, therefore there is no need to continue further
- // past this point if not enabled
+ // This is a hard disable of all animations the element itself, therefore there is no need to
+ // continue further past this point if not enabled
// Animations are also disabled if the document is currently hidden (page is not visible
// to the user), because browsers slow down or do not flush calls to requestAnimationFrame
- var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node);
+ var skipAnimations = documentHidden || disabledElementsLookup.get(node);
var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
var hasExistingAnimation = !!existingAnimation.state;
// there is no point in traversing the same collection of parent ancestors if a followup
// animation will be run on the same element that already did all that checking work
- if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) {
- skipAnimations = !areAnimationsAllowed(element, parent, event);
+ if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state !== PRE_DIGEST_STATE)) {
+ skipAnimations = !areAnimationsAllowed(node, parentNode, event);
}
if (skipAnimations) {
// Callbacks should fire even if the document is hidden (regression fix for issue #14120)
- if (documentHidden) notifyProgress(runner, event, 'start');
+ if (documentHidden) notifyProgress(runner, event, 'start', getEventData(options));
close();
- if (documentHidden) notifyProgress(runner, event, 'close');
+ if (documentHidden) notifyProgress(runner, event, 'close', getEventData(options));
return runner;
}
if (isStructural) {
- closeChildAnimations(element);
+ closeChildAnimations(node);
}
var newAnimation = {
@@ -2530,7 +2524,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
};
if (hasExistingAnimation) {
- var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation);
+ var skipAnimationFlag = isAllowed('skip', newAnimation, existingAnimation);
if (skipAnimationFlag) {
if (existingAnimation.state === RUNNING_STATE) {
close();
@@ -2540,7 +2534,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return existingAnimation.runner;
}
}
- var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation);
+ var cancelAnimationFlag = isAllowed('cancel', newAnimation, existingAnimation);
if (cancelAnimationFlag) {
if (existingAnimation.state === RUNNING_STATE) {
// this will end the animation right away and it is safe
@@ -2562,12 +2556,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// a joined animation means that this animation will take over the existing one
// so an example would involve a leave animation taking over an enter. Then when
// the postDigest kicks in the enter will be ignored.
- var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation);
+ var joinAnimationFlag = isAllowed('join', newAnimation, existingAnimation);
if (joinAnimationFlag) {
if (existingAnimation.state === RUNNING_STATE) {
normalizeAnimationDetails(element, newAnimation);
} else {
- applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
+ applyGeneratedPreparationClasses($$jqLite, element, isStructural ? event : null, options);
event = newAnimation.event = existingAnimation.event;
options = mergeAnimationDetails(element, existingAnimation, newAnimation);
@@ -2596,7 +2590,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
if (!isValidAnimation) {
close();
- clearElementAnimationState(element);
+ clearElementAnimationState(node);
return runner;
}
@@ -2604,9 +2598,18 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var counter = (existingAnimation.counter || 0) + 1;
newAnimation.counter = counter;
- markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation);
+ markElementAnimationState(node, PRE_DIGEST_STATE, newAnimation);
$rootScope.$$postDigest(function() {
+ // It is possible that the DOM nodes inside `originalElement` have been replaced. This can
+ // happen if the animated element is a transcluded clone and also has a `templateUrl`
+ // directive on it. Therefore, we must recreate `element` in order to interact with the
+ // actual DOM nodes.
+ // Note: We still need to use the old `node` for certain things, such as looking up in
+ // HashMaps where it was used as the key.
+
+ element = stripCommentsFromElement(originalElement);
+
var animationDetails = activeAnimationsLookup.get(node);
var animationCancelled = !animationDetails;
animationDetails = animationDetails || {};
@@ -2645,7 +2648,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// isn't allowed to animate from here then we need to clear the state of the element
// so that any future animations won't read the expired animation data.
if (!isValidAnimation) {
- clearElementAnimationState(element);
+ clearElementAnimationState(node);
}
return;
@@ -2657,21 +2660,21 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
? 'setClass'
: animationDetails.event;
- markElementAnimationState(element, RUNNING_STATE);
+ markElementAnimationState(node, RUNNING_STATE);
var realRunner = $$animation(element, event, animationDetails.options);
// this will update the runner's flow-control events based on
// the `realRunner` object.
runner.setHost(realRunner);
- notifyProgress(runner, event, 'start', {});
+ notifyProgress(runner, event, 'start', getEventData(options));
realRunner.done(function(status) {
close(!status);
var animationDetails = activeAnimationsLookup.get(node);
if (animationDetails && animationDetails.counter === counter) {
- clearElementAnimationState(getDomNode(element));
+ clearElementAnimationState(node);
}
- notifyProgress(runner, event, 'close', {});
+ notifyProgress(runner, event, 'close', getEventData(options));
});
});
@@ -2679,7 +2682,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
function notifyProgress(runner, event, phase, data) {
runInNextPostDigestOrNow(function() {
- var callbacks = findCallbacks(parent, element, event);
+ var callbacks = findCallbacks(parentNode, node, event);
if (callbacks.length) {
// do not optimize this call here to RAF because
// we don't know how heavy the callback code here will
@@ -2689,16 +2692,16 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
forEach(callbacks, function(callback) {
callback(element, phase, data);
});
- cleanupEventListeners(phase, element);
+ cleanupEventListeners(phase, node);
});
} else {
- cleanupEventListeners(phase, element);
+ cleanupEventListeners(phase, node);
}
});
runner.progress(event, phase, data);
}
- function close(reject) { // jshint ignore:line
+ function close(reject) {
clearGeneratedClasses(element, options);
applyAnimationClasses(element, options);
applyAnimationStyles(element, options);
@@ -2707,11 +2710,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
}
- function closeChildAnimations(element) {
- var node = getDomNode(element);
+ function closeChildAnimations(node) {
var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
forEach(children, function(child) {
- var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME));
+ var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME), 10);
var animationDetails = activeAnimationsLookup.get(child);
if (animationDetails) {
switch (state) {
@@ -2719,21 +2721,16 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
animationDetails.runner.end();
/* falls through */
case PRE_DIGEST_STATE:
- activeAnimationsLookup.remove(child);
+ activeAnimationsLookup.delete(child);
break;
}
}
});
}
- function clearElementAnimationState(element) {
- var node = getDomNode(element);
+ function clearElementAnimationState(node) {
node.removeAttribute(NG_ANIMATE_ATTR_NAME);
- activeAnimationsLookup.remove(node);
- }
-
- function isMatchingElement(nodeOrElmA, nodeOrElmB) {
- return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB);
+ activeAnimationsLookup.delete(node);
}
/**
@@ -2743,54 +2740,54 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
* c) the element is not a child of the body
* d) the element is not a child of the $rootElement
*/
- function areAnimationsAllowed(element, parentElement, event) {
- var bodyElement = jqLite($document[0].body);
- var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
- var rootElementDetected = isMatchingElement(element, $rootElement);
+ function areAnimationsAllowed(node, parentNode, event) {
+ var bodyNode = $document[0].body;
+ var rootNode = getDomNode($rootElement);
+
+ var bodyNodeDetected = (node === bodyNode) || node.nodeName === 'HTML';
+ var rootNodeDetected = (node === rootNode);
var parentAnimationDetected = false;
+ var elementDisabled = disabledElementsLookup.get(node);
var animateChildren;
- var elementDisabled = disabledElementsLookup.get(getDomNode(element));
- var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA);
+ var parentHost = jqLite.data(node, NG_ANIMATE_PIN_DATA);
if (parentHost) {
- parentElement = parentHost;
+ parentNode = getDomNode(parentHost);
}
- parentElement = getDomNode(parentElement);
-
- while (parentElement) {
- if (!rootElementDetected) {
- // angular doesn't want to attempt to animate elements outside of the application
+ while (parentNode) {
+ if (!rootNodeDetected) {
+ // AngularJS doesn't want to attempt to animate elements outside of the application
// therefore we need to ensure that the rootElement is an ancestor of the current element
- rootElementDetected = isMatchingElement(parentElement, $rootElement);
+ rootNodeDetected = (parentNode === rootNode);
}
- if (parentElement.nodeType !== ELEMENT_NODE) {
+ if (parentNode.nodeType !== ELEMENT_NODE) {
// no point in inspecting the #document element
break;
}
- var details = activeAnimationsLookup.get(parentElement) || {};
+ var details = activeAnimationsLookup.get(parentNode) || {};
// either an enter, leave or move animation will commence
// therefore we can't allow any animations to take place
// but if a parent animation is class-based then that's ok
if (!parentAnimationDetected) {
- var parentElementDisabled = disabledElementsLookup.get(parentElement);
+ var parentNodeDisabled = disabledElementsLookup.get(parentNode);
- if (parentElementDisabled === true && elementDisabled !== false) {
+ if (parentNodeDisabled === true && elementDisabled !== false) {
// disable animations if the user hasn't explicitly enabled animations on the
// current element
elementDisabled = true;
// element is disabled via parent element, no need to check anything else
break;
- } else if (parentElementDisabled === false) {
+ } else if (parentNodeDisabled === false) {
elementDisabled = false;
}
parentAnimationDetected = details.structural;
}
if (isUndefined(animateChildren) || animateChildren === true) {
- var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA);
+ var value = jqLite.data(parentNode, NG_ANIMATE_CHILDREN_DATA);
if (isDefined(value)) {
animateChildren = value;
}
@@ -2799,57 +2796,57 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// there is no need to continue traversing at this point
if (parentAnimationDetected && animateChildren === false) break;
- if (!bodyElementDetected) {
+ if (!bodyNodeDetected) {
// we also need to ensure that the element is or will be a part of the body element
// otherwise it is pointless to even issue an animation to be rendered
- bodyElementDetected = isMatchingElement(parentElement, bodyElement);
+ bodyNodeDetected = (parentNode === bodyNode);
}
- if (bodyElementDetected && rootElementDetected) {
+ if (bodyNodeDetected && rootNodeDetected) {
// If both body and root have been found, any other checks are pointless,
// as no animation data should live outside the application
break;
}
- if (!rootElementDetected) {
- // If no rootElement is detected, check if the parentElement is pinned to another element
- parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA);
+ if (!rootNodeDetected) {
+ // If `rootNode` is not detected, check if `parentNode` is pinned to another element
+ parentHost = jqLite.data(parentNode, NG_ANIMATE_PIN_DATA);
if (parentHost) {
// The pin target element becomes the next parent element
- parentElement = getDomNode(parentHost);
+ parentNode = getDomNode(parentHost);
continue;
}
}
- parentElement = parentElement.parentNode;
+ parentNode = parentNode.parentNode;
}
var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true;
- return allowAnimation && rootElementDetected && bodyElementDetected;
+ return allowAnimation && rootNodeDetected && bodyNodeDetected;
}
- function markElementAnimationState(element, state, details) {
+ function markElementAnimationState(node, state, details) {
details = details || {};
details.state = state;
- var node = getDomNode(element);
node.setAttribute(NG_ANIMATE_ATTR_NAME, state);
var oldValue = activeAnimationsLookup.get(node);
var newValue = oldValue
? extend(oldValue, details)
: details;
- activeAnimationsLookup.put(node, newValue);
+ activeAnimationsLookup.set(node, newValue);
}
}];
}];
-var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
+var $$AnimationProvider = ['$animateProvider', /** @this */ function($animateProvider) {
var NG_ANIMATE_REF_ATTR = 'ng-animate-ref';
var drivers = this.drivers = [];
var RUNNER_STORAGE_KEY = '$$animationRunner';
+ var PREPARE_CLASSES_KEY = '$$animatePrepareClasses';
function setRunner(element, runner) {
element.data(RUNNER_STORAGE_KEY, runner);
@@ -2863,22 +2860,23 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
return element.data(RUNNER_STORAGE_KEY);
}
- this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler',
- function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) {
+ this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler', '$$animateCache',
+ function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler, $$animateCache) {
var animationQueue = [];
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
function sortAnimations(animations) {
var tree = { children: [] };
- var i, lookup = new $$HashMap();
+ var i, lookup = new $$Map();
- // this is done first beforehand so that the hashmap
+ // this is done first beforehand so that the map
// is filled with a list of the elements that will be animated
for (i = 0; i < animations.length; i++) {
var animation = animations[i];
- lookup.put(animation.domNode, animations[i] = {
+ lookup.set(animation.domNode, animations[i] = {
domNode: animation.domNode,
+ element: animation.element,
fn: animation.fn,
children: []
});
@@ -2896,7 +2894,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
var elementNode = entry.domNode;
var parentNode = elementNode.parentNode;
- lookup.put(elementNode, entry);
+ lookup.set(elementNode, entry);
var parentEntry;
while (parentNode) {
@@ -2935,7 +2933,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
result.push(row);
row = [];
}
- row.push(entry.fn);
+ row.push(entry);
entry.children.forEach(function(childEntry) {
nextLevelEntries++;
queue.push(childEntry);
@@ -2970,8 +2968,6 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
return runner;
}
- setRunner(element, runner);
-
var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass));
var tempClasses = options.tempClasses;
if (tempClasses) {
@@ -2979,12 +2975,12 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
options.tempClasses = null;
}
- var prepareClassName;
if (isStructural) {
- prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX;
- $$jqLite.addClass(element, prepareClassName);
+ element.data(PREPARE_CLASSES_KEY, 'ng-' + event + PREPARE_CLASS_SUFFIX);
}
+ setRunner(element, runner);
+
animationQueue.push({
// this data is used by the postDigest code and passed into
// the driver step function
@@ -3024,16 +3020,31 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
var toBeSortedAnimations = [];
forEach(groupedAnimations, function(animationEntry) {
+ var element = animationEntry.from ? animationEntry.from.element : animationEntry.element;
+ var extraClasses = options.addClass;
+
+ extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
+ var cacheKey = $$animateCache.cacheKey(element[0], animationEntry.event, extraClasses, options.removeClass);
+
toBeSortedAnimations.push({
- domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element),
+ element: element,
+ domNode: getDomNode(element),
fn: function triggerAnimationStart() {
+ var startAnimationFn, closeFn = animationEntry.close;
+
+ // in the event that we've cached the animation status for this element
+ // and it's in fact an invalid animation (something that has duration = 0)
+ // then we should skip all the heavy work from here on
+ if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
+ closeFn();
+ return;
+ }
+
// it's important that we apply the `ng-animate` CSS class and the
// temporary classes before we do any driver invoking since these
// CSS classes may be required for proper CSS detection.
animationEntry.beforeStart();
- var startAnimationFn, closeFn = animationEntry.close;
-
// in the event that the element was removed before the digest runs or
// during the RAF sequencing then we should not trigger the animation.
var targetElement = animationEntry.anchors
@@ -3063,7 +3074,32 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
// we need to sort each of the animations in order of parent to child
// relationships. This ensures that the child classes are applied at the
// right time.
- $$rAFScheduler(sortAnimations(toBeSortedAnimations));
+ var finalAnimations = sortAnimations(toBeSortedAnimations);
+ for (var i = 0; i < finalAnimations.length; i++) {
+ var innerArray = finalAnimations[i];
+ for (var j = 0; j < innerArray.length; j++) {
+ var entry = innerArray[j];
+ var element = entry.element;
+
+ // the RAFScheduler code only uses functions
+ finalAnimations[i][j] = entry.fn;
+
+ // the first row of elements shouldn't have a prepare-class added to them
+ // since the elements are at the top of the animation hierarchy and they
+ // will be applied without a RAF having to pass...
+ if (i === 0) {
+ element.removeData(PREPARE_CLASSES_KEY);
+ continue;
+ }
+
+ var prepareClassName = element.data(PREPARE_CLASSES_KEY);
+ if (prepareClassName) {
+ $$jqLite.addClass(element, prepareClassName);
+ }
+ }
+ }
+
+ $$rAFScheduler(finalAnimations);
});
return runner;
@@ -3201,10 +3237,10 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
}
function beforeStart() {
- element.addClass(NG_ANIMATE_CLASSNAME);
- if (tempClasses) {
- $$jqLite.addClass(element, tempClasses);
- }
+ tempClasses = (tempClasses ? (tempClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
+ $$jqLite.addClass(element, tempClasses);
+
+ var prepareClassName = element.data(PREPARE_CLASSES_KEY);
if (prepareClassName) {
$$jqLite.removeClass(element, prepareClassName);
prepareClassName = null;
@@ -3232,7 +3268,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
}
}
- function close(rejected) { // jshint ignore:line
+ function close(rejected) {
element.off('$destroy', handleDestroyedElement);
removeRunner(element);
@@ -3244,7 +3280,6 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
$$jqLite.removeClass(element, tempClasses);
}
- element.removeClass(NG_ANIMATE_CLASSNAME);
runner.complete(!rejected);
}
};
@@ -3338,12 +3373,13 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
* </file>
* </example>
*/
-var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $rootScope) {
+var ngAnimateSwapDirective = ['$animate', function($animate) {
return {
restrict: 'A',
transclude: 'element',
terminal: true,
- priority: 600, // we use 600 here to ensure that the directive is caught before others
+ priority: 550, // We use 550 here to ensure that the directive is caught before others,
+ // but after `ngIf` (at priority 600).
link: function(scope, $element, attrs, ctrl, $transclude) {
var previousElement, previousScope;
scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) {
@@ -3355,10 +3391,10 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
previousScope = null;
}
if (value || value === 0) {
- previousScope = scope.$new();
- $transclude(previousScope, function(element) {
- previousElement = element;
- $animate.enter(element, null, $element);
+ $transclude(function(clone, childScope) {
+ previousElement = clone;
+ previousScope = childScope;
+ $animate.enter(clone, null, $element);
});
}
});
@@ -3372,11 +3408,9 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
* @description
*
* The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via
- * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app.
- *
- * <div doc-module-components="ngAnimate"></div>
+ * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an AngularJS app.
*
- * # Usage
+ * ## Usage
* Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based
* using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For
* both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within
@@ -3385,25 +3419,33 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
* ## Directive Support
* The following directives are "animation aware":
*
- * | Directive | Supported Animations |
- * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
- * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
- * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
- * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
- * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
- * | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
- * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
- * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
- * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
- * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
- * | {@link module:ngMessages#animations ngMessage} | enter and leave |
- *
- * (More information can be found by visiting each the documentation associated with each directive.)
+ * | Directive | Supported Animations |
+ * |-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
+ * | {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
+ * | {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave |
+ * | {@link ng.directive:ngClass#animations ngClass / {{class&#125;&#8203;&#125;} | add and remove |
+ * | {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
+ * | {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
+ * | {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
+ * | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
+ * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
+ * | {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
+ * | {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
+ * | {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
+ * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
+ * | {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
+ * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
+ * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
+ *
+ * (More information can be found by visiting the documentation associated with each directive.)
+ *
+ * For a full breakdown of the steps involved during each animation event, refer to the
+ * {@link ng.$animate `$animate` API docs}.
*
* ## CSS-based Animations
*
* CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML
- * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation.
+ * and CSS code we can create an animation that will be picked up by AngularJS when an underlying directive performs an operation.
*
* The example below shows how an `enter` animation can be made possible on an element using `ng-if`:
*
@@ -3543,6 +3585,10 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
* /&#42; As of 1.4.4, this must always be set: it signals ngAnimate
* to not accidentally inherit a delay property from another CSS class &#42;/
* transition-duration: 0s;
+ *
+ * /&#42; if you are using animations instead of transitions you should configure as follows:
+ * animation-delay: 0.1s;
+ * animation-duration: 0s; &#42;/
* }
* .my-animation.ng-enter.ng-enter-active {
* /&#42; standard transition styles &#42;/
@@ -3631,9 +3677,22 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
* .message.ng-enter-prepare {
* opacity: 0;
* }
- *
* ```
*
+ * ### Animating between value changes
+ *
+ * Sometimes you need to animate between different expression states, whose values
+ * don't necessary need to be known or referenced in CSS styles.
+ * Unless possible with another {@link ngAnimate#directive-support "animation aware" directive},
+ * that specific use case can always be covered with {@link ngAnimate.directive:ngAnimateSwap} as
+ * can be seen in {@link ngAnimate.directive:ngAnimateSwap#examples this example}.
+ *
+ * Note that {@link ngAnimate.directive:ngAnimateSwap} is a *structural directive*, which means it
+ * creates a new instance of the element (including any other/child directives it may have) and
+ * links it to a new scope every time *swap* happens. In some cases this might not be desirable
+ * (e.g. for performance reasons, or when you wish to retain internal state on the original
+ * element instance).
+ *
* ## JavaScript-based Animations
*
* ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
@@ -3658,7 +3717,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
* enter: function(element, doneFn) {
* jQuery(element).fadeIn(1000, doneFn);
*
- * // remember to call doneFn so that angular
+ * // remember to call doneFn so that AngularJS
* // knows that the animation has concluded
* },
*
@@ -3706,7 +3765,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
*
* ## CSS + JS Animations Together
*
- * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular,
+ * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of AngularJS,
* defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking
* charge of the animation**:
*
@@ -3898,7 +3957,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
deps="angular-animate.js;angular-route.js"
animations="true">
<file name="index.html">
- <a href="#/">Home</a>
+ <a href="#!/">Home</a>
<hr />
<div class="view-container">
<div ng-view class="view"></div>
@@ -3918,22 +3977,23 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
}])
.run(['$rootScope', function($rootScope) {
$rootScope.records = [
- { id:1, title: "Miss Beulah Roob" },
- { id:2, title: "Trent Morissette" },
- { id:3, title: "Miss Ava Pouros" },
- { id:4, title: "Rod Pouros" },
- { id:5, title: "Abdul Rice" },
- { id:6, title: "Laurie Rutherford Sr." },
- { id:7, title: "Nakia McLaughlin" },
- { id:8, title: "Jordon Blanda DVM" },
- { id:9, title: "Rhoda Hand" },
- { id:10, title: "Alexandrea Sauer" }
+ { id: 1, title: 'Miss Beulah Roob' },
+ { id: 2, title: 'Trent Morissette' },
+ { id: 3, title: 'Miss Ava Pouros' },
+ { id: 4, title: 'Rod Pouros' },
+ { id: 5, title: 'Abdul Rice' },
+ { id: 6, title: 'Laurie Rutherford Sr.' },
+ { id: 7, title: 'Nakia McLaughlin' },
+ { id: 8, title: 'Jordon Blanda DVM' },
+ { id: 9, title: 'Rhoda Hand' },
+ { id: 10, title: 'Alexandrea Sauer' }
];
}])
.controller('HomeController', [function() {
//empty
}])
- .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) {
+ .controller('ProfileController', ['$rootScope', '$routeParams',
+ function ProfileController($rootScope, $routeParams) {
var index = parseInt($routeParams.id, 10);
var record = $rootScope.records[index - 1];
@@ -3945,7 +4005,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
<h2>Welcome to the home page</h1>
<p>Please click on an element</p>
<a class="record"
- ng-href="#/profile/{{ record.id }}"
+ ng-href="#!/profile/{{ record.id }}"
ng-animate-ref="{{ record.id }}"
ng-repeat="record in records">
{{ record.title }}
@@ -4021,7 +4081,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
*
* ## Using $animate in your directive code
*
- * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application?
+ * So far we've explored how to feed in animations into an AngularJS application, but how do we trigger animations within our own directives in our application?
* By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's
* imagine we have a greeting box that shows and hides itself when the data changes
*
@@ -4064,7 +4124,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
* });
* ```
*
- * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case
+ * (Note that earlier versions of AngularJS prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case
* anymore.)
*
* In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering
@@ -4079,7 +4139,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root
* }])
* ```
*
- * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.)
+ * (Note that you will need to trigger a digest within the callback to get AngularJS to notice any scope-related changes.)
*/
var copy;
@@ -4106,7 +4166,7 @@ var noop;
* Click here {@link ng.$animate to learn more about animations with `$animate`}.
*/
angular.module('ngAnimate', [], function initAngularHelpers() {
- // Access helpers from angular core.
+ // Access helpers from AngularJS core.
// Do it inside a `config` block to ensure `window.angular` is available.
noop = angular.noop;
copy = angular.copy;
@@ -4121,12 +4181,14 @@ angular.module('ngAnimate', [], function initAngularHelpers() {
isFunction = angular.isFunction;
isElement = angular.isElement;
})
+ .info({ angularVersion: '"1.8.2"' })
.directive('ngAnimateSwap', ngAnimateSwapDirective)
.directive('ngAnimateChildren', $$AnimateChildrenDirective)
.factory('$$rAFScheduler', $$rAFSchedulerFactory)
.provider('$$animateQueue', $$AnimateQueueProvider)
+ .provider('$$animateCache', $$AnimateCacheProvider)
.provider('$$animation', $$AnimationProvider)
.provider('$animateCss', $AnimateCssProvider)
diff --git a/xstatic/pkg/angular/data/angular-aria.js b/xstatic/pkg/angular/data/angular-aria.js
index e9048c6..cc0f6f6 100644
--- a/xstatic/pkg/angular/data/angular-aria.js
+++ b/xstatic/pkg/angular/data/angular-aria.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -15,30 +15,28 @@
* attributes that convey state or semantic information about the application for users
* of assistive technologies, such as screen readers.
*
- * <div doc-module-components="ngAria"></div>
- *
* ## Usage
*
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
* directives are supported:
- * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
- * `ngDblClick`, and `ngMessages`.
+ * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`,
+ * `ngClick`, `ngDblClick`, and `ngMessages`.
*
* Below is a more detailed breakdown of the attributes handled by ngAria:
*
- * | Directive | Supported Attributes |
- * |---------------------------------------------|----------------------------------------------------------------------------------------|
+ * | Directive | Supported Attributes |
+ * |---------------------------------------------|-----------------------------------------------------------------------------------------------------|
* | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles |
- * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
- * | {@link ng.directive:ngRequired ngRequired} | aria-required
- * | {@link ng.directive:ngChecked ngChecked} | aria-checked
- * | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly |
- * | {@link ng.directive:ngValue ngValue} | aria-checked |
- * | {@link ng.directive:ngShow ngShow} | aria-hidden |
- * | {@link ng.directive:ngHide ngHide} | aria-hidden |
- * | {@link ng.directive:ngDblclick ngDblclick} | tabindex |
- * | {@link module:ngMessages ngMessages} | aria-live |
- * | {@link ng.directive:ngClick ngClick} | tabindex, keypress event, button role |
+ * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
+ * | {@link ng.directive:ngRequired ngRequired} | aria-required |
+ * | {@link ng.directive:ngChecked ngChecked} | aria-checked |
+ * | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly |
+ * | {@link ng.directive:ngValue ngValue} | aria-checked |
+ * | {@link ng.directive:ngShow ngShow} | aria-hidden |
+ * | {@link ng.directive:ngHide ngHide} | aria-hidden |
+ * | {@link ng.directive:ngDblclick ngDblclick} | tabindex |
+ * | {@link module:ngMessages ngMessages} | aria-live |
+ * | {@link ng.directive:ngClick ngClick} | tabindex, keydown event, button role |
*
* Find out more information about each directive by reading the
* {@link guide/accessibility ngAria Developer Guide}.
@@ -53,19 +51,25 @@
* <md-checkbox ng-disabled="disabled" aria-disabled="true">
* ```
*
- * ## Disabling Attributes
- * It's possible to disable individual attributes added by ngAria with the
+ * ## Disabling Specific Attributes
+ * It is possible to disable individual attributes added by ngAria with the
* {@link ngAria.$ariaProvider#config config} method. For more details, see the
* {@link guide/accessibility Developer Guide}.
+ *
+ * ## Disabling `ngAria` on Specific Elements
+ * It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable`
+ * attribute on it. Note that only the element itself (and not its child elements) will be ignored.
*/
- /* global -ngAriaModule */
+var ARIA_DISABLE_ATTR = 'ngAriaDisable';
+
var ngAriaModule = angular.module('ngAria', ['ng']).
+ info({ angularVersion: '"1.8.2"' }).
provider('$aria', $AriaProvider);
/**
* Internal Utilities
*/
-var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY'];
+var nativeAriaNodeNames = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY'];
var isNodeOneOf = function(elem, nodeTypeArray) {
if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
@@ -75,6 +79,7 @@ var isNodeOneOf = function(elem, nodeTypeArray) {
/**
* @ngdoc provider
* @name $ariaProvider
+ * @this
*
* @description
*
@@ -103,7 +108,7 @@ function $AriaProvider() {
ariaInvalid: true,
ariaValue: true,
tabindex: true,
- bindKeypress: true,
+ bindKeydown: true,
bindRoleForClick: true
};
@@ -119,12 +124,15 @@ function $AriaProvider() {
* - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags
* - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags
* - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags
- * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags
+ * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and
+ * aria-valuenow tags
* - **tabindex** – `{boolean}` – Enables/disables tabindex tags
- * - **bindKeypress** – `{boolean}` – Enables/disables keypress event binding on `div` and
- * `li` elements with ng-click
- * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements like `div`
- * using ng-click, making them more accessible to users of assistive technologies
+ * - **bindKeydown** – `{boolean}` – Enables/disables keyboard event binding on non-interactive
+ * elements (such as `div` or `li`) using ng-click, making them more accessible to users of
+ * assistive technologies
+ * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements (such as
+ * `div` or `li`) using ng-click, making them more accessible to users of assistive
+ * technologies
*
* @description
* Enables/disables various ARIA attributes
@@ -133,10 +141,12 @@ function $AriaProvider() {
config = angular.extend(config, newConfig);
};
- function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
+ function watchExpr(attrName, ariaAttr, nativeAriaNodeNames, negate) {
return function(scope, elem, attr) {
+ if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
+
var ariaCamelName = attr.$normalize(ariaAttr);
- if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
+ if (config[ariaCamelName] && !isNodeOneOf(elem, nativeAriaNodeNames) && !attr[ariaCamelName]) {
scope.$watch(attr[attrName], function(boolVal) {
// ensure boolean value
boolVal = negate ? !boolVal : !!boolVal;
@@ -150,7 +160,6 @@ function $AriaProvider() {
* @name $aria
*
* @description
- * @priority 200
*
* The $aria service contains helper methods for applying common
* [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives.
@@ -161,7 +170,7 @@ function $AriaProvider() {
*
*```js
* ngAriaModule.directive('ngDisabled', ['$aria', function($aria) {
- * return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false);
+ * return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nativeAriaNodeNames, false);
* }])
*```
* Shown above, the ngAria module creates a directive with the same signature as the
@@ -213,28 +222,31 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false);
}])
.directive('ngValue', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngValue', 'aria-checked', nodeBlackList, false);
+ return $aria.$$watchExpr('ngValue', 'aria-checked', nativeAriaNodeNames, false);
}])
.directive('ngChecked', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngChecked', 'aria-checked', nodeBlackList, false);
+ return $aria.$$watchExpr('ngChecked', 'aria-checked', nativeAriaNodeNames, false);
}])
.directive('ngReadonly', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngReadonly', 'aria-readonly', nodeBlackList, false);
+ return $aria.$$watchExpr('ngReadonly', 'aria-readonly', nativeAriaNodeNames, false);
}])
.directive('ngRequired', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngRequired', 'aria-required', nodeBlackList, false);
+ return $aria.$$watchExpr('ngRequired', 'aria-required', nativeAriaNodeNames, false);
}])
.directive('ngModel', ['$aria', function($aria) {
- function shouldAttachAttr(attr, normalizedAttr, elem, allowBlacklistEls) {
- return $aria.config(normalizedAttr) && !elem.attr(attr) && (allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList));
+ function shouldAttachAttr(attr, normalizedAttr, elem, allowNonAriaNodes) {
+ return $aria.config(normalizedAttr) &&
+ !elem.attr(attr) &&
+ (allowNonAriaNodes || !isNodeOneOf(elem, nativeAriaNodeNames)) &&
+ (elem.attr('type') !== 'hidden' || elem[0].nodeName !== 'INPUT');
}
function shouldAttachRole(role, elem) {
// if element does not have role attribute
// AND element type is equal to role (if custom element has a type equaling shape) <-- remove?
- // AND element is not INPUT
- return !elem.attr('role') && (elem.attr('type') === role) && (elem[0].nodeName !== 'INPUT');
+ // AND element is not in nativeAriaNodeNames
+ return !elem.attr('role') && (elem.attr('type') === role) && !isNodeOneOf(elem, nativeAriaNodeNames);
}
function getShape(attr, elem) {
@@ -251,17 +263,11 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
require: 'ngModel',
priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
compile: function(elem, attr) {
+ if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
+
var shape = getShape(attr, elem);
return {
- pre: function(scope, elem, attr, ngModel) {
- if (shape === 'checkbox') {
- //Use the input[checkbox] $isEmpty implementation for elements with checkbox roles
- ngModel.$isEmpty = function(value) {
- return value === false;
- };
- }
- },
post: function(scope, elem, attr, ngModel) {
var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem, false);
@@ -270,6 +276,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
}
function getRadioReaction(newVal) {
+ // Strict comparison would cause a BC
+ // eslint-disable-next-line eqeqeq
var boolVal = (attr.value == ngModel.$viewValue);
elem.attr('aria-checked', boolVal);
}
@@ -346,13 +354,15 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
};
}])
.directive('ngDisabled', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false);
+ return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nativeAriaNodeNames, false);
}])
.directive('ngMessages', function() {
return {
restrict: 'A',
require: '?ngMessages',
link: function(scope, elem, attr, ngMessages) {
+ if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
+
if (!elem.attr('aria-live')) {
elem.attr('aria-live', 'assertive');
}
@@ -363,10 +373,12 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
return {
restrict: 'A',
compile: function(elem, attr) {
- var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true);
+ if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
+
+ var fn = $parse(attr.ngClick);
return function(scope, elem, attr) {
- if (!isNodeOneOf(elem, nodeBlackList)) {
+ if (!isNodeOneOf(elem, nativeAriaNodeNames)) {
if ($aria.config('bindRoleForClick') && !elem.attr('role')) {
elem.attr('role', 'button');
@@ -376,10 +388,17 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
elem.attr('tabindex', 0);
}
- if ($aria.config('bindKeypress') && !attr.ngKeypress) {
- elem.on('keypress', function(event) {
+ if ($aria.config('bindKeydown') && !attr.ngKeydown && !attr.ngKeypress && !attr.ngKeyup) {
+ elem.on('keydown', function(event) {
var keyCode = event.which || event.keyCode;
- if (keyCode === 32 || keyCode === 13) {
+
+ if (keyCode === 13 || keyCode === 32) {
+ // If the event is triggered on a non-interactive element ...
+ if (nativeAriaNodeNames.indexOf(event.target.nodeName) === -1 && !event.target.isContentEditable) {
+ // ... prevent the default browser behavior (e.g. scrolling when pressing spacebar)
+ // See https://github.com/angular/angular.js/issues/16664
+ event.preventDefault();
+ }
scope.$apply(callback);
}
@@ -395,7 +414,9 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
}])
.directive('ngDblclick', ['$aria', function($aria) {
return function(scope, elem, attr) {
- if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) {
+ if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
+
+ if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nativeAriaNodeNames)) {
elem.attr('tabindex', 0);
}
};
diff --git a/xstatic/pkg/angular/data/angular-cookies.js b/xstatic/pkg/angular/data/angular-cookies.js
index a03ff49..a1dac51 100644
--- a/xstatic/pkg/angular/data/angular-cookies.js
+++ b/xstatic/pkg/angular/data/angular-cookies.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -10,25 +10,21 @@
* @name ngCookies
* @description
*
- * # ngCookies
- *
* The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies.
*
- *
- * <div doc-module-components="ngCookies"></div>
- *
* See {@link ngCookies.$cookies `$cookies`} for usage.
*/
angular.module('ngCookies', ['ng']).
+ info({ angularVersion: '"1.8.2"' }).
/**
* @ngdoc provider
* @name $cookiesProvider
* @description
* Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service.
* */
- provider('$cookies', [function $CookiesProvider() {
+ provider('$cookies', [/** @this */function $CookiesProvider() {
/**
* @ngdoc property
* @name $cookiesProvider#defaults
@@ -47,10 +43,24 @@ angular.module('ngCookies', ['ng']).
* or a Date object indicating the exact date/time this cookie will expire.
* - **secure** - `{boolean}` - If `true`, then the cookie will only be available through a
* secured connection.
+ * - **samesite** - `{string}` - prevents the browser from sending the cookie along with cross-site requests.
+ * Accepts the values `lax` and `strict`. See the [OWASP Wiki](https://www.owasp.org/index.php/SameSite)
+ * for more info. Note that as of May 2018, not all browsers support `SameSite`,
+ * so it cannot be used as a single measure against Cross-Site-Request-Forgery (CSRF) attacks.
*
* Note: By default, the address that appears in your `<base>` tag will be used as the path.
* This is important so that cookies will be visible for all routes when html5mode is enabled.
*
+ * @example
+ *
+ * ```js
+ * angular.module('cookiesProviderExample', ['ngCookies'])
+ * .config(['$cookiesProvider', function($cookiesProvider) {
+ * // Setting default options
+ * $cookiesProvider.defaults.domain = 'foo.com';
+ * $cookiesProvider.defaults.secure = true;
+ * }]);
+ * ```
**/
var defaults = this.defaults = {};
@@ -66,7 +76,7 @@ angular.module('ngCookies', ['ng']).
* Provides read/write access to browser's cookies.
*
* <div class="alert alert-info">
- * Up until Angular 1.3, `$cookies` exposed properties that represented the
+ * Up until AngularJS 1.3, `$cookies` exposed properties that represented the
* current browser cookie values. In version 1.4, this behavior has changed, and
* `$cookies` now provides a standard api of getters, setters etc.
* </div>
@@ -179,86 +189,6 @@ angular.module('ngCookies', ['ng']).
}];
}]);
-angular.module('ngCookies').
-/**
- * @ngdoc service
- * @name $cookieStore
- * @deprecated
- * @requires $cookies
- *
- * @description
- * Provides a key-value (string-object) storage, that is backed by session cookies.
- * Objects put or retrieved from this storage are automatically serialized or
- * deserialized by angular's toJson/fromJson.
- *
- * Requires the {@link ngCookies `ngCookies`} module to be installed.
- *
- * <div class="alert alert-danger">
- * **Note:** The $cookieStore service is **deprecated**.
- * Please use the {@link ngCookies.$cookies `$cookies`} service instead.
- * </div>
- *
- * @example
- *
- * ```js
- * angular.module('cookieStoreExample', ['ngCookies'])
- * .controller('ExampleController', ['$cookieStore', function($cookieStore) {
- * // Put cookie
- * $cookieStore.put('myFavorite','oatmeal');
- * // Get cookie
- * var favoriteCookie = $cookieStore.get('myFavorite');
- * // Removing a cookie
- * $cookieStore.remove('myFavorite');
- * }]);
- * ```
- */
- factory('$cookieStore', ['$cookies', function($cookies) {
-
- return {
- /**
- * @ngdoc method
- * @name $cookieStore#get
- *
- * @description
- * Returns the value of given cookie key
- *
- * @param {string} key Id to use for lookup.
- * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist.
- */
- get: function(key) {
- return $cookies.getObject(key);
- },
-
- /**
- * @ngdoc method
- * @name $cookieStore#put
- *
- * @description
- * Sets a value for given cookie key
- *
- * @param {string} key Id for the `value`.
- * @param {Object} value Value to be stored.
- */
- put: function(key, value) {
- $cookies.putObject(key, value);
- },
-
- /**
- * @ngdoc method
- * @name $cookieStore#remove
- *
- * @description
- * Remove given cookie
- *
- * @param {string} key Id of the key-value pair to delete.
- */
- remove: function(key) {
- $cookies.remove(key);
- }
- };
-
- }]);
-
/**
* @name $$cookieWriter
* @requires $document
@@ -292,6 +222,7 @@ function $$CookieWriter($document, $log, $browser) {
str += options.domain ? ';domain=' + options.domain : '';
str += expires ? ';expires=' + expires.toUTCString() : '';
str += options.secure ? ';secure' : '';
+ str += options.samesite ? ';samesite=' + options.samesite : '';
// per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
// - 300 cookies
@@ -299,9 +230,9 @@ function $$CookieWriter($document, $log, $browser) {
// - 4096 bytes per cookie
var cookieLength = str.length + 1;
if (cookieLength > 4096) {
- $log.warn("Cookie '" + name +
- "' possibly not set or overflowed because it was too large (" +
- cookieLength + " > 4096 bytes)!");
+ $log.warn('Cookie \'' + name +
+ '\' possibly not set or overflowed because it was too large (' +
+ cookieLength + ' > 4096 bytes)!');
}
return str;
@@ -314,7 +245,7 @@ function $$CookieWriter($document, $log, $browser) {
$$CookieWriter.$inject = ['$document', '$log', '$browser'];
-angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() {
+angular.module('ngCookies').provider('$$cookieWriter', /** @this */ function $$CookieWriterProvider() {
this.$get = $$CookieWriter;
});
diff --git a/xstatic/pkg/angular/data/angular-loader.js b/xstatic/pkg/angular/data/angular-loader.js
index b005220..77111d5 100644
--- a/xstatic/pkg/angular/data/angular-loader.js
+++ b/xstatic/pkg/angular/data/angular-loader.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
@@ -9,9 +9,17 @@
/* global toDebugString: true */
-function serializeObject(obj) {
+function serializeObject(obj, maxDepth) {
var seen = [];
+ // There is no direct way to stringify object until reaching a specific depth
+ // and a very deep object can cause a performance issue, so we copy the object
+ // based on this specific depth and then stringify it.
+ if (isValidObjectMaxDepth(maxDepth)) {
+ // This file is also included in `angular-loader`, so `copy()` might not always be available in
+ // the closure. Therefore, it is lazily retrieved as `angular.copy()` when needed.
+ obj = angular.copy(obj, null, maxDepth);
+ }
return JSON.stringify(obj, function(key, val) {
val = toJsonReplacer(key, val);
if (isObject(val)) {
@@ -24,13 +32,13 @@ function serializeObject(obj) {
});
}
-function toDebugString(obj) {
+function toDebugString(obj, maxDepth) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (isUndefined(obj)) {
return 'undefined';
} else if (typeof obj !== 'string') {
- return serializeObject(obj);
+ return serializeObject(obj, maxDepth);
}
return obj;
}
@@ -39,7 +47,7 @@ function toDebugString(obj) {
* @description
*
* This object provides a utility for producing rich Error messages within
- * Angular. It can be called as follows:
+ * AngularJS. It can be called as follows:
*
* var exampleMinErr = minErr('example');
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
@@ -56,7 +64,7 @@ function toDebugString(obj) {
* Since data will be parsed statically during a build step, some restrictions
* are applied with respect to how minErr instances are created and called.
* Instances should have names of the form namespaceMinErr for a minErr created
- * using minErr('namespace') . Error codes, namespaces and template strings
+ * using minErr('namespace'). Error codes, namespaces and template strings
* should all be static strings, not variables or general expressions.
*
* @param {string} module The namespace to use for the new minErr instance.
@@ -67,32 +75,41 @@ function toDebugString(obj) {
function minErr(module, ErrorConstructor) {
ErrorConstructor = ErrorConstructor || Error;
- return function() {
- var SKIP_INDEXES = 2;
- var templateArgs = arguments,
- code = templateArgs[0],
+ var url = 'https://errors.angularjs.org/"1.8.2"/';
+ var regex = url.replace('.', '\\.') + '[\\s\\S]*';
+ var errRegExp = new RegExp(regex, 'g');
+
+ return function() {
+ var code = arguments[0],
+ template = arguments[1],
message = '[' + (module ? module + ':' : '') + code + '] ',
- template = templateArgs[1],
+ templateArgs = sliceArgs(arguments, 2).map(function(arg) {
+ return toDebugString(arg, minErrConfig.objectMaxDepth);
+ }),
paramPrefix, i;
+ // A minErr message has two parts: the message itself and the url that contains the
+ // encoded message.
+ // The message's parameters can contain other error messages which also include error urls.
+ // To prevent the messages from getting too long, we strip the error urls from the parameters.
+
message += template.replace(/\{\d+\}/g, function(match) {
- var index = +match.slice(1, -1),
- shiftedIndex = index + SKIP_INDEXES;
+ var index = +match.slice(1, -1);
- if (shiftedIndex < templateArgs.length) {
- return toDebugString(templateArgs[shiftedIndex]);
+ if (index < templateArgs.length) {
+ return templateArgs[index].replace(errRegExp, '');
}
return match;
});
- message += '\nhttp://errors.angularjs.org/1.5.8/' +
- (module ? module + '/' : '') + code;
+ message += '\n' + url + (module ? module + '/' : '') + code;
- for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
- message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
- encodeURIComponent(toDebugString(templateArgs[i]));
+ if (minErrConfig.urlErrorParamsEnabled) {
+ for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
+ message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
+ }
}
return new ErrorConstructor(message);
@@ -105,7 +122,7 @@ function minErr(module, ErrorConstructor) {
* @module ng
* @description
*
- * Interface for configuring angular {@link angular.module modules}.
+ * Interface for configuring AngularJS {@link angular.module modules}.
*/
function setupModuleLoader(window) {
@@ -132,9 +149,9 @@ function setupModuleLoader(window) {
* @module ng
* @description
*
- * The `angular.module` is a global place for creating, registering and retrieving Angular
+ * The `angular.module` is a global place for creating, registering and retrieving AngularJS
* modules.
- * All modules (angular core or 3rd party) that should be available to an application must be
+ * All modules (AngularJS core or 3rd party) that should be available to an application must be
* registered using this mechanism.
*
* Passing one argument retrieves an existing {@link angular.Module},
@@ -178,6 +195,9 @@ function setupModuleLoader(window) {
* @returns {angular.Module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
+
+ var info = {};
+
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
@@ -190,9 +210,9 @@ function setupModuleLoader(window) {
}
return ensure(modules, name, function() {
if (!requires) {
- throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
- "the module name or forgot to load it. If registering a module ensure that you " +
- "specify the dependencies as the second argument.", name);
+ throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' +
+ 'the module name or forgot to load it. If registering a module ensure that you ' +
+ 'specify the dependencies as the second argument.', name);
}
/** @type {!Array.<Array.<*>>} */
@@ -214,6 +234,45 @@ function setupModuleLoader(window) {
_runBlocks: runBlocks,
/**
+ * @ngdoc method
+ * @name angular.Module#info
+ * @module ng
+ *
+ * @param {Object=} info Information about the module
+ * @returns {Object|Module} The current info object for this module if called as a getter,
+ * or `this` if called as a setter.
+ *
+ * @description
+ * Read and write custom information about this module.
+ * For example you could put the version of the module in here.
+ *
+ * ```js
+ * angular.module('myModule', []).info({ version: '1.0.0' });
+ * ```
+ *
+ * The version could then be read back out by accessing the module elsewhere:
+ *
+ * ```
+ * var version = angular.module('myModule').info().version;
+ * ```
+ *
+ * You can also retrieve this information during runtime via the
+ * {@link $injector#modules `$injector.modules`} property:
+ *
+ * ```js
+ * var version = $injector.modules['myModule'].info().version;
+ * ```
+ */
+ info: function(value) {
+ if (isDefined(value)) {
+ if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value');
+ info = value;
+ return this;
+ }
+ return info;
+ },
+
+ /**
* @ngdoc property
* @name angular.Module#requires
* @module ng
@@ -302,7 +361,7 @@ function setupModuleLoader(window) {
* @description
* See {@link auto.$provide#decorator $provide.decorator()}.
*/
- decorator: invokeLaterAndSetModuleName('$provide', 'decorator'),
+ decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks),
/**
* @ngdoc method
@@ -342,13 +401,13 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#filter
* @module ng
- * @param {string} name Filter name - this must be a valid angular expression identifier
+ * @param {string} name Filter name - this must be a valid AngularJS expression identifier
* @param {Function} filterFactory Factory function for creating new instance of filter.
* @description
* See {@link ng.$filterProvider#register $filterProvider.register()}.
*
* <div class="alert alert-warning">
- * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
* Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
@@ -385,7 +444,8 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#component
* @module ng
- * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
+ * @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`),
+ * or an object map of components where the keys are the names and the values are the component definition objects.
* @param {Object} options Component definition object (a simplified
* {@link ng.$compile#directive-definition-object directive definition object})
*
@@ -401,7 +461,13 @@ function setupModuleLoader(window) {
* @param {Function} configFn Execute this function on module load. Useful for service
* configuration.
* @description
- * Use this method to register work which needs to be performed on module loading.
+ * Use this method to configure services by injecting their
+ * {@link angular.Module#provider `providers`}, e.g. for adding routes to the
+ * {@link ngRoute.$routeProvider $routeProvider}.
+ *
+ * Note that you can only inject {@link angular.Module#provider `providers`} and
+ * {@link angular.Module#constant `constants`} into this function.
+ *
* For more about how to configure services, see
* {@link providers#provider-recipe Provider Recipe}.
*/
@@ -448,10 +514,11 @@ function setupModuleLoader(window) {
* @param {string} method
* @returns {angular.Module}
*/
- function invokeLaterAndSetModuleName(provider, method) {
+ function invokeLaterAndSetModuleName(provider, method, queue) {
+ if (!queue) queue = invokeQueue;
return function(recipeName, factoryFunction) {
if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
- invokeQueue.push([provider, method, arguments]);
+ queue.push([provider, method, arguments]);
return moduleInstance;
};
}
@@ -481,4 +548,3 @@ setupModuleLoader(window);
* } }
*/
angular.Module;
-
diff --git a/xstatic/pkg/angular/data/angular-message-format.js b/xstatic/pkg/angular/data/angular-message-format.js
index f128916..09ccb57 100644
--- a/xstatic/pkg/angular/data/angular-message-format.js
+++ b/xstatic/pkg/angular/data/angular-message-format.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -13,22 +13,14 @@
/* global isFunction: false */
/* global noop: false */
/* global toJson: false */
-
-function stringify(value) {
- if (value == null /* null/undefined */) { return ''; }
- switch (typeof value) {
- case 'string': return value;
- case 'number': return '' + value;
- default: return toJson(value);
- }
-}
+/* global $$stringify: false */
// Convert an index into the string into line/column for use in error messages
// As such, this doesn't have to be efficient.
function indexToLineAndColumn(text, index) {
var lines = text.split(/\n/g);
- for (var i=0; i < lines.length; i++) {
- var line=lines[i];
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i];
if (index >= line.length) {
index -= line.length;
} else {
@@ -47,7 +39,7 @@ function parseTextLiteral(text) {
parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) {
var unwatch = scope['$watch'](noop,
function textLiteralWatcher() {
- if (isFunction(listener)) { listener.call(null, text, text, scope); }
+ listener(text, text, scope);
unwatch();
},
objectEquality);
@@ -64,14 +56,14 @@ function subtractOffset(expressionFn, offset) {
return expressionFn;
}
function minusOffset(value) {
- return (value == void 0) ? value : value - offset;
+ return (value == null) ? value : value - offset;
}
function parsedFn(context) { return minusOffset(expressionFn(context)); }
var unwatch;
parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) {
unwatch = scope['$watch'](expressionFn,
function pluralExpressionWatchListener(newValue, oldValue) {
- if (isFunction(listener)) { listener.call(null, minusOffset(newValue), minusOffset(oldValue), scope); }
+ listener(minusOffset(newValue), minusOffset(oldValue), scope);
},
objectEquality);
return unwatch;
@@ -96,7 +88,7 @@ function MessageSelectorBase(expressionFn, choices) {
var self = this;
this.expressionFn = expressionFn;
this.choices = choices;
- if (choices["other"] === void 0) {
+ if (choices['other'] === undefined) {
throw $interpolateMinErr('reqother', '“other” is a required option.');
}
this.parsedFn = function(context) { return self.getResult(context); };
@@ -130,7 +122,7 @@ function MessageSelectorWatchers(msgSelector, scope, listener, objectEquality) {
this.msgSelector = msgSelector;
this.listener = listener;
this.objectEquality = objectEquality;
- this.lastMessage = void 0;
+ this.lastMessage = undefined;
this.messageFnWatcher = noop;
var expressionFnListener = function(newValue, oldValue) { return self.expressionFnListener(newValue, oldValue); };
this.expressionFnWatcher = scope['$watch'](msgSelector.expressionFn, expressionFnListener, objectEquality);
@@ -145,9 +137,7 @@ MessageSelectorWatchers.prototype.expressionFnListener = function expressionFnLi
};
MessageSelectorWatchers.prototype.messageFnListener = function messageFnListener(newMessage, oldMessage) {
- if (isFunction(this.listener)) {
- this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope);
- }
+ this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope);
this.lastMessage = newMessage;
};
@@ -170,7 +160,7 @@ SelectMessageProto.prototype = MessageSelectorBase.prototype;
SelectMessage.prototype = new SelectMessageProto();
SelectMessage.prototype.categorizeValue = function categorizeSelectValue(value) {
- return (this.choices[value] !== void 0) ? value : "other";
+ return (this.choices[value] !== undefined) ? value : 'other';
};
/**
@@ -190,12 +180,12 @@ PluralMessageProto.prototype = MessageSelectorBase.prototype;
PluralMessage.prototype = new PluralMessageProto();
PluralMessage.prototype.categorizeValue = function categorizePluralValue(value) {
if (isNaN(value)) {
- return "other";
- } else if (this.choices[value] !== void 0) {
+ return 'other';
+ } else if (this.choices[value] !== undefined) {
return value;
} else {
var category = this.pluralCat(value - this.offset);
- return (this.choices[category] !== void 0) ? category : "other";
+ return (this.choices[category] !== undefined) ? category : 'other';
}
};
@@ -264,7 +254,7 @@ InterpolationParts.prototype.getExpressionValues = function getExpressionValues(
InterpolationParts.prototype.getResult = function getResult(expressionValues) {
for (var i = 0; i < this.expressionIndices.length; i++) {
var expressionValue = expressionValues[i];
- if (this.allOrNothing && expressionValue === void 0) return;
+ if (this.allOrNothing && expressionValue === undefined) return;
this.textParts[this.expressionIndices[i]] = expressionValue;
}
return this.textParts.join('');
@@ -275,7 +265,7 @@ InterpolationParts.prototype.toParsedFn = function toParsedFn(mustHaveExpression
var self = this;
this.flushPartialText();
if (mustHaveExpression && this.expressionFns.length === 0) {
- return void 0;
+ return undefined;
}
if (this.textParts.length === 0) {
return parseTextLiteral('');
@@ -284,7 +274,7 @@ InterpolationParts.prototype.toParsedFn = function toParsedFn(mustHaveExpression
$interpolateMinErr['throwNoconcat'](originalText);
}
if (this.expressionFns.length === 0) {
- if (this.textParts.length != 1) { this.errorInParseLogic(); }
+ if (this.textParts.length !== 1) { this.errorInParseLogic(); }
return parseTextLiteral(this.textParts[0]);
}
var parsedFn = function(context) {
@@ -311,7 +301,7 @@ InterpolationParts.prototype.watchDelegate = function watchDelegate(scope, liste
function InterpolationPartsWatcher(interpolationParts, scope, listener, objectEquality) {
this.interpolationParts = interpolationParts;
this.scope = scope;
- this.previousResult = (void 0);
+ this.previousResult = (undefined);
this.listener = listener;
var self = this;
this.expressionFnsWatcher = scope['$watchGroup'](interpolationParts.expressionFns, function(newExpressionValues, oldExpressionValues) {
@@ -321,9 +311,7 @@ function InterpolationPartsWatcher(interpolationParts, scope, listener, objectEq
InterpolationPartsWatcher.prototype.watchListener = function watchListener(newExpressionValues, oldExpressionValues) {
var result = this.interpolationParts.getResult(newExpressionValues);
- if (isFunction(this.listener)) {
- this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope);
- }
+ this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope);
this.previousResult = result;
};
@@ -423,7 +411,7 @@ MessageFormatParser.prototype.popState = function popState() {
MessageFormatParser.prototype.matchRe = function matchRe(re, search) {
re.lastIndex = this.index;
var match = re.exec(this.text);
- if (match != null && (search === true || (match.index == this.index))) {
+ if (match != null && (search === true || (match.index === this.index))) {
this.index = re.lastIndex;
return match;
}
@@ -461,7 +449,7 @@ MessageFormatParser.prototype.errorInParseLogic = function errorInParseLogic() {
};
MessageFormatParser.prototype.assertRuleOrNull = function assertRuleOrNull(rule) {
- if (rule === void 0) {
+ if (rule === undefined) {
this.errorInParseLogic();
}
};
@@ -477,7 +465,7 @@ MessageFormatParser.prototype.errorExpecting = function errorExpecting() {
position.line, position.column, this.text);
}
var word = match[1];
- if (word == "select" || word == "plural") {
+ if (word === 'select' || word === 'plural') {
position = indexToLineAndColumn(this.text, this.index);
throw $interpolateMinErr('reqcomma',
'Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”',
@@ -505,7 +493,7 @@ MessageFormatParser.prototype.ruleString = function ruleString() {
MessageFormatParser.prototype.startStringAtMatch = function startStringAtMatch(match) {
this.stringStartIndex = match.index;
this.stringQuote = match[0];
- this.stringInterestsRe = this.stringQuote == "'" ? SQUOTED_STRING_INTEREST_RE : DQUOTED_STRING_INTEREST_RE;
+ this.stringInterestsRe = this.stringQuote === '\'' ? SQUOTED_STRING_INTEREST_RE : DQUOTED_STRING_INTEREST_RE;
this.rule = this.ruleInsideString;
};
@@ -519,8 +507,7 @@ MessageFormatParser.prototype.ruleInsideString = function ruleInsideString() {
'The string beginning at line {0}, column {1} is unterminated in text “{2}”',
position.line, position.column, this.text);
}
- var chars = match[0];
- if (match == this.stringQuote) {
+ if (match[0] === this.stringQuote) {
this.rule = null;
}
};
@@ -533,8 +520,8 @@ MessageFormatParser.prototype.rulePluralOrSelect = function rulePluralOrSelect()
}
var argType = match[1];
switch (argType) {
- case "plural": this.rule = this.rulePluralStyle; break;
- case "select": this.rule = this.ruleSelectStyle; break;
+ case 'plural': this.rule = this.rulePluralStyle; break;
+ case 'select': this.rule = this.ruleSelectStyle; break;
default: this.errorInParseLogic();
}
};
@@ -552,7 +539,7 @@ MessageFormatParser.prototype.ruleSelectStyle = function ruleSelectStyle() {
};
var NUMBER_RE = /[0]|(?:[1-9][0-9]*)/g;
-var PLURAL_OFFSET_RE = new RegExp("\\s*offset\\s*:\\s*(" + NUMBER_RE.source + ")", "g");
+var PLURAL_OFFSET_RE = new RegExp('\\s*offset\\s*:\\s*(' + NUMBER_RE.source + ')', 'g');
MessageFormatParser.prototype.rulePluralOffset = function rulePluralOffset() {
var match = this.matchRe(PLURAL_OFFSET_RE);
@@ -562,7 +549,7 @@ MessageFormatParser.prototype.rulePluralOffset = function rulePluralOffset() {
};
MessageFormatParser.prototype.assertChoiceKeyIsNew = function assertChoiceKeyIsNew(choiceKey, index) {
- if (this.choices[choiceKey] !== void 0) {
+ if (this.choices[choiceKey] !== undefined) {
var position = indexToLineAndColumn(this.text, index);
throw $interpolateMinErr('dupvalue',
'The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”',
@@ -583,7 +570,7 @@ MessageFormatParser.prototype.ruleSelectKeyword = function ruleSelectKeyword() {
this.rule = this.ruleMessageText;
};
-var EXPLICIT_VALUE_OR_KEYWORD_RE = new RegExp("\\s*(?:(?:=(" + NUMBER_RE.source + "))|(\\w+))", "g");
+var EXPLICIT_VALUE_OR_KEYWORD_RE = new RegExp('\\s*(?:(?:=(' + NUMBER_RE.source + '))|(\\w+))', 'g');
MessageFormatParser.prototype.rulePluralValueOrKeyword = function rulePluralValueOrKeyword() {
var match = this.matchRe(EXPLICIT_VALUE_OR_KEYWORD_RE);
if (match == null) {
@@ -600,7 +587,7 @@ MessageFormatParser.prototype.rulePluralValueOrKeyword = function rulePluralValu
this.rule = this.ruleMessageText;
};
-var BRACE_OPEN_RE = /\s*{/g;
+var BRACE_OPEN_RE = /\s*\{/g;
var BRACE_CLOSE_RE = /}/g;
MessageFormatParser.prototype.ruleMessageText = function ruleMessageText() {
if (!this.consumeRe(BRACE_OPEN_RE)) {
@@ -620,7 +607,7 @@ var INTERP_OR_END_MESSAGE_RE = /\\.|{{|}/g;
var INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE = /\\.|{{|#|}/g;
var ESCAPE_OR_MUSTACHE_BEGIN_RE = /\\.|{{/g;
MessageFormatParser.prototype.advanceInInterpolationOrMessageText = function advanceInInterpolationOrMessageText() {
- var currentIndex = this.index, match, re;
+ var currentIndex = this.index, match;
if (this.ruleChoiceKeyword == null) { // interpolation
match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE);
if (match == null) { // End of interpolation text. Nothing more to process.
@@ -629,7 +616,7 @@ MessageFormatParser.prototype.advanceInInterpolationOrMessageText = function adv
return null;
}
} else {
- match = this.searchRe(this.ruleChoiceKeyword == this.rulePluralValueOrKeyword ?
+ match = this.searchRe(this.ruleChoiceKeyword === this.rulePluralValueOrKeyword ?
INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE : INTERP_OR_END_MESSAGE_RE);
if (match == null) {
var position = indexToLineAndColumn(this.text, this.msgStartIndex);
@@ -654,20 +641,20 @@ MessageFormatParser.prototype.ruleInInterpolationOrMessageText = function ruleIn
this.rule = null;
return;
}
- if (token[0] == "\\") {
+ if (token[0] === '\\') {
// unescape next character and continue
this.interpolationParts.addText(this.textPart + token[1]);
return;
}
this.interpolationParts.addText(this.textPart);
- if (token == "{{") {
+ if (token === '{{') {
this.pushState();
this.ruleStack.push(this.ruleEndMustacheInInterpolationOrMessage);
this.rule = this.ruleEnteredMustache;
- } else if (token == "}") {
+ } else if (token === '}') {
this.choices[this.choiceKey] = this.interpolationParts.toParsedFn(/*mustHaveExpression=*/false, this.text);
this.rule = this.ruleChoiceKeyword;
- } else if (token == "#") {
+ } else if (token === '#') {
this.interpolationParts.addExpressionFn(this.expressionMinusOffsetFn);
} else {
this.errorInParseLogic();
@@ -691,7 +678,7 @@ MessageFormatParser.prototype.ruleInInterpolation = function ruleInInterpolation
return;
}
var token = match[0];
- if (token[0] == "\\") {
+ if (token[0] === '\\') {
// unescape next character and continue
this.interpolationParts.addText(this.text.substring(currentIndex, match.index) + token[1]);
return;
@@ -738,7 +725,7 @@ MessageFormatParser.prototype.ruleEndMustache = function ruleEndMustache() {
// day), then the result *has* to be a string and those rules would have already set
// this.parsedFn. If there was no MessageFormat extension, then there is no requirement to
// stringify the result and parsedFn isn't set. We set it here. While we could have set it
- // unconditionally when exiting the Angular expression, I intend for us to not just replace
+ // unconditionally when exiting the AngularJS expression, I intend for us to not just replace
// $interpolate, but also to replace $parse in a future version (so ng-bind can work), and in
// such a case we do not want to unnecessarily stringify something if it's not going to be used
// in a string context.
@@ -757,18 +744,18 @@ MessageFormatParser.prototype.ruleAngularExpression = function ruleAngularExpres
function getEndOperator(opBegin) {
switch (opBegin) {
- case "{": return "}";
- case "[": return "]";
- case "(": return ")";
+ case '{': return '}';
+ case '[': return ']';
+ case '(': return ')';
default: return null;
}
}
function getBeginOperator(opEnd) {
switch (opEnd) {
- case "}": return "{";
- case "]": return "[";
- case ")": return "(";
+ case '}': return '{';
+ case ']': return '[';
+ case ')': return '(';
default: return null;
}
}
@@ -778,12 +765,11 @@ function getBeginOperator(opEnd) {
// should support any other type of start/end interpolation symbol.
var INTERESTING_OPERATORS_RE = /[[\]{}()'",]/g;
MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularExpression() {
- var startIndex = this.index;
var match = this.searchRe(INTERESTING_OPERATORS_RE);
var position;
if (match == null) {
if (this.angularOperatorStack.length === 0) {
- // This is the end of the Angular expression so this is actually a
+ // This is the end of the AngularJS expression so this is actually a
// success. Note that when inside an interpolation, this means we even
// consumed the closing interpolation symbols if they were curlies. This
// is NOT an error at this point but will become an error further up the
@@ -799,16 +785,16 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
}
var innermostOperator = this.angularOperatorStack[0];
throw $interpolateMinErr('badexpr',
- 'Unexpected end of Angular expression. Expecting operator “{0}” at the end of the text “{1}”',
+ 'Unexpected end of AngularJS expression. Expecting operator “{0}” at the end of the text “{1}”',
this.getEndOperator(innermostOperator), this.text);
}
var operator = match[0];
- if (operator == "'" || operator == '"') {
+ if (operator === '\'' || operator === '"') {
this.ruleStack.push(this.ruleInAngularExpression);
this.startStringAtMatch(match);
return;
}
- if (operator == ",") {
+ if (operator === ',') {
if (this.trustedContext) {
position = indexToLineAndColumn(this.text, this.index);
throw $interpolateMinErr('unsafe',
@@ -836,7 +822,7 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
this.errorInParseLogic();
}
if (this.angularOperatorStack.length > 0) {
- if (beginOperator == this.angularOperatorStack[0]) {
+ if (beginOperator === this.angularOperatorStack[0]) {
this.angularOperatorStack.shift();
return;
}
@@ -864,7 +850,6 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
/* global noop: true */
/* global toJson: true */
/* global MessageFormatParser: false */
-/* global stringify: false */
/**
* @ngdoc module
@@ -875,7 +860,7 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
*
* ## What is ngMessageFormat?
*
- * The ngMessageFormat module extends the Angular {@link ng.$interpolate `$interpolate`} service
+ * The ngMessageFormat module extends the AngularJS {@link ng.$interpolate `$interpolate`} service
* with a syntax for handling pluralization and gender specific messages, which is based on the
* [ICU MessageFormat syntax][ICU].
*
@@ -909,9 +894,9 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
* this.gender = gender;
* }
*
- * var alice = new Person("Alice", "female"),
- * bob = new Person("Bob", "male"),
- * ashley = new Person("Ashley", "");
+ * var alice = new Person('Alice', 'female'),
+ * bob = new Person('Bob', 'male'),
+ * ashley = new Person('Ashley', '');
*
* angular.module('msgFmtExample', ['ngMessageFormat'])
* .controller('AppController', ['$scope', function($scope) {
@@ -952,11 +937,11 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
* this.gender = gender;
* }
*
- * var alice = new Person("Alice", "female"),
- * bob = new Person("Bob", "male"),
- * sarah = new Person("Sarah", "female"),
- * harry = new Person("Harry Potter", "male"),
- * ashley = new Person("Ashley", "");
+ * var alice = new Person('Alice', 'female'),
+ * bob = new Person('Bob', 'male'),
+ * sarah = new Person('Sarah', 'female'),
+ * harry = new Person('Harry Potter', 'male'),
+ * ashley = new Person('Ashley', '');
*
* angular.module('msgFmtExample', ['ngMessageFormat'])
* .controller('AppController', ['$scope', function($scope) {
@@ -1012,10 +997,10 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
* this.gender = gender;
* }
*
- * var alice = new Person("Alice", "female"),
- * bob = new Person("Bob", "male"),
- * harry = new Person("Harry Potter", "male"),
- * ashley = new Person("Ashley", "");
+ * var alice = new Person('Alice', 'female'),
+ * bob = new Person('Bob', 'male'),
+ * harry = new Person('Harry Potter', 'male'),
+ * ashley = new Person('Ashley', '');
*
* angular.module('msgFmtExample', ['ngMessageFormat'])
* .controller('AppController', ['$scope', function($scope) {
@@ -1028,13 +1013,13 @@ MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularEx
*/
var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler', function $$messageFormat(
- $parse, $locale, $sce, $exceptionHandler) {
+ $parse, $locale, $sce, $exceptionHandler) {
function getStringifier(trustedContext, allOrNothing, text) {
return function stringifier(value) {
try {
value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value);
- return allOrNothing && (value === void 0) ? value : stringify(value);
+ return allOrNothing && (value === undefined) ? value : $$stringify(value);
} catch (err) {
$exceptionHandler($interpolateMinErr['interr'](text, err));
}
@@ -1055,7 +1040,7 @@ var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler',
}];
var $$interpolateDecorator = ['$$messageFormat', '$delegate', function $$interpolateDecorator($$messageFormat, $interpolate) {
- if ($interpolate['startSymbol']() != "{{" || $interpolate['endSymbol']() != "}}") {
+ if ($interpolate['startSymbol']() !== '{{' || $interpolate['endSymbol']() !== '}}') {
throw $interpolateMinErr('nochgmustache', 'angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.');
}
var interpolate = $$messageFormat['interpolate'];
@@ -1068,14 +1053,17 @@ var $interpolateMinErr;
var isFunction;
var noop;
var toJson;
+var $$stringify;
-var module = window['angular']['module']('ngMessageFormat', ['ng']);
-module['factory']('$$messageFormat', $$MessageFormatFactory);
-module['config'](['$provide', function($provide) {
+var ngModule = window['angular']['module']('ngMessageFormat', ['ng']);
+ngModule['info']({ 'angularVersion': '"1.8.2"' });
+ngModule['factory']('$$messageFormat', $$MessageFormatFactory);
+ngModule['config'](['$provide', function($provide) {
$interpolateMinErr = window['angular']['$interpolateMinErr'];
isFunction = window['angular']['isFunction'];
noop = window['angular']['noop'];
toJson = window['angular']['toJson'];
+ $$stringify = window['angular']['$$stringify'];
$provide['decorator']('$interpolate', $$interpolateDecorator);
}]);
diff --git a/xstatic/pkg/angular/data/angular-messages.js b/xstatic/pkg/angular/data/angular-messages.js
index 9322b53..72a6d86 100644
--- a/xstatic/pkg/angular/data/angular-messages.js
+++ b/xstatic/pkg/angular/data/angular-messages.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -23,9 +23,9 @@ var jqLite;
* sequencing based on the order of how the messages are defined in the template.
*
* Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude`
- * `ngMessage` and `ngMessageExp` directives.
+ * `ngMessage`, `ngMessageExp` and `ngMessageDefault` directives.
*
- * # Usage
+ * ## Usage
* The `ngMessages` directive allows keys in a key/value collection to be associated with a child element
* (or 'message') that will show or hide based on the truthiness of that key's value in the collection. A common use
* case for `ngMessages` is to display error messages for inputs using the `$error` object exposed by the
@@ -69,7 +69,7 @@ var jqLite;
* By default, `ngMessages` will only display one message for a particular key/value collection at any time. If more
* than one message (or error) key is currently true, then which message is shown is determined by the order of messages
* in the HTML template code (messages declared first are prioritised). This mechanism means the developer does not have
- * to prioritise messages using custom JavaScript code.
+ * to prioritize messages using custom JavaScript code.
*
* Given the following error object for our example (which informs us that the field `myField` currently has both the
* `required` and `email` errors):
@@ -200,7 +200,7 @@ var jqLite;
*
* Feel free to use other structural directives such as ng-if and ng-switch to further control
* what messages are active and when. Be careful, if you place ng-message on the same element
- * as these structural directives, Angular may not be able to determine if a message is active
+ * as these structural directives, AngularJS may not be able to determine if a message is active
* or not. Therefore it is best to place the ng-message on a child element of the structural
* directive.
*
@@ -262,16 +262,36 @@ var jqLite;
* .some-message.ng-leave.ng-leave-active {}
* ```
*
- * {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
+ * {@link ngAnimate See the ngAnimate docs} to learn how to use JavaScript animations or to learn
+ * more about ngAnimate.
+ *
+ * ## Displaying a default message
+ * If the ngMessages renders no inner ngMessage directive (i.e. when none of the truthy
+ * keys are matched by a defined message), then it will render a default message
+ * using the {@link ngMessageDefault} directive.
+ * Note that matched messages will always take precedence over unmatched messages. That means
+ * the default message will not be displayed when another message is matched. This is also
+ * true for `ng-messages-multiple`.
+ *
+ * ```html
+ * <div ng-messages="myForm.myField.$error" role="alert">
+ * <div ng-message="required">This field is required</div>
+ * <div ng-message="minlength">This field is too short</div>
+ * <div ng-message-default>This field has an input error</div>
+ * </div>
+ * ```
+ *
+
*/
angular.module('ngMessages', [], function initAngularHelpers() {
- // Access helpers from angular core.
+ // Access helpers from AngularJS core.
// Do it inside a `config` block to ensure `window.angular` is available.
forEach = angular.forEach;
isArray = angular.isArray;
isString = angular.isString;
jqLite = angular.element;
})
+ .info({ angularVersion: '"1.8.2"' })
/**
* @ngdoc directive
@@ -290,8 +310,11 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* at a time and this depends on the prioritization of the messages within the template. (This can
* be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.)
*
- * A remote template can also be used to promote message reusability and messages can also be
- * overridden.
+ * A remote template can also be used (With {@link ngMessagesInclude}) to promote message
+ * reusability and messages can also be overridden.
+ *
+ * A default message can also be displayed when no `ngMessage` directive is inserted, using the
+ * {@link ngMessageDefault} directive.
*
* {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`.
*
@@ -302,6 +325,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <ANY ng-message="stringValue">...</ANY>
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
* <ANY ng-message-exp="expressionValue">...</ANY>
+ * <ANY ng-message-default>...</ANY>
* </ANY>
*
* <!-- or by using element directives -->
@@ -309,10 +333,11 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <ng-message when="stringValue">...</ng-message>
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
* <ng-message when-exp="expressionValue">...</ng-message>
+ * <ng-message-default>...</ng-message-default>
* </ng-messages>
* ```
*
- * @param {string} ngMessages an angular expression evaluating to a key/value object
+ * @param {string} ngMessages an AngularJS expression evaluating to a key/value object
* (this is typically the $error object on an ngModel instance).
* @param {string=} ngMessagesMultiple|multiple when set, all messages will be displayed with true
*
@@ -337,6 +362,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <div ng-message="required">You did not enter a field</div>
* <div ng-message="minlength">Your field is too short</div>
* <div ng-message="maxlength">Your field is too long</div>
+ * <div ng-message-default>This field has an input error</div>
* </div>
* </form>
* </file>
@@ -352,7 +378,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
return {
require: 'ngMessages',
restrict: 'AE',
- controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
+ controller: ['$element', '$scope', '$attrs', function NgMessagesCtrl($element, $scope, $attrs) {
var ctrl = this;
var latestKey = 0;
var nextAttachId = 0;
@@ -374,6 +400,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
var unmatchedMessages = [];
var matchedKeys = {};
+ var truthyKeys = 0;
var messageItem = ctrl.head;
var messageFound = false;
var totalMessages = 0;
@@ -386,13 +413,17 @@ angular.module('ngMessages', [], function initAngularHelpers() {
var messageUsed = false;
if (!messageFound) {
forEach(collection, function(value, key) {
- if (!messageUsed && truthy(value) && messageCtrl.test(key)) {
- // this is to prevent the same error name from showing up twice
- if (matchedKeys[key]) return;
- matchedKeys[key] = true;
+ if (truthy(value) && !messageUsed) {
+ truthyKeys++;
+
+ if (messageCtrl.test(key)) {
+ // this is to prevent the same error name from showing up twice
+ if (matchedKeys[key]) return;
+ matchedKeys[key] = true;
- messageUsed = true;
- messageCtrl.attach();
+ messageUsed = true;
+ messageCtrl.attach();
+ }
}
});
}
@@ -412,48 +443,60 @@ angular.module('ngMessages', [], function initAngularHelpers() {
messageCtrl.detach();
});
- unmatchedMessages.length !== totalMessages
- ? $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS)
- : $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
+ var messageMatched = unmatchedMessages.length !== totalMessages;
+ var attachDefault = ctrl.default && !messageMatched && truthyKeys > 0;
+
+ if (attachDefault) {
+ ctrl.default.attach();
+ } else if (ctrl.default) {
+ ctrl.default.detach();
+ }
+
+ if (messageMatched || attachDefault) {
+ $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
+ } else {
+ $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
+ }
};
$scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render);
- // If the element is destroyed, proactively destroy all the currently visible messages
- $element.on('$destroy', function() {
- forEach(messages, function(item) {
- item.message.detach();
- });
- });
-
this.reRender = function() {
if (!renderLater) {
renderLater = true;
$scope.$evalAsync(function() {
- if (renderLater) {
- cachedCollection && ctrl.render(cachedCollection);
+ if (renderLater && cachedCollection) {
+ ctrl.render(cachedCollection);
}
});
}
};
- this.register = function(comment, messageCtrl) {
- var nextKey = latestKey.toString();
- messages[nextKey] = {
- message: messageCtrl
- };
- insertMessageNode($element[0], comment, nextKey);
- comment.$$ngMessageNode = nextKey;
- latestKey++;
+ this.register = function(comment, messageCtrl, isDefault) {
+ if (isDefault) {
+ ctrl.default = messageCtrl;
+ } else {
+ var nextKey = latestKey.toString();
+ messages[nextKey] = {
+ message: messageCtrl
+ };
+ insertMessageNode($element[0], comment, nextKey);
+ comment.$$ngMessageNode = nextKey;
+ latestKey++;
+ }
ctrl.reRender();
};
- this.deregister = function(comment) {
- var key = comment.$$ngMessageNode;
- delete comment.$$ngMessageNode;
- removeMessageNode($element[0], comment, key);
- delete messages[key];
+ this.deregister = function(comment, isDefault) {
+ if (isDefault) {
+ delete ctrl.default;
+ } else {
+ var key = comment.$$ngMessageNode;
+ delete comment.$$ngMessageNode;
+ removeMessageNode($element[0], comment, key);
+ delete messages[key];
+ }
ctrl.reRender();
};
@@ -500,6 +543,9 @@ angular.module('ngMessages', [], function initAngularHelpers() {
function removeMessageNode(parent, comment, key) {
var messageNode = messages[key];
+ // This message node may have already been removed by a call to deregister()
+ if (!messageNode) return;
+
var match = findPreviousMessage(parent, comment);
if (match) {
match.next = messageNode.next;
@@ -594,6 +640,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* @name ngMessage
* @restrict AE
* @scope
+ * @priority 1
*
* @description
* `ngMessage` is a directive with the purpose to show and hide a particular message.
@@ -632,10 +679,8 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* @scope
*
* @description
- * `ngMessageExp` is a directive with the purpose to show and hide a particular message.
- * For `ngMessageExp` to operate, a parent `ngMessages` directive on a parent DOM element
- * must be situated since it determines which messages are visible based on the state
- * of the provided key/value map that `ngMessages` listens on.
+ * `ngMessageExp` is the same as {@link directive:ngMessage `ngMessage`}, but instead of a static
+ * value, it accepts an expression to be evaluated for the message key.
*
* @usage
* ```html
@@ -654,9 +699,41 @@ angular.module('ngMessages', [], function initAngularHelpers() {
*
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
*/
- .directive('ngMessageExp', ngMessageDirectiveFactory());
+ .directive('ngMessageExp', ngMessageDirectiveFactory())
-function ngMessageDirectiveFactory() {
+ /**
+ * @ngdoc directive
+ * @name ngMessageDefault
+ * @restrict AE
+ * @scope
+ *
+ * @description
+ * `ngMessageDefault` is a directive with the purpose to show and hide a default message for
+ * {@link directive:ngMessages}, when none of provided messages matches.
+ *
+ * More information about using `ngMessageDefault` can be found in the
+ * {@link module:ngMessages `ngMessages` module documentation}.
+ *
+ * @usage
+ * ```html
+ * <!-- using attribute directives -->
+ * <ANY ng-messages="expression" role="alert">
+ * <ANY ng-message="stringValue">...</ANY>
+ * <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
+ * <ANY ng-message-default>...</ANY>
+ * </ANY>
+ *
+ * <!-- or by using element directives -->
+ * <ng-messages for="expression" role="alert">
+ * <ng-message when="stringValue">...</ng-message>
+ * <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
+ * <ng-message-default>...</ng-message-default>
+ * </ng-messages>
+ *
+ */
+ .directive('ngMessageDefault', ngMessageDirectiveFactory(true));
+
+function ngMessageDirectiveFactory(isDefault) {
return ['$animate', function($animate) {
return {
restrict: 'AE',
@@ -665,25 +742,28 @@ function ngMessageDirectiveFactory() {
terminal: true,
require: '^^ngMessages',
link: function(scope, element, attrs, ngMessagesCtrl, $transclude) {
- var commentNode = element[0];
-
- var records;
- var staticExp = attrs.ngMessage || attrs.when;
- var dynamicExp = attrs.ngMessageExp || attrs.whenExp;
- var assignRecords = function(items) {
- records = items
- ? (isArray(items)
- ? items
- : items.split(/[\s,]+/))
- : null;
- ngMessagesCtrl.reRender();
- };
+ var commentNode, records, staticExp, dynamicExp;
+
+ if (!isDefault) {
+ commentNode = element[0];
+ staticExp = attrs.ngMessage || attrs.when;
+ dynamicExp = attrs.ngMessageExp || attrs.whenExp;
+
+ var assignRecords = function(items) {
+ records = items
+ ? (isArray(items)
+ ? items
+ : items.split(/[\s,]+/))
+ : null;
+ ngMessagesCtrl.reRender();
+ };
- if (dynamicExp) {
- assignRecords(scope.$eval(dynamicExp));
- scope.$watchCollection(dynamicExp, assignRecords);
- } else {
- assignRecords(staticExp);
+ if (dynamicExp) {
+ assignRecords(scope.$eval(dynamicExp));
+ scope.$watchCollection(dynamicExp, assignRecords);
+ } else {
+ assignRecords(staticExp);
+ }
}
var currentElement, messageCtrl;
@@ -705,8 +785,10 @@ function ngMessageDirectiveFactory() {
// by another structural directive then it's time
// to deregister the message from the controller
currentElement.on('$destroy', function() {
+ // If the message element was removed via a call to `detach` then `currentElement` will be null
+ // So this handler only handles cases where something else removed the message element.
if (currentElement && currentElement.$$attachId === $$attachId) {
- ngMessagesCtrl.deregister(commentNode);
+ ngMessagesCtrl.deregister(commentNode, isDefault);
messageCtrl.detach();
}
newScope.$destroy();
@@ -721,6 +803,14 @@ function ngMessageDirectiveFactory() {
$animate.leave(elm);
}
}
+ }, isDefault);
+
+ // We need to ensure that this directive deregisters itself when it no longer exists
+ // Normally this is done when the attached element is destroyed; but if this directive
+ // gets removed before we attach the message to the DOM there is nothing to watch
+ // in which case we must deregister when the containing scope is destroyed.
+ scope.$on('$destroy', function() {
+ ngMessagesCtrl.deregister(commentNode, isDefault);
});
}
};
diff --git a/xstatic/pkg/angular/data/angular-mocks.js b/xstatic/pkg/angular/data/angular-mocks.js
index 42f19b7..c4c4868 100644
--- a/xstatic/pkg/angular/data/angular-mocks.js
+++ b/xstatic/pkg/angular/data/angular-mocks.js
@@ -1,12 +1,14 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {
'use strict';
+/* global routeToRegExp: false */
+
/**
* @ngdoc object
* @name angular.mock
@@ -31,23 +33,27 @@ angular.mock = {};
* that there are several helper methods available which can be used in tests.
*/
angular.mock.$BrowserProvider = function() {
- this.$get = function() {
- return new angular.mock.$Browser();
- };
+ this.$get = [
+ '$log', '$$taskTrackerFactory',
+ function($log, $$taskTrackerFactory) {
+ return new angular.mock.$Browser($log, $$taskTrackerFactory);
+ }
+ ];
};
-angular.mock.$Browser = function() {
+angular.mock.$Browser = function($log, $$taskTrackerFactory) {
var self = this;
+ var taskTracker = $$taskTrackerFactory($log);
this.isMock = true;
- self.$$url = "http://server/";
+ self.$$url = 'http://server/';
self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = [];
- // TODO(vojta): remove this temporary api
- self.$$completeOutstandingRequest = angular.noop;
- self.$$incOutstandingRequestCount = angular.noop;
-
+ // Task-tracking API
+ self.$$completeOutstandingRequest = taskTracker.completeTask;
+ self.$$incOutstandingRequestCount = taskTracker.incTaskCount;
+ self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks;
// register url polling fn
@@ -71,11 +77,22 @@ angular.mock.$Browser = function() {
self.deferredFns = [];
self.deferredNextId = 0;
- self.defer = function(fn, delay) {
+ self.defer = function(fn, delay, taskType) {
+ var timeoutId = self.deferredNextId++;
+
delay = delay || 0;
- self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
- self.deferredFns.sort(function(a, b) { return a.time - b.time;});
- return self.deferredNextId++;
+ taskType = taskType || taskTracker.DEFAULT_TASK_TYPE;
+
+ taskTracker.incTaskCount(taskType);
+ self.deferredFns.push({
+ id: timeoutId,
+ type: taskType,
+ time: (self.defer.now + delay),
+ fn: fn
+ });
+ self.deferredFns.sort(function(a, b) { return a.time - b.time; });
+
+ return timeoutId;
};
@@ -89,14 +106,15 @@ angular.mock.$Browser = function() {
self.defer.cancel = function(deferId) {
- var fnIndex;
+ var taskIndex;
- angular.forEach(self.deferredFns, function(fn, index) {
- if (fn.id === deferId) fnIndex = index;
+ angular.forEach(self.deferredFns, function(task, index) {
+ if (task.id === deferId) taskIndex = index;
});
- if (angular.isDefined(fnIndex)) {
- self.deferredFns.splice(fnIndex, 1);
+ if (angular.isDefined(taskIndex)) {
+ var task = self.deferredFns.splice(taskIndex, 1)[0];
+ taskTracker.completeTask(angular.noop, task.type);
return true;
}
@@ -110,6 +128,8 @@ angular.mock.$Browser = function() {
* @description
* Flushes all pending requests and executes the defer callbacks.
*
+ * See {@link ngMock.$flushPendingsTasks} for more info.
+ *
* @param {number=} number of milliseconds to flush. See {@link #defer.now}
*/
self.defer.flush = function(delay) {
@@ -118,26 +138,76 @@ angular.mock.$Browser = function() {
if (angular.isDefined(delay)) {
// A delay was passed so compute the next time
nextTime = self.defer.now + delay;
+ } else if (self.deferredFns.length) {
+ // No delay was passed so set the next time so that it clears the deferred queue
+ nextTime = self.deferredFns[self.deferredFns.length - 1].time;
} else {
- if (self.deferredFns.length) {
- // No delay was passed so set the next time so that it clears the deferred queue
- nextTime = self.deferredFns[self.deferredFns.length - 1].time;
- } else {
- // No delay passed, but there are no deferred tasks so flush - indicates an error!
- throw new Error('No deferred tasks to be flushed');
- }
+ // No delay passed, but there are no deferred tasks so flush - indicates an error!
+ throw new Error('No deferred tasks to be flushed');
}
while (self.deferredFns.length && self.deferredFns[0].time <= nextTime) {
// Increment the time and call the next deferred function
self.defer.now = self.deferredFns[0].time;
- self.deferredFns.shift().fn();
+ var task = self.deferredFns.shift();
+ taskTracker.completeTask(task.fn, task.type);
}
// Ensure that the current time is correct
self.defer.now = nextTime;
};
+ /**
+ * @name $browser#defer.getPendingTasks
+ *
+ * @description
+ * Returns the currently pending tasks that need to be flushed.
+ * You can request a specific type of tasks only, by specifying a `taskType`.
+ *
+ * @param {string=} taskType - The type tasks to return.
+ */
+ self.defer.getPendingTasks = function(taskType) {
+ return !taskType
+ ? self.deferredFns
+ : self.deferredFns.filter(function(task) { return task.type === taskType; });
+ };
+
+ /**
+ * @name $browser#defer.formatPendingTasks
+ *
+ * @description
+ * Formats each task in a list of pending tasks as a string, suitable for use in error messages.
+ *
+ * @param {Array<Object>} pendingTasks - A list of task objects.
+ * @return {Array<string>} A list of stringified tasks.
+ */
+ self.defer.formatPendingTasks = function(pendingTasks) {
+ return pendingTasks.map(function(task) {
+ return '{id: ' + task.id + ', type: ' + task.type + ', time: ' + task.time + '}';
+ });
+ };
+
+ /**
+ * @name $browser#defer.verifyNoPendingTasks
+ *
+ * @description
+ * Verifies that there are no pending tasks that need to be flushed.
+ * You can check for a specific type of tasks only, by specifying a `taskType`.
+ *
+ * See {@link $verifyNoPendingTasks} for more info.
+ *
+ * @param {string=} taskType - The type tasks to check for.
+ */
+ self.defer.verifyNoPendingTasks = function(taskType) {
+ var pendingTasks = self.defer.getPendingTasks(taskType);
+
+ if (pendingTasks.length) {
+ var formattedTasks = self.defer.formatPendingTasks(pendingTasks).join('\n ');
+ throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' +
+ formattedTasks);
+ }
+ };
+
self.$$baseHref = '/';
self.baseHref = function() {
return this.$$baseHref;
@@ -162,7 +232,8 @@ angular.mock.$Browser.prototype = {
state = null;
}
if (url) {
- this.$$url = url;
+ // The `$browser` service trims empty hashes; simulate it.
+ this.$$url = url.replace(/#$/, '');
// Native pushState serializes & copies the object; simulate it.
this.$$state = angular.copy(state);
return this;
@@ -173,13 +244,85 @@ angular.mock.$Browser.prototype = {
state: function() {
return this.$$state;
- },
-
- notifyWhenNoOutstandingRequests: function(fn) {
- fn();
}
};
+/**
+ * @ngdoc service
+ * @name $flushPendingTasks
+ *
+ * @description
+ * Flushes all currently pending tasks and executes the corresponding callbacks.
+ *
+ * Optionally, you can also pass a `delay` argument to only flush tasks that are scheduled to be
+ * executed within `delay` milliseconds. Currently, `delay` only applies to timeouts, since all
+ * other tasks have a delay of 0 (i.e. they are scheduled to be executed as soon as possible, but
+ * still asynchronously).
+ *
+ * If no delay is specified, it uses a delay such that all currently pending tasks are flushed.
+ *
+ * The types of tasks that are flushed include:
+ *
+ * - Pending timeouts (via {@link $timeout}).
+ * - Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}.
+ * - Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
+ * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
+ *
+ * <div class="alert alert-info">
+ * Periodic tasks scheduled via {@link $interval} use a different queue and are not flushed by
+ * `$flushPendingTasks()`. Use {@link ngMock.$interval#flush $interval.flush(millis)} instead.
+ * </div>
+ *
+ * @param {number=} delay - The number of milliseconds to flush.
+ */
+angular.mock.$FlushPendingTasksProvider = function() {
+ this.$get = [
+ '$browser',
+ function($browser) {
+ return function $flushPendingTasks(delay) {
+ return $browser.defer.flush(delay);
+ };
+ }
+ ];
+};
+
+/**
+ * @ngdoc service
+ * @name $verifyNoPendingTasks
+ *
+ * @description
+ * Verifies that there are no pending tasks that need to be flushed. It throws an error if there are
+ * still pending tasks.
+ *
+ * You can check for a specific type of tasks only, by specifying a `taskType`.
+ *
+ * Available task types:
+ *
+ * - `$timeout`: Pending timeouts (via {@link $timeout}).
+ * - `$http`: Pending HTTP requests (via {@link $http}).
+ * - `$route`: In-progress route transitions (via {@link $route}).
+ * - `$applyAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}.
+ * - `$evalAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
+ * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
+ *
+ * <div class="alert alert-info">
+ * Periodic tasks scheduled via {@link $interval} use a different queue and are not taken into
+ * account by `$verifyNoPendingTasks()`. There is currently no way to verify that there are no
+ * pending {@link $interval} tasks.
+ * </div>
+ *
+ * @param {string=} taskType - The type of tasks to check for.
+ */
+angular.mock.$VerifyNoPendingTasksProvider = function() {
+ this.$get = [
+ '$browser',
+ function($browser) {
+ return function $verifyNoPendingTasks(taskType) {
+ return $browser.defer.verifyNoPendingTasks(taskType);
+ };
+ }
+ ];
+};
/**
* @ngdoc provider
@@ -252,19 +395,19 @@ angular.mock.$ExceptionHandlerProvider = function() {
case 'rethrow':
var errors = [];
handler = function(e) {
- if (arguments.length == 1) {
+ if (arguments.length === 1) {
errors.push(e);
} else {
errors.push([].slice.call(arguments, 0));
}
- if (mode === "rethrow") {
+ if (mode === 'rethrow') {
throw e;
}
};
handler.errors = errors;
break;
default:
- throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
+ throw new Error('Unknown mode \'' + mode + '\', only \'log\'/\'rethrow\' modes are allowed!');
}
};
@@ -414,8 +557,8 @@ angular.mock.$LogProvider = function() {
});
});
if (errors.length) {
- errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " +
- "an expected log message was not checked and removed:");
+ errors.unshift('Expected $log to be empty! Either a message was logged unexpectedly, or ' +
+ 'an expected log message was not checked and removed:');
errors.push('');
throw new Error(errors.join('\n---------\n'));
}
@@ -448,62 +591,40 @@ angular.mock.$LogProvider = function() {
* @returns {promise} A promise which will be notified on each iteration.
*/
angular.mock.$IntervalProvider = function() {
- this.$get = ['$browser', '$rootScope', '$q', '$$q',
- function($browser, $rootScope, $q, $$q) {
+ this.$get = ['$browser', '$$intervalFactory',
+ function($browser, $$intervalFactory) {
var repeatFns = [],
nextRepeatId = 0,
- now = 0;
-
- var $interval = function(fn, delay, count, invokeApply) {
- var hasParams = arguments.length > 4,
- args = hasParams ? Array.prototype.slice.call(arguments, 4) : [],
- iteration = 0,
- skipApply = (angular.isDefined(invokeApply) && !invokeApply),
- deferred = (skipApply ? $$q : $q).defer(),
- promise = deferred.promise;
-
- count = (angular.isDefined(count)) ? count : 0;
- promise.then(null, null, (!hasParams) ? fn : function() {
- fn.apply(null, args);
- });
-
- promise.$$intervalId = nextRepeatId;
-
- function tick() {
- deferred.notify(iteration++);
-
- if (count > 0 && iteration >= count) {
- var fnIndex;
- deferred.resolve(iteration);
-
- angular.forEach(repeatFns, function(fn, index) {
- if (fn.id === promise.$$intervalId) fnIndex = index;
+ now = 0,
+ setIntervalFn = function(tick, delay, deferred, skipApply) {
+ var id = nextRepeatId++;
+ var fn = !skipApply ? tick : function() {
+ tick();
+ $browser.defer.flush();
+ };
+
+ repeatFns.push({
+ nextTime: (now + (delay || 0)),
+ delay: delay || 1,
+ fn: fn,
+ id: id,
+ deferred: deferred
});
+ repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime; });
- if (angular.isDefined(fnIndex)) {
- repeatFns.splice(fnIndex, 1);
+ return id;
+ },
+ clearIntervalFn = function(id) {
+ for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) {
+ if (repeatFns[fnIndex].id === id) {
+ repeatFns.splice(fnIndex, 1);
+ break;
+ }
}
- }
-
- if (skipApply) {
- $browser.defer.flush();
- } else {
- $rootScope.$apply();
- }
- }
+ };
- repeatFns.push({
- nextTime:(now + delay),
- delay: delay,
- fn: tick,
- id: nextRepeatId,
- deferred: deferred
- });
- repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
+ var $interval = $$intervalFactory(setIntervalFn, clearIntervalFn);
- nextRepeatId++;
- return promise;
- };
/**
* @ngdoc method
* @name $interval#cancel
@@ -516,16 +637,15 @@ angular.mock.$IntervalProvider = function() {
*/
$interval.cancel = function(promise) {
if (!promise) return false;
- var fnIndex;
-
- angular.forEach(repeatFns, function(fn, index) {
- if (fn.id === promise.$$intervalId) fnIndex = index;
- });
- if (angular.isDefined(fnIndex)) {
- repeatFns[fnIndex].deferred.reject('canceled');
- repeatFns.splice(fnIndex, 1);
- return true;
+ for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) {
+ if (repeatFns[fnIndex].id === promise.$$intervalId) {
+ var deferred = repeatFns[fnIndex].deferred;
+ deferred.promise.then(undefined, function() {});
+ deferred.reject('canceled');
+ repeatFns.splice(fnIndex, 1);
+ return true;
+ }
}
return false;
@@ -538,15 +658,21 @@ angular.mock.$IntervalProvider = function() {
*
* Runs interval tasks scheduled to be run in the next `millis` milliseconds.
*
- * @param {number=} millis maximum timeout amount to flush up until.
+ * @param {number} millis maximum timeout amount to flush up until.
*
* @return {number} The amount of time moved forward.
*/
$interval.flush = function(millis) {
+ var before = now;
now += millis;
while (repeatFns.length && repeatFns[0].nextTime <= now) {
var task = repeatFns[0];
task.fn();
+ if (task.nextTime === before) {
+ // this can only happen the first time
+ // a zero-delay interval gets triggered
+ task.nextTime++;
+ }
task.nextTime += task.delay;
repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
}
@@ -558,16 +684,13 @@ angular.mock.$IntervalProvider = function() {
};
-/* jshint -W101 */
-/* The R_ISO8061_STR regex is never going to fit into the 100 char limit!
- * This directive should go inside the anonymous function but a bug in JSHint means that it would
- * not be enacted early enough to prevent the warning.
- */
-var R_ISO8061_STR = /^(-?\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
-
function jsonStringToDate(string) {
+ // The R_ISO8061_STR regex is never going to fit into the 100 char limit!
+ // eslit-disable-next-line max-len
+ var R_ISO8061_STR = /^(-?\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
+
var match;
- if (match = string.match(R_ISO8061_STR)) {
+ if ((match = string.match(R_ISO8061_STR))) {
var date = new Date(0),
tzHour = 0,
tzMin = 0;
@@ -650,9 +773,10 @@ angular.mock.TzDate = function(offset, timestamp) {
timestamp = self.origDate.getTime();
if (isNaN(timestamp)) {
+ // eslint-disable-next-line no-throw-literal
throw {
- name: "Illegal Argument",
- message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
+ name: 'Illegal Argument',
+ message: 'Arg \'' + tsStr + '\' passed into TzDate constructor is not a valid date string'
};
}
} else {
@@ -758,7 +882,7 @@ angular.mock.TzDate = function(offset, timestamp) {
angular.forEach(unimplementedMethods, function(methodName) {
self[methodName] = function() {
- throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock");
+ throw new Error('Method \'' + methodName + '\' is not implemented in the TzDate mock');
};
});
@@ -767,7 +891,6 @@ angular.mock.TzDate = function(offset, timestamp) {
//make "tzDateInstance instanceof Date" return true
angular.mock.TzDate.prototype = Date.prototype;
-/* jshint +W101 */
/**
@@ -781,6 +904,7 @@ angular.mock.TzDate.prototype = Date.prototype;
* You need to require the `ngAnimateMock` module in your test suite for instance `beforeEach(module('ngAnimateMock'))`
*/
angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
+ .info({ angularVersion: '"1.8.2"' })
.config(['$provide', function($provide) {
@@ -946,7 +1070,7 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
*
* *NOTE*: This is not an injectable instance, just a globally available function.
*
- * Method for serializing common angular objects (scope, elements, etc..) into strings.
+ * Method for serializing common AngularJS objects (scope, elements, etc..) into strings.
* It is useful for logging objects to the console when debugging.
*
* @param {*} object - any object to turn into string.
@@ -1028,7 +1152,7 @@ angular.mock.dump = function(object) {
* This mock implementation can be used to respond with static or dynamic responses via the
* `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc).
*
- * When an Angular application needs some data from a server, it calls the $http service, which
+ * When an AngularJS application needs some data from a server, it calls the $http service, which
* sends the request to a real server using $httpBackend service. With dependency injection, it is
* easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify
* the requests and respond with some testing data without sending a request to a real server.
@@ -1123,6 +1247,8 @@ angular.mock.dump = function(object) {
$http.get('/auth.py').then(function(response) {
authToken = response.headers('A-Token');
$scope.user = response.data;
+ }).catch(function() {
+ $scope.status = 'Failed...';
});
$scope.saveMessage = function(message) {
@@ -1215,7 +1341,7 @@ angular.mock.dump = function(object) {
$httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
// check if the header was sent, if it wasn't the expectation won't
// match the request and the test will fail
- return headers['Authorization'] == 'xxx';
+ return headers['Authorization'] === 'xxx';
}).respond(201, '');
$rootScope.saveMessage('whatever');
@@ -1264,7 +1390,7 @@ angular.mock.dump = function(object) {
* ## Matching route requests
*
* For extra convenience, `whenRoute` and `expectRoute` shortcuts are available. These methods offer colon
- * delimited matching of the url path, ignoring the query string. This allows declarations
+ * delimited matching of the url path, ignoring the query string and trailing slashes. This allows declarations
* similar to how application routes are configured with `$routeProvider`. Because these methods convert
* the definition url to regex, declaration order is important. Combined with query parameter parsing,
* the following is possible:
@@ -1304,9 +1430,8 @@ angular.mock.dump = function(object) {
});
```
*/
-angular.mock.$HttpBackendProvider = function() {
- this.$get = ['$rootScope', '$timeout', createHttpBackendMock];
-};
+angular.mock.$httpBackendDecorator =
+ ['$rootScope', '$timeout', '$delegate', createHttpBackendMock];
/**
* General factory function for $httpBackend mock.
@@ -1325,17 +1450,21 @@ angular.mock.$HttpBackendProvider = function() {
function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
var definitions = [],
expectations = [],
+ matchLatestDefinition = false,
responses = [],
responsesPush = angular.bind(responses, responses.push),
- copy = angular.copy;
+ copy = angular.copy,
+ // We cache the original backend so that if both ngMock and ngMockE2E override the
+ // service the ngMockE2E version can pass through to the real backend
+ originalHttpBackend = $delegate.$$originalHttpBackend || $delegate;
function createResponse(status, data, headers, statusText) {
if (angular.isFunction(status)) return status;
return function() {
return angular.isNumber(status)
- ? [status, data, headers, statusText]
- : [200, status, data, headers];
+ ? [status, data, headers, statusText, 'complete']
+ : [200, status, data, headers, 'complete'];
};
}
@@ -1357,39 +1486,57 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
function wrapResponse(wrapped) {
if (!$browser && timeout) {
- timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout);
+ if (timeout.then) {
+ timeout.then(function() {
+ handlePrematureEnd(angular.isDefined(timeout.$$timeoutId) ? 'timeout' : 'abort');
+ });
+ } else {
+ $timeout(function() {
+ handlePrematureEnd('timeout');
+ }, timeout);
+ }
}
+ handleResponse.description = method + ' ' + url;
return handleResponse;
function handleResponse() {
var response = wrapped.response(method, url, data, headers, wrapped.params(url));
xhr.$$respHeaders = response[2];
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
- copy(response[3] || ''));
+ copy(response[3] || ''), copy(response[4]));
}
- function handleTimeout() {
+ function handlePrematureEnd(reason) {
for (var i = 0, ii = responses.length; i < ii; i++) {
if (responses[i] === handleResponse) {
responses.splice(i, 1);
- callback(-1, undefined, '');
+ callback(-1, undefined, '', undefined, reason);
break;
}
}
}
}
+ function createFatalError(message) {
+ var error = new Error(message);
+ // In addition to being converted to a rejection, these errors also need to be passed to
+ // the $exceptionHandler and be rethrown (so that the test fails).
+ error.$$passToExceptionHandler = true;
+ return error;
+ }
+
if (expectation && expectation.match(method, url)) {
if (!expectation.matchData(data)) {
- throw new Error('Expected ' + expectation + ' with different data\n' +
- 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
+ throw createFatalError('Expected ' + expectation + ' with different data\n' +
+ 'EXPECTED: ' + prettyPrint(expectation.data) + '\n' +
+ 'GOT: ' + data);
}
if (!expectation.matchHeaders(headers)) {
- throw new Error('Expected ' + expectation + ' with different headers\n' +
- 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
- prettyPrint(headers));
+ throw createFatalError('Expected ' + expectation + ' with different headers\n' +
+ 'EXPECTED: ' + prettyPrint(expectation.headers) + '\n' +
+ 'GOT: ' + prettyPrint(headers));
}
expectations.shift();
@@ -1401,22 +1548,26 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
wasExpected = true;
}
- var i = -1, definition;
- while ((definition = definitions[++i])) {
+ var i = matchLatestDefinition ? definitions.length : -1, definition;
+
+ while ((definition = definitions[matchLatestDefinition ? --i : ++i])) {
if (definition.match(method, url, data, headers || {})) {
if (definition.response) {
// if $browser specified, we do auto flush all requests
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
} else if (definition.passThrough) {
- $delegate(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers);
- } else throw new Error('No response defined !');
+ originalHttpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers);
+ } else throw createFatalError('No response defined !');
return;
}
}
- throw wasExpected ?
- new Error('No response defined !') :
- new Error('Unexpected request: ' + method + ' ' + url + '\n' +
- (expectation ? 'Expected ' + expectation : 'No more request expected'));
+
+ if (wasExpected) {
+ throw createFatalError('No response defined !');
+ }
+
+ throw createFatalError('Unexpected request: ' + method + ' ' + url + '\n' +
+ (expectation ? 'Expected ' + expectation : 'No more request expected'));
}
/**
@@ -1426,7 +1577,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new backend definition.
*
* @param {string} method HTTP method.
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
@@ -1444,10 +1595,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* ```
* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (Array|Object|string),
- * response headers (Object), and the text for the status (string). The respond method returns
- * the `requestHandler` object for possible overrides.
+ * response headers (Object), HTTP status text (string), and XMLHttpRequest status (string:
+ * `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler`
+ * object for possible overrides.
*/
$httpBackend.when = function(method, url, data, headers, keys) {
+
+ assertArgDefined(arguments, 1, 'url');
+
var definition = new MockHttpExpectation(method, url, data, headers, keys),
chain = {
respond: function(status, data, headers, statusText) {
@@ -1471,13 +1626,55 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
/**
* @ngdoc method
+ * @name $httpBackend#matchLatestDefinitionEnabled
+ * @description
+ * This method can be used to change which mocked responses `$httpBackend` returns, when defining
+ * them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
+ * By default, `$httpBackend` returns the first definition that matches. When setting
+ * `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the
+ * one that was added last.
+ *
+ * ```js
+ * hb.when('GET', '/url1').respond(200, 'content', {});
+ * hb.when('GET', '/url1').respond(201, 'another', {});
+ * hb('GET', '/url1'); // receives "content"
+ *
+ * $http.matchLatestDefinitionEnabled(true)
+ * hb('GET', '/url1'); // receives "another"
+ *
+ * hb.when('GET', '/url1').respond(201, 'onemore', {});
+ * hb('GET', '/url1'); // receives "onemore"
+ * ```
+ *
+ * This is useful if a you have a default response that is overriden inside specific tests.
+ *
+ * Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed
+ * even when the application is already running.
+ *
+ * @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
+ * If omitted, it will return the current value.
+ * @return {$httpBackend|Boolean} self when used as a setter, and the current value when used
+ * as a getter
+ */
+ $httpBackend.matchLatestDefinitionEnabled = function(value) {
+ if (angular.isDefined(value)) {
+ matchLatestDefinition = value;
+ return this;
+ } else {
+ return matchLatestDefinition;
+ }
+ };
+
+ /**
+ * @ngdoc method
* @name $httpBackend#whenGET
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
- * @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1490,9 +1687,10 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
- * @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1505,9 +1703,10 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
- * @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1520,11 +1719,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
- * @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1537,11 +1737,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
- * @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1554,7 +1755,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
@@ -1573,42 +1774,13 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string} url HTTP url string that supports colon param matching.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
- * order to change how a matched request is handled. See #when for more info.
+ * order to change how a matched request is handled.
+ * See {@link ngMock.$httpBackend#when `when`} for more info.
*/
$httpBackend.whenRoute = function(method, url) {
- var pathObj = parseRoute(url);
- return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys);
- };
-
- function parseRoute(url) {
- var ret = {
- regexp: url
- },
- keys = ret.keys = [];
-
- if (!url || !angular.isString(url)) return ret;
-
- url = url
- .replace(/([().])/g, '\\$1')
- .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) {
- var optional = option === '?' ? option : null;
- var star = option === '*' ? option : null;
- keys.push({ name: key, optional: !!optional });
- slash = slash || '';
- return ''
- + (optional ? '' : slash)
- + '(?:'
- + (optional ? slash : '')
- + (star && '(.+?)' || '([^/]+)')
- + (optional || '')
- + ')'
- + (optional || '');
- })
- .replace(/([\/$\*])/g, '\\$1');
-
- ret.regexp = new RegExp('^' + url, 'i');
- return ret;
- }
+ var parsed = parseRouteUrl(url);
+ return $httpBackend.when(method, parsed.regexp, undefined, undefined, parsed.keys);
+ };
/**
* @ngdoc method
@@ -1617,7 +1789,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation.
*
* @param {string} method HTTP method.
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
@@ -1630,16 +1802,20 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* order to change how a matched request is handled.
*
* - respond –
- * ```
- * { function([status,] data[, headers, statusText])
- * | function(function(method, url, data, headers, params)}
- * ```
+ * ```js
+ * {function([status,] data[, headers, statusText])
+ * | function(function(method, url, data, headers, params)}
+ * ```
* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (Array|Object|string),
- * response headers (Object), and the text for the status (string). The respond method returns
- * the `requestHandler` object for possible overrides.
+ * response headers (Object), HTTP status text (string), and XMLHttpRequest status (string:
+ * `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler`
+ * object for possible overrides.
*/
$httpBackend.expect = function(method, url, data, headers, keys) {
+
+ assertArgDefined(arguments, 1, 'url');
+
var expectation = new MockHttpExpectation(method, url, data, headers, keys),
chain = {
respond: function(status, data, headers, statusText) {
@@ -1658,9 +1834,10 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new request expectation for GET requests. For more info see `expect()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
- * and returns true if the url matches the current definition.
- * @param {Object=} headers HTTP headers.
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
+ * and returns true if the url matches the current expectation.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1673,9 +1850,10 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new request expectation for HEAD requests. For more info see `expect()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
- * and returns true if the url matches the current definition.
- * @param {Object=} headers HTTP headers.
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
+ * and returns true if the url matches the current expectation.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1688,9 +1866,10 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new request expectation for DELETE requests. For more info see `expect()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
- * and returns true if the url matches the current definition.
- * @param {Object=} headers HTTP headers.
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
+ * and returns true if the url matches the current expectation.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1703,12 +1882,13 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new request expectation for POST requests. For more info see `expect()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
- * and returns true if the url matches the current definition.
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
+ * and returns true if the url matches the current expectation.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
- * @param {Object=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1721,12 +1901,13 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new request expectation for PUT requests. For more info see `expect()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
- * and returns true if the url matches the current definition.
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
+ * and returns true if the url matches the current expectation.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
- * @param {Object=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1739,12 +1920,13 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new request expectation for PATCH requests. For more info see `expect()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
- * and returns true if the url matches the current definition.
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
+ * and returns true if the url matches the current expectation.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
- * @param {Object=} headers HTTP headers.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1757,8 +1939,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @description
* Creates a new request expectation for JSONP requests. For more info see `expect()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives an url
- * and returns true if the url matches the current definition.
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives an url
+ * and returns true if the url matches the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1776,11 +1958,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string} url HTTP url string that supports colon param matching.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
- * order to change how a matched request is handled. See #expect for more info.
+ * order to change how a matched request is handled.
+ * See {@link ngMock.$httpBackend#expect `expect`} for more info.
*/
$httpBackend.expectRoute = function(method, url) {
- var pathObj = parseRoute(url);
- return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys);
+ var parsed = parseRouteUrl(url);
+ return $httpBackend.expect(method, parsed.regexp, undefined, undefined, parsed.keys);
};
@@ -1788,24 +1971,34 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @ngdoc method
* @name $httpBackend#flush
* @description
- * Flushes all pending requests using the trained responses.
- *
- * @param {number=} count Number of responses to flush (in the order they arrived). If undefined,
- * all pending requests will be flushed. If there are no pending requests when the flush method
- * is called an exception is thrown (as this typically a sign of programming error).
+ * Flushes pending requests using the trained responses. Requests are flushed in the order they
+ * were made, but it is also possible to skip one or more requests (for example to have them
+ * flushed later). This is useful for simulating scenarios where responses arrive from the server
+ * in any order.
+ *
+ * If there are no pending requests to flush when the method is called, an exception is thrown (as
+ * this is typically a sign of programming error).
+ *
+ * @param {number=} count - Number of responses to flush. If undefined/null, all pending requests
+ * (starting after `skip`) will be flushed.
+ * @param {number=} [skip=0] - Number of pending requests to skip. For example, a value of `5`
+ * would skip the first 5 pending requests and start flushing from the 6th onwards.
*/
- $httpBackend.flush = function(count, digest) {
+ $httpBackend.flush = function(count, skip, digest) {
if (digest !== false) $rootScope.$digest();
- if (!responses.length) throw new Error('No pending request to flush !');
+
+ skip = skip || 0;
+ if (skip >= responses.length) throw new Error('No pending request to flush !');
if (angular.isDefined(count) && count !== null) {
while (count--) {
- if (!responses.length) throw new Error('No more pending request to flush !');
- responses.shift()();
+ var part = responses.splice(skip, 1);
+ if (!part.length) throw new Error('No more pending request to flush !');
+ part[0]();
}
} else {
- while (responses.length) {
- responses.shift()();
+ while (responses.length > skip) {
+ responses.splice(skip, 1)[0]();
}
}
$httpBackend.verifyNoOutstandingExpectation(digest);
@@ -1847,9 +2040,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* afterEach($httpBackend.verifyNoOutstandingRequest);
* ```
*/
- $httpBackend.verifyNoOutstandingRequest = function() {
+ $httpBackend.verifyNoOutstandingRequest = function(digest) {
+ if (digest !== false) $rootScope.$digest();
if (responses.length) {
- throw new Error('Unflushed requests: ' + responses.length);
+ var unflushedDescriptions = responses.map(function(res) { return res.description; });
+ throw new Error('Unflushed requests: ' + responses.length + '\n ' +
+ unflushedDescriptions.join('\n '));
}
};
@@ -1867,125 +2063,166 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
responses.length = 0;
};
+ $httpBackend.$$originalHttpBackend = originalHttpBackend;
+
return $httpBackend;
function createShortMethods(prefix) {
angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) {
$httpBackend[prefix + method] = function(url, headers, keys) {
+ assertArgDefined(arguments, 0, 'url');
+
+ // Change url to `null` if `undefined` to stop it throwing an exception further down
+ if (angular.isUndefined(url)) url = null;
+
return $httpBackend[prefix](method, url, undefined, headers, keys);
};
});
angular.forEach(['PUT', 'POST', 'PATCH'], function(method) {
$httpBackend[prefix + method] = function(url, data, headers, keys) {
+ assertArgDefined(arguments, 0, 'url');
+
+ // Change url to `null` if `undefined` to stop it throwing an exception further down
+ if (angular.isUndefined(url)) url = null;
+
return $httpBackend[prefix](method, url, data, headers, keys);
};
});
}
-}
-function MockHttpExpectation(method, url, data, headers, keys) {
-
- function getUrlParams(u) {
- var params = u.slice(u.indexOf('?') + 1).split('&');
- return params.sort();
+ function parseRouteUrl(url) {
+ var strippedUrl = stripQueryAndHash(url);
+ var parseOptions = {caseInsensitiveMatch: true, ignoreTrailingSlashes: true};
+ return routeToRegExp(strippedUrl, parseOptions);
}
+}
- function compareUrl(u) {
- return (url.slice(0, url.indexOf('?')) == u.slice(0, u.indexOf('?')) && getUrlParams(url).join() == getUrlParams(u).join());
+function assertArgDefined(args, index, name) {
+ if (args.length > index && angular.isUndefined(args[index])) {
+ throw new Error('Undefined argument `' + name + '`; the argument is provided but not defined');
}
+}
+
+function stripQueryAndHash(url) {
+ return url.replace(/[?#].*$/, '');
+}
+
+function MockHttpExpectation(expectedMethod, expectedUrl, expectedData, expectedHeaders,
+ expectedKeys) {
- this.data = data;
- this.headers = headers;
+ this.data = expectedData;
+ this.headers = expectedHeaders;
- this.match = function(m, u, d, h) {
- if (method != m) return false;
- if (!this.matchUrl(u)) return false;
- if (angular.isDefined(d) && !this.matchData(d)) return false;
- if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
+ this.match = function(method, url, data, headers) {
+ if (expectedMethod !== method) return false;
+ if (!this.matchUrl(url)) return false;
+ if (angular.isDefined(data) && !this.matchData(data)) return false;
+ if (angular.isDefined(headers) && !this.matchHeaders(headers)) return false;
return true;
};
- this.matchUrl = function(u) {
- if (!url) return true;
- if (angular.isFunction(url.test)) return url.test(u);
- if (angular.isFunction(url)) return url(u);
- return (url == u || compareUrl(u));
+ this.matchUrl = function(url) {
+ if (!expectedUrl) return true;
+ if (angular.isFunction(expectedUrl.test)) return expectedUrl.test(url);
+ if (angular.isFunction(expectedUrl)) return expectedUrl(url);
+ return (expectedUrl === url || compareUrlWithQuery(url));
};
- this.matchHeaders = function(h) {
- if (angular.isUndefined(headers)) return true;
- if (angular.isFunction(headers)) return headers(h);
- return angular.equals(headers, h);
+ this.matchHeaders = function(headers) {
+ if (angular.isUndefined(expectedHeaders)) return true;
+ if (angular.isFunction(expectedHeaders)) return expectedHeaders(headers);
+ return angular.equals(expectedHeaders, headers);
};
- this.matchData = function(d) {
- if (angular.isUndefined(data)) return true;
- if (data && angular.isFunction(data.test)) return data.test(d);
- if (data && angular.isFunction(data)) return data(d);
- if (data && !angular.isString(data)) {
- return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d));
+ this.matchData = function(data) {
+ if (angular.isUndefined(expectedData)) return true;
+ if (expectedData && angular.isFunction(expectedData.test)) return expectedData.test(data);
+ if (expectedData && angular.isFunction(expectedData)) return expectedData(data);
+ if (expectedData && !angular.isString(expectedData)) {
+ return angular.equals(angular.fromJson(angular.toJson(expectedData)), angular.fromJson(data));
}
- return data == d;
+ // eslint-disable-next-line eqeqeq
+ return expectedData == data;
};
this.toString = function() {
- return method + ' ' + url;
+ return expectedMethod + ' ' + expectedUrl;
};
- this.params = function(u) {
- return angular.extend(parseQuery(), pathParams());
+ this.params = function(url) {
+ var queryStr = url.indexOf('?') === -1 ? '' : url.substring(url.indexOf('?') + 1);
+ var strippedUrl = stripQueryAndHash(url);
- function pathParams() {
- var keyObj = {};
- if (!url || !angular.isFunction(url.test) || !keys || keys.length === 0) return keyObj;
+ return angular.extend(extractParamsFromQuery(queryStr), extractParamsFromPath(strippedUrl));
+ };
- var m = url.exec(u);
- if (!m) return keyObj;
- for (var i = 1, len = m.length; i < len; ++i) {
- var key = keys[i - 1];
- var val = m[i];
- if (key && val) {
- keyObj[key.name || key] = val;
- }
- }
+ function compareUrlWithQuery(url) {
+ var urlWithQueryRe = /^([^?]*)\?(.*)$/;
+
+ var expectedMatch = urlWithQueryRe.exec(expectedUrl);
+ var actualMatch = urlWithQueryRe.exec(url);
+
+ return !!(expectedMatch && actualMatch) &&
+ (expectedMatch[1] === actualMatch[1]) &&
+ (normalizeQuery(expectedMatch[2]) === normalizeQuery(actualMatch[2]));
+ }
+
+ function normalizeQuery(queryStr) {
+ return queryStr.split('&').sort().join('&');
+ }
- return keyObj;
+ function extractParamsFromPath(strippedUrl) {
+ var keyObj = {};
+
+ if (!expectedUrl || !angular.isFunction(expectedUrl.test) ||
+ !expectedKeys || !expectedKeys.length) return keyObj;
+
+ var match = expectedUrl.exec(strippedUrl);
+ if (!match) return keyObj;
+
+ for (var i = 1, len = match.length; i < len; ++i) {
+ var key = expectedKeys[i - 1];
+ var val = match[i];
+ if (key && val) {
+ keyObj[key.name || key] = val;
+ }
}
- function parseQuery() {
- var obj = {}, key_value, key,
- queryStr = u.indexOf('?') > -1
- ? u.substring(u.indexOf('?') + 1)
- : "";
-
- angular.forEach(queryStr.split('&'), function(keyValue) {
- if (keyValue) {
- key_value = keyValue.replace(/\+/g,'%20').split('=');
- key = tryDecodeURIComponent(key_value[0]);
- if (angular.isDefined(key)) {
- var val = angular.isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
- if (!hasOwnProperty.call(obj, key)) {
- obj[key] = val;
- } else if (angular.isArray(obj[key])) {
- obj[key].push(val);
- } else {
- obj[key] = [obj[key],val];
- }
- }
+ return keyObj;
+ }
+
+ function extractParamsFromQuery(queryStr) {
+ var obj = {},
+ keyValuePairs = queryStr.split('&').
+ filter(angular.identity). // Ignore empty segments.
+ map(function(keyValue) { return keyValue.replace(/\+/g, '%20').split('='); });
+
+ angular.forEach(keyValuePairs, function(pair) {
+ var key = tryDecodeURIComponent(pair[0]);
+ if (angular.isDefined(key)) {
+ var val = angular.isDefined(pair[1]) ? tryDecodeURIComponent(pair[1]) : true;
+ if (!hasOwnProperty.call(obj, key)) {
+ obj[key] = val;
+ } else if (angular.isArray(obj[key])) {
+ obj[key].push(val);
+ } else {
+ obj[key] = [obj[key], val];
}
- });
- return obj;
- }
- function tryDecodeURIComponent(value) {
- try {
- return decodeURIComponent(value);
- } catch (e) {
- // Ignore any invalid uri component
}
+ });
+
+ return obj;
+ }
+
+ function tryDecodeURIComponent(value) {
+ try {
+ return decodeURIComponent(value);
+ } catch (e) {
+ // Ignore any invalid uri component
}
- };
+ }
}
function createMockXhr() {
@@ -2019,13 +2256,13 @@ function MockXhr() {
var header = this.$$respHeaders[name];
if (header) return header;
- name = angular.lowercase(name);
+ name = angular.$$lowercase(name);
header = this.$$respHeaders[name];
if (header) return header;
header = undefined;
angular.forEach(this.$$respHeaders, function(headerVal, headerName) {
- if (!header && angular.lowercase(headerName) == name) header = headerVal;
+ if (!header && angular.$$lowercase(headerName) === name) header = headerVal;
});
return header;
};
@@ -2039,10 +2276,14 @@ function MockXhr() {
return lines.join('\n');
};
- this.abort = angular.noop;
+ this.abort = function() {
+ if (isFunction(this.onabort)) {
+ this.onabort();
+ }
+ };
// This section simulates the events on a real XHR object (and the upload object)
- // When we are testing $httpBackend (inside the angular project) we make partial use of this
+ // When we are testing $httpBackend (inside the AngularJS project) we make partial use of this
// but store the events directly ourselves on `$$events`, instead of going through the `addEventListener`
this.$$events = {};
this.addEventListener = function(name, listener) {
@@ -2071,39 +2312,86 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $
/**
* @ngdoc method
* @name $timeout#flush
+ *
+ * @deprecated
+ * sinceVersion="1.7.3"
+ *
+ * This method flushes all types of tasks (not only timeouts), which is unintuitive.
+ * It is recommended to use {@link ngMock.$flushPendingTasks} instead.
+ *
* @description
*
* Flushes the queue of pending tasks.
*
+ * _This method is essentially an alias of {@link ngMock.$flushPendingTasks}._
+ *
+ * <div class="alert alert-warning">
+ * For historical reasons, this method will also flush non-`$timeout` pending tasks, such as
+ * {@link $q} promises and tasks scheduled via
+ * {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and
+ * {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
+ * </div>
+ *
* @param {number=} delay maximum timeout amount to flush up until
*/
$delegate.flush = function(delay) {
+ // For historical reasons, `$timeout.flush()` flushes all types of pending tasks.
+ // Keep the same behavior for backwards compatibility (and because it doesn't make sense to
+ // selectively flush scheduled events out of order).
$browser.defer.flush(delay);
};
/**
* @ngdoc method
* @name $timeout#verifyNoPendingTasks
+ *
+ * @deprecated
+ * sinceVersion="1.7.3"
+ *
+ * This method takes all types of tasks (not only timeouts) into account, which is unintuitive.
+ * It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally
+ * allows checking for timeouts only (with `$verifyNoPendingTasks('$timeout')`).
+ *
* @description
*
- * Verifies that there are no pending tasks that need to be flushed.
+ * Verifies that there are no pending tasks that need to be flushed. It throws an error if there
+ * are still pending tasks.
+ *
+ * _This method is essentially an alias of {@link ngMock.$verifyNoPendingTasks} (called with no
+ * arguments)._
+ *
+ * <div class="alert alert-warning">
+ * <p>
+ * For historical reasons, this method will also verify non-`$timeout` pending tasks, such as
+ * pending {@link $http} requests, in-progress {@link $route} transitions, unresolved
+ * {@link $q} promises and tasks scheduled via
+ * {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and
+ * {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
+ * </p>
+ * <p>
+ * It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally
+ * supports verifying a specific type of tasks. For example, you can verify there are no
+ * pending timeouts with `$verifyNoPendingTasks('$timeout')`.
+ * </p>
+ * </div>
*/
$delegate.verifyNoPendingTasks = function() {
- if ($browser.deferredFns.length) {
- throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' +
- formatPendingTasksAsString($browser.deferredFns));
+ // For historical reasons, `$timeout.verifyNoPendingTasks()` takes all types of pending tasks
+ // into account. Keep the same behavior for backwards compatibility.
+ var pendingTasks = $browser.defer.getPendingTasks();
+
+ if (pendingTasks.length) {
+ var formattedTasks = $browser.defer.formatPendingTasks(pendingTasks).join('\n ');
+ var hasPendingTimeout = pendingTasks.some(function(task) { return task.type === '$timeout'; });
+ var extraMessage = hasPendingTimeout ? '' : '\n\nNone of the pending tasks are timeouts. ' +
+ 'If you only want to verify pending timeouts, use ' +
+ '`$verifyNoPendingTasks(\'$timeout\')` instead.';
+
+ throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' +
+ formattedTasks + extraMessage);
}
};
- function formatPendingTasksAsString(tasks) {
- var result = [];
- angular.forEach(tasks, function(task) {
- result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}');
- });
-
- return result.join(', ');
- }
-
return $delegate;
}];
@@ -2153,7 +2441,6 @@ angular.mock.$RootElementProvider = function() {
* A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing
* controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}.
*
- *
* ## Example
*
* ```js
@@ -2171,18 +2458,24 @@ angular.mock.$RootElementProvider = function() {
* // Controller definition ...
*
* myMod.controller('MyDirectiveController', ['$log', function($log) {
- * $log.info(this.name);
+ * this.log = function() {
+ * $log.info(this.name);
+ * };
* }]);
*
*
* // In a test ...
*
* describe('myDirectiveController', function() {
- * it('should write the bound name to the log', inject(function($controller, $log) {
- * var ctrl = $controller('MyDirectiveController', { /* no locals &#42;/ }, { name: 'Clark Kent' });
- * expect(ctrl.name).toEqual('Clark Kent');
- * expect($log.info.logs).toEqual(['Clark Kent']);
- * }));
+ * describe('log()', function() {
+ * it('should write the bound name to the log', inject(function($controller, $log) {
+ * var ctrl = $controller('MyDirectiveController', { /* no locals &#42;/ }, { name: 'Clark Kent' });
+ * ctrl.log();
+ *
+ * expect(ctrl.name).toEqual('Clark Kent');
+ * expect($log.info.logs).toEqual(['Clark Kent']);
+ * }));
+ * });
* });
*
* ```
@@ -2193,45 +2486,51 @@ angular.mock.$RootElementProvider = function() {
*
* * check if a controller with given name is registered via `$controllerProvider`
* * check if evaluating the string on the current scope returns a constructor
- * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
- * `window` object (not recommended)
*
* The string can use the `controller as property` syntax, where the controller instance is published
* as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
* to work correctly.
*
* @param {Object} locals Injection locals for Controller.
- * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used
- * to simulate the `bindToController` feature and simplify certain kinds of tests.
+ * @param {Object=} bindings Properties to add to the controller instance. This is used to simulate
+ * the `bindToController` feature and simplify certain kinds of tests.
* @return {Object} Instance of given controller.
*/
-angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
- return function(expression, locals, later, ident) {
- if (later && typeof later === 'object') {
- var instantiate = $delegate(expression, locals, true, ident);
- angular.extend(instantiate.instance, later);
-
- var instance = instantiate();
- if (instance !== instantiate.instance) {
+function createControllerDecorator() {
+ angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
+ return function(expression, locals, later, ident) {
+ if (later && typeof later === 'object') {
+ var instantiate = $delegate(expression, locals, true, ident);
+ var instance = instantiate();
angular.extend(instance, later);
+ return instance;
}
+ return $delegate(expression, locals, later, ident);
+ };
+ }];
- return instance;
- }
- return $delegate(expression, locals, later, ident);
- };
-}];
+ return angular.mock.$ControllerDecorator;
+}
/**
* @ngdoc service
* @name $componentController
* @description
- * A service that can be used to create instances of component controllers.
- * <div class="alert alert-info">
+ * A service that can be used to create instances of component controllers. Useful for unit-testing.
+ *
* Be aware that the controller will be instantiated and attached to the scope as specified in
* the component definition object. If you do not provide a `$scope` object in the `locals` param
* then the helper will create a new isolated scope as a child of `$rootScope`.
- * </div>
+ *
+ * If you are using `$element` or `$attrs` in the controller, make sure to provide them as `locals`.
+ * The `$element` must be a jqLite-wrapped DOM element, and `$attrs` should be an object that
+ * has all properties / functions that you are using in the controller. If this is getting too complex,
+ * you should compile the component instead and access the component's controller via the
+ * {@link angular.element#methods `controller`} function.
+ *
+ * See also the section on {@link guide/component#unit-testing-component-controllers unit-testing component controllers}
+ * in the guide.
+ *
* @param {string} componentName the name of the component whose controller we want to instantiate
* @param {Object} locals Injection locals for Controller.
* @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used
@@ -2239,7 +2538,8 @@ angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
* @param {string=} ident Override the property name to use when attaching the controller to the scope.
* @return {Object} Instance of requested controller.
*/
-angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compileProvider) {
+angular.mock.$ComponentControllerProvider = ['$compileProvider',
+ function ComponentControllerProvider($compileProvider) {
this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) {
return function $componentController(componentName, locals, bindings, ident) {
// get all directives associated to the component name
@@ -2273,21 +2573,17 @@ angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compi
* @packageName angular-mocks
* @description
*
- * # ngMock
- *
- * The `ngMock` module provides support to inject and mock Angular services into unit tests.
- * In addition, ngMock also extends various core ng services such that they can be
+ * The `ngMock` module provides support to inject and mock AngularJS services into unit tests.
+ * In addition, ngMock also extends various core AngularJS services such that they can be
* inspected and controlled in a synchronous manner within test code.
*
- *
- * <div doc-module-components="ngMock"></div>
- *
* @installation
*
* First, download the file:
* * [Google CDN](https://developers.google.com/speed/libraries/devguide#angularjs) e.g.
* `"//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/angular-mocks.js"`
* * [NPM](https://www.npmjs.com/) e.g. `npm install angular-mocks@X.Y.Z`
+ * * [Yarn](https://yarnpkg.com) e.g. `yarn add angular-mocks@X.Y.Z`
* * [Bower](http://bower.io) e.g. `bower install angular-mocks#X.Y.Z`
* * [code.angularjs.org](https://code.angularjs.org/) (discouraged for production use) e.g.
* `"//code.angularjs.org/X.Y.Z/angular-mocks.js"`
@@ -2316,15 +2612,17 @@ angular.module('ngMock', ['ng']).provider({
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$interval: angular.mock.$IntervalProvider,
- $httpBackend: angular.mock.$HttpBackendProvider,
$rootElement: angular.mock.$RootElementProvider,
- $componentController: angular.mock.$ComponentControllerProvider
-}).config(['$provide', function($provide) {
+ $componentController: angular.mock.$ComponentControllerProvider,
+ $flushPendingTasks: angular.mock.$FlushPendingTasksProvider,
+ $verifyNoPendingTasks: angular.mock.$VerifyNoPendingTasksProvider
+}).config(['$provide', '$compileProvider', function($provide, $compileProvider) {
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
$provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
- $provide.decorator('$controller', angular.mock.$ControllerDecorator);
-}]);
+ $provide.decorator('$controller', createControllerDecorator($compileProvider));
+ $provide.decorator('$httpBackend', angular.mock.$httpBackendDecorator);
+}]).info({ angularVersion: '"1.8.2"' });
/**
* @ngdoc module
@@ -2333,14 +2631,13 @@ angular.module('ngMock', ['ng']).provider({
* @packageName angular-mocks
* @description
*
- * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing.
+ * The `ngMockE2E` is an AngularJS module which contains mocks suitable for end-to-end testing.
* Currently there is only one mock present in this module -
* the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock.
*/
angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
- $provide.value('$httpBackend', angular.injector(['ng']).get('$httpBackend'));
$provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
-}]);
+}]).info({ angularVersion: '"1.8.2"' });
/**
* @ngdoc service
@@ -2387,19 +2684,19 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* phones.push(phone);
* return [200, phone, {}];
* });
- * $httpBackend.whenGET(/^\/templates\//).passThrough(); // Requests for templare are handled by the real server
+ * $httpBackend.whenGET(/^\/templates\//).passThrough(); // Requests for templates are handled by the real server
* //...
* });
* ```
*
* Afterwards, bootstrap your app with this new module.
*
- * ## Example
+ * @example
* <example name="httpbackend-e2e-testing" module="myAppE2E" deps="angular-mocks.js">
* <file name="app.js">
* var myApp = angular.module('myApp', []);
*
- * myApp.controller('main', function($http) {
+ * myApp.controller('MainCtrl', function MainCtrl($http) {
* var ctrl = this;
*
* ctrl.phones = [];
@@ -2441,7 +2738,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* });
* </file>
* <file name="index.html">
- * <div ng-controller="main as $ctrl">
+ * <div ng-controller="MainCtrl as $ctrl">
* <form name="newPhoneForm" ng-submit="$ctrl.addPhone($ctrl.newPhone)">
* <input type="text" ng-model="$ctrl.newPhone.name">
* <input type="submit" value="Add Phone">
@@ -2465,9 +2762,10 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* Creates a new backend definition.
*
* @param {string} method HTTP method.
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
- * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
+ * data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
@@ -2497,7 +2795,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
@@ -2514,7 +2812,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
@@ -2531,7 +2829,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
@@ -2548,9 +2846,10 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
- * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
+ * data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
* {@link ngMock.$httpBackend $httpBackend mock}.
@@ -2566,9 +2865,10 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
- * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
+ * data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
* {@link ngMock.$httpBackend $httpBackend mock}.
@@ -2584,9 +2884,10 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for PATCH requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
- * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
+ * data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
* {@link ngMock.$httpBackend $httpBackend mock}.
@@ -2602,7 +2903,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
- * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
+ * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
* {@link ngMock.$httpBackend $httpBackend mock}.
@@ -2623,6 +2924,39 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
*/
+/**
+ * @ngdoc method
+ * @name $httpBackend#matchLatestDefinitionEnabled
+ * @module ngMockE2E
+ * @description
+ * This method can be used to change which mocked responses `$httpBackend` returns, when defining
+ * them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
+ * By default, `$httpBackend` returns the first definition that matches. When setting
+ * `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the
+ * one that was added last.
+ *
+ * ```js
+ * hb.when('GET', '/url1').respond(200, 'content', {});
+ * hb.when('GET', '/url1').respond(201, 'another', {});
+ * hb('GET', '/url1'); // receives "content"
+ *
+ * $http.matchLatestDefinitionEnabled(true)
+ * hb('GET', '/url1'); // receives "another"
+ *
+ * hb.when('GET', '/url1').respond(201, 'onemore', {});
+ * hb('GET', '/url1'); // receives "onemore"
+ * ```
+ *
+ * This is useful if a you have a default response that is overriden inside specific tests.
+ *
+ * Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed
+ * even when the application is already running.
+ *
+ * @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
+ * If omitted, it will return the current value.
+ * @return {$httpBackend|Boolean} self when used as a setter, and the current value when used
+ * as a getter
+ */
angular.mock.e2e = {};
angular.mock.e2e.$httpBackendDecorator =
['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock];
@@ -2654,6 +2988,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
* @ngdoc method
* @name $rootScope.Scope#$countChildScopes
* @module ngMock
+ * @this $rootScope.Scope
* @description
* Counts all the direct and indirect child scopes of the current scope.
*
@@ -2662,7 +2997,6 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
* @returns {number} Total number of child scopes.
*/
function countChildScopes() {
- // jshint validthis: true
var count = 0; // exclude the current scope
var pendingChildHeads = [this.$$childHead];
var currentScope;
@@ -2684,6 +3018,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
/**
* @ngdoc method
* @name $rootScope.Scope#$countWatchers
+ * @this $rootScope.Scope
* @module ngMock
* @description
* Counts all the watchers of direct and indirect child scopes of the current scope.
@@ -2694,7 +3029,6 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
* @returns {number} Total number of watchers.
*/
function countWatchers() {
- // jshint validthis: true
var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope
var pendingChildHeads = [this.$$childHead];
var currentScope;
@@ -2714,7 +3048,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
}];
-!(function(jasmineOrMocha) {
+(function(jasmineOrMocha) {
if (!jasmineOrMocha) {
return;
@@ -2809,7 +3143,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
*
* You cannot call `sharedInjector()` from within a context already using `sharedInjector()`.
*
- * ## Example
+ * ## Example
*
* Typically beforeAll is used to make many assertions about a single operation. This can
* cut down test run-time as the test setup doesn't need to be re-run, and enabling focussed
@@ -2847,14 +3181,14 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
*/
module.sharedInjector = function() {
if (!(module.$$beforeAllHook && module.$$afterAllHook)) {
- throw Error("sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll");
+ throw Error('sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll');
}
var initialized = false;
- module.$$beforeAllHook(function() {
+ module.$$beforeAllHook(/** @this */ function() {
if (injectorState.shared) {
- injectorState.sharedError = Error("sharedInjector() cannot be called inside a context that has already called sharedInjector()");
+ injectorState.sharedError = Error('sharedInjector() cannot be called inside a context that has already called sharedInjector()');
throw injectorState.sharedError;
}
initialized = true;
@@ -2873,10 +3207,10 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
};
module.$$beforeEach = function() {
- if (injectorState.shared && currentSpec && currentSpec != this) {
+ if (injectorState.shared && currentSpec && currentSpec !== this) {
var state = currentSpec;
currentSpec = this;
- angular.forEach(["$injector","$modules","$providerInjector", "$injectorStrict"], function(k) {
+ angular.forEach(['$injector','$modules','$providerInjector', '$injectorStrict'], function(k) {
currentSpec[k] = state[k];
state[k] = null;
});
@@ -2900,12 +3234,6 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
delete fn.$inject;
});
- angular.forEach(currentSpec.$modules, function(module) {
- if (module && module.$$hashKey) {
- module.$$hashKey = undefined;
- }
- });
-
currentSpec.$injector = null;
currentSpec.$modules = null;
currentSpec.$providerInjector = null;
@@ -2967,7 +3295,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
* These are ignored by the injector when the reference name is resolved.
*
* For example, the parameter `_myService_` would be resolved as the reference `myService`.
- * Since it is available in the function body as _myService_, we can then assign it to a variable
+ * Since it is available in the function body as `_myService_`, we can then assign it to a variable
* defined in an outer scope.
*
* ```
@@ -3031,7 +3359,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
- var ErrorAddingDeclarationLocationStack = function(e, errorForStack) {
+ var ErrorAddingDeclarationLocationStack = function ErrorAddingDeclarationLocationStack(e, errorForStack) {
this.message = e.message;
this.name = e.name;
if (e.line) this.line = e.line;
@@ -3049,11 +3377,11 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
if (!errorForStack.stack) {
try {
throw errorForStack;
- } catch (e) {}
+ } catch (e) { /* empty */ }
}
- return wasInjectorCreated() ? workFn.call(currentSpec) : workFn;
+ return wasInjectorCreated() ? WorkFn.call(currentSpec) : WorkFn;
/////////////////////
- function workFn() {
+ function WorkFn() {
var modules = currentSpec.$modules || [];
var strictDi = !!currentSpec.$injectorStrict;
modules.unshift(['$injector', function($injector) {
@@ -3066,7 +3394,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
if (strictDi) {
// If strictDi is enabled, annotate the providerInjector blocks
angular.forEach(modules, function(moduleFn) {
- if (typeof moduleFn === "function") {
+ if (typeof moduleFn === 'function') {
angular.injector.$$annotate(moduleFn);
}
});
@@ -3081,9 +3409,7 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
injector.annotate(blockFns[i]);
}
try {
- /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
injector.invoke(blockFns[i] || angular.noop, this);
- /* jshint +W040 */
} catch (e) {
if (e.stack && errorForStack) {
throw new ErrorAddingDeclarationLocationStack(e, errorForStack);
diff --git a/xstatic/pkg/angular/data/angular-parse-ext.js b/xstatic/pkg/angular/data/angular-parse-ext.js
index 42e1335..0edc89d 100644
--- a/xstatic/pkg/angular/data/angular-parse-ext.js
+++ b/xstatic/pkg/angular/data/angular-parse-ext.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -1223,24 +1223,27 @@ function IDC_Y(cp) {
return false;
}
+/* eslint-disable new-cap */
+
/**
* @ngdoc module
* @name ngParseExt
* @packageName angular-parse-ext
- * @description
*
- * # ngParseExt
+ * @description
*
* The `ngParseExt` module provides functionality to allow Unicode characters in
- * identifiers inside Angular expressions.
- *
- *
- * <div doc-module-components="ngParseExt"></div>
+ * identifiers inside AngularJS expressions.
*
* This module allows the usage of any identifier that follows ES6 identifier naming convention
- * to be used as an identifier in an Angular expression. ES6 delegates some of the identifier
+ * to be used as an identifier in an AngularJS expression. ES6 delegates some of the identifier
* rules definition to Unicode, this module uses ES6 and Unicode 8.0 identifiers convention.
*
+ * <div class="alert alert-warning">
+ * You cannot use Unicode characters for variable names in the {@link ngRepeat} or {@link ngOptions}
+ * expressions (e.g. `ng-repeat="f in поля"`), because even with `ngParseExt` included, these
+ * special expressions are not parsed by the {@link $parse} service.
+ * </div>
*/
/* global angularParseExtModule: true,
@@ -1265,7 +1268,8 @@ function isValidIdentifierContinue(ch, cp) {
angular.module('ngParseExt', [])
.config(['$parseProvider', function($parseProvider) {
$parseProvider.setIdentifierFns(isValidIdentifierStart, isValidIdentifierContinue);
- }]);
+ }])
+ .info({ angularVersion: '"1.8.2"' });
})(window, window.angular);
diff --git a/xstatic/pkg/angular/data/angular-resource.js b/xstatic/pkg/angular/data/angular-resource.js
index e8bb301..b98a438 100644
--- a/xstatic/pkg/angular/data/angular-resource.js
+++ b/xstatic/pkg/angular/data/angular-resource.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -53,14 +53,9 @@ function shallowClearAndCopy(src, dst) {
* @name ngResource
* @description
*
- * # ngResource
- *
* The `ngResource` module provides interaction support with RESTful services
* via the $resource service.
*
- *
- * <div doc-module-components="ngResource"></div>
- *
* See {@link ngResource.$resourceProvider} and {@link ngResource.$resource} for usage.
*/
@@ -120,30 +115,35 @@ function shallowClearAndCopy(src, dst) {
*
* @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
* `actions` methods. If a parameter value is a function, it will be called every time
- * a param value needs to be obtained for a request (unless the param was overridden). The function
- * will be passed the current data value as an argument.
+ * a param value needs to be obtained for a request (unless the param was overridden). The
+ * function will be passed the current data value as an argument.
*
* Each key value in the parameter object is first bound to url template if present and then any
* excess keys are appended to the url search query after the `?`.
*
- * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
+ * Given a template `/path/:verb` and parameter `{verb: 'greet', salutation: 'Hello'}` results in
* URL `/path/greet?salutation=Hello`.
*
* If the parameter value is prefixed with `@`, then the value for that parameter will be
- * extracted from the corresponding property on the `data` object (provided when calling a
- * "non-GET" action method).
+ * extracted from the corresponding property on the `data` object (provided when calling actions
+ * with a request body).
* For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of
* `someParam` will be `data.someProp`.
* Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action
- * method that does not accept a request body)
+ * method that does not accept a request body).
+ *
+ * @param {Object.<Object>=} actions Hash with declaration of custom actions that will be available
+ * in addition to the default set of resource actions (see below). If a custom action has the same
+ * key as a default action (e.g. `save`), then the default action will be *overwritten*, and not
+ * extended.
*
- * @param {Object.<Object>=} actions Hash with declaration of custom actions that should extend
- * the default set of resource actions. The declaration should be created in the format of {@link
- * ng.$http#usage $http.config}:
+ * The declaration should be created in the format of {@link ng.$http#usage $http.config}:
*
- * {action1: {method:?, params:?, isArray:?, headers:?, ...},
- * action2: {method:?, params:?, isArray:?, headers:?, ...},
- * ...}
+ * {
+ * action1: {method:?, params:?, isArray:?, headers:?, ...},
+ * action2: {method:?, params:?, isArray:?, headers:?, ...},
+ * ...
+ * }
*
* Where:
*
@@ -155,46 +155,58 @@ function shallowClearAndCopy(src, dst) {
* the parameter value is a function, it will be called every time when a param value needs to
* be obtained for a request (unless the param was overridden). The function will be passed the
* current data value as an argument.
- * - **`url`** – {string} – action specific `url` override. The url templating is supported just
+ * - **`url`** – {string} – Action specific `url` override. The url templating is supported just
* like for the resource-level urls.
* - **`isArray`** – {boolean=} – If true then the returned object for this action is an array,
* see `returns` section.
* - **`transformRequest`** –
* `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
- * transform function or an array of such functions. The transform function takes the http
+ * Transform function or an array of such functions. The transform function takes the http
* request body and headers and returns its transformed (typically serialized) version.
* By default, transformRequest will contain one function that checks if the request data is
- * an object and serializes to using `angular.toJson`. To prevent this behavior, set
+ * an object and serializes it using `angular.toJson`. To prevent this behavior, set
* `transformRequest` to an empty array: `transformRequest: []`
* - **`transformResponse`** –
- * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
- * transform function or an array of such functions. The transform function takes the http
- * response body and headers and returns its transformed (typically deserialized) version.
+ * `{function(data, headersGetter, status)|Array.<function(data, headersGetter, status)>}` –
+ * Transform function or an array of such functions. The transform function takes the HTTP
+ * response body, headers and status and returns its transformed (typically deserialized)
+ * version.
* By default, transformResponse will contain one function that checks if the response looks
* like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior,
* set `transformResponse` to an empty array: `transformResponse: []`
- * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
- * GET request, otherwise if a cache instance built with
- * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
- * caching.
- * - **`timeout`** – `{number}` – timeout in milliseconds.<br />
+ * - **`cache`** – `{boolean|Cache}` – A boolean value or object created with
+ * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response.
+ * See {@link $http#caching $http Caching} for more information.
+ * - **`timeout`** – `{number}` – Timeout in milliseconds.<br />
* **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are
- * **not** supported in $resource, because the same value would be used for multiple requests.
+ * **not** supported in `$resource`, because the same value would be used for multiple requests.
* If you are looking for a way to cancel requests, you should use the `cancellable` option.
- * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call
- * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's
- * return value. Calling `$cancelRequest()` for a non-cancellable or an already
- * completed/cancelled request will have no effect.<br />
- * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
+ * - **`cancellable`** – `{boolean}` – If true, the request made by a "non-instance" call will be
+ * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return
+ * value. Calling `$cancelRequest()` for a non-cancellable or an already completed/cancelled
+ * request will have no effect.
+ * - **`withCredentials`** – `{boolean}` – Whether to set the `withCredentials` flag on the
* XHR object. See
- * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5)
+ * [XMLHttpRequest.withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
* for more information.
- * - **`responseType`** - `{string}` - see
- * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
- * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
- * `response` and `responseError`. Both `response` and `responseError` interceptors get called
- * with `http response` object. See {@link ng.$http $http interceptors}.
- *
+ * - **`responseType`** – `{string}` – See
+ * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType).
+ * - **`interceptor`** – `{Object=}` – The interceptor object has four optional methods -
+ * `request`, `requestError`, `response`, and `responseError`. See
+ * {@link ng.$http#interceptors $http interceptors} for details. Note that
+ * `request`/`requestError` interceptors are applied before calling `$http`, thus before any
+ * global `$http` interceptors. Also, rejecting or throwing an error inside the `request`
+ * interceptor will result in calling the `responseError` interceptor.
+ * The resource instance or collection is available on the `resource` property of the
+ * `http response` object passed to `response`/`responseError` interceptors.
+ * Keep in mind that the associated promise will be resolved with the value returned by the
+ * response interceptors. Make sure you return an appropriate value and not the `response`
+ * object passed as input. For reference, the default `response` interceptor (which gets applied
+ * if you don't specify a custom one) returns `response.resource`.<br />
+ * See {@link ngResource.$resource#using-interceptors below} for an example of using
+ * interceptors in `$resource`.
+ * - **`hasBody`** – `{boolean}` – If true, then the request will have a body.
+ * If not specified, then only POST, PUT and PATCH requests will have a body. *
* @param {Object} options Hash with custom settings that should extend the
* default `$resourceProvider` behavior. The supported options are:
*
@@ -207,27 +219,29 @@ function shallowClearAndCopy(src, dst) {
* @returns {Object} A resource "class" object with methods for the default set of resource actions
* optionally extended with custom `actions`. The default set contains these actions:
* ```js
- * { 'get': {method:'GET'},
- * 'save': {method:'POST'},
- * 'query': {method:'GET', isArray:true},
- * 'remove': {method:'DELETE'},
- * 'delete': {method:'DELETE'} };
+ * {
+ * 'get': {method: 'GET'},
+ * 'save': {method: 'POST'},
+ * 'query': {method: 'GET', isArray: true},
+ * 'remove': {method: 'DELETE'},
+ * 'delete': {method: 'DELETE'}
+ * }
* ```
*
- * Calling these methods invoke an {@link ng.$http} with the specified http method,
- * destination and parameters. When the data is returned from the server then the object is an
- * instance of the resource class. The actions `save`, `remove` and `delete` are available on it
- * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
- * read, update, delete) on server-side data like this:
+ * Calling these methods invoke {@link ng.$http} with the specified http method, destination and
+ * parameters. When the data is returned from the server then the object is an instance of the
+ * resource class. The actions `save`, `remove` and `delete` are available on it as methods with
+ * the `$` prefix. This allows you to easily perform CRUD operations (create, read, update,
+ * delete) on server-side data like this:
* ```js
- * var User = $resource('/user/:userId', {userId:'@id'});
- * var user = User.get({userId:123}, function() {
+ * var User = $resource('/user/:userId', {userId: '@id'});
+ * User.get({userId: 123}).$promise.then(function(user) {
* user.abc = true;
* user.$save();
* });
* ```
*
- * It is important to realize that invoking a $resource object method immediately returns an
+ * It is important to realize that invoking a `$resource` object method immediately returns an
* empty reference (object or array depending on `isArray`). Once the data is returned from the
* server the existing reference is populated with the actual data. This is a useful trick since
* usually the resource is assigned to a model which is then rendered by the view. Having an empty
@@ -238,37 +252,43 @@ function shallowClearAndCopy(src, dst) {
* The action methods on the class object or instance object can be invoked with the following
* parameters:
*
- * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
- * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
- * - non-GET instance actions: `instance.$action([parameters], [success], [error])`
+ * - "class" actions without a body: `Resource.action([parameters], [success], [error])`
+ * - "class" actions with a body: `Resource.action([parameters], postData, [success], [error])`
+ * - instance actions: `instance.$action([parameters], [success], [error])`
+ *
+ *
+ * When calling instance methods, the instance itself is used as the request body (if the action
+ * should have a body). By default, only actions using `POST`, `PUT` or `PATCH` have request
+ * bodies, but you can use the `hasBody` configuration option to specify whether an action
+ * should have a body or not (regardless of its HTTP method).
*
*
- * Success callback is called with (value, responseHeaders) arguments, where the value is
- * the populated resource instance or collection object. The error callback is called
- * with (httpResponse) argument.
+ * Success callback is called with (value (Object|Array), responseHeaders (Function),
+ * status (number), statusText (string)) arguments, where `value` is the populated resource
+ * instance or collection object. The error callback is called with (httpResponse) argument.
*
- * Class actions return empty instance (with additional properties below).
- * Instance actions return promise of the action.
+ * Class actions return an empty instance (with the additional properties listed below).
+ * Instance actions return a promise for the operation.
*
* The Resource instances and collections have these additional properties:
*
- * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
+ * - `$promise`: The {@link ng.$q promise} of the original server interaction that created this
* instance or collection.
*
* On success, the promise is resolved with the same resource instance or collection object,
- * updated with data from server. This makes it easy to use in
- * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view
+ * updated with data from server. This makes it easy to use in the
+ * {@link ngRoute.$routeProvider `resolve` section of `$routeProvider.when()`} to defer view
* rendering until the resource(s) are loaded.
*
- * On failure, the promise is rejected with the {@link ng.$http http response} object, without
- * the `resource` property.
+ * On failure, the promise is rejected with the {@link ng.$http http response} object.
*
* If an interceptor object was provided, the promise will instead be resolved with the value
- * returned by the interceptor.
+ * returned by the response interceptor (on success) or responceError interceptor (on failure).
*
* - `$resolved`: `true` after first server interaction is completed (either with success or
* rejection), `false` before that. Knowing if the Resource has been resolved is useful in
- * data-binding.
+ * data-binding. If there is a response/responseError interceptor and it returns a promise,
+ * `$resolved` will wait for that too.
*
* The Resource instances and collections have these additional methods:
*
@@ -279,138 +299,145 @@ function shallowClearAndCopy(src, dst) {
*
* - `toJSON`: It returns a simple object without any of the extra properties added as part of
* the Resource API. This object can be serialized through {@link angular.toJson} safely
- * without attaching Angular-specific fields. Notice that `JSON.stringify` (and
+ * without attaching AngularJS-specific fields. Notice that `JSON.stringify` (and
* `angular.toJson`) automatically use this method when serializing a Resource instance
- * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior)).
+ * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior)).
*
* @example
*
- * # Credit card resource
+ * ### Basic usage
*
- * ```js
- // Define CreditCard class
- var CreditCard = $resource('/user/:userId/card/:cardId',
- {userId:123, cardId:'@id'}, {
- charge: {method:'POST', params:{charge:true}}
- });
+ ```js
+ // Define a CreditCard class
+ var CreditCard = $resource('/users/:userId/cards/:cardId',
+ {userId: 123, cardId: '@id'}, {
+ charge: {method: 'POST', params: {charge: true}}
+ });
// We can retrieve a collection from the server
- var cards = CreditCard.query(function() {
- // GET: /user/123/card
- // server returns: [ {id:456, number:'1234', name:'Smith'} ];
+ var cards = CreditCard.query();
+ // GET: /users/123/cards
+ // server returns: [{id: 456, number: '1234', name: 'Smith'}]
+ // Wait for the request to complete
+ cards.$promise.then(function() {
var card = cards[0];
- // each item is an instance of CreditCard
+
+ // Each item is an instance of CreditCard
expect(card instanceof CreditCard).toEqual(true);
- card.name = "J. Smith";
- // non GET methods are mapped onto the instances
+
+ // Non-GET methods are mapped onto the instances
+ card.name = 'J. Smith';
card.$save();
- // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
- // server returns: {id:456, number:'1234', name: 'J. Smith'};
+ // POST: /users/123/cards/456 {id: 456, number: '1234', name: 'J. Smith'}
+ // server returns: {id: 456, number: '1234', name: 'J. Smith'}
- // our custom method is mapped as well.
- card.$charge({amount:9.99});
- // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
+ // Our custom method is mapped as well (since it uses POST)
+ card.$charge({amount: 9.99});
+ // POST: /users/123/cards/456?amount=9.99&charge=true {id: 456, number: '1234', name: 'J. Smith'}
});
- // we can create an instance as well
- var newCard = new CreditCard({number:'0123'});
- newCard.name = "Mike Smith";
- newCard.$save();
- // POST: /user/123/card {number:'0123', name:'Mike Smith'}
- // server returns: {id:789, number:'0123', name: 'Mike Smith'};
- expect(newCard.id).toEqual(789);
- * ```
+ // We can create an instance as well
+ var newCard = new CreditCard({number: '0123'});
+ newCard.name = 'Mike Smith';
+
+ var savePromise = newCard.$save();
+ // POST: /users/123/cards {number: '0123', name: 'Mike Smith'}
+ // server returns: {id: 789, number: '0123', name: 'Mike Smith'}
+
+ savePromise.then(function() {
+ // Once the promise is resolved, the created instance
+ // is populated with the data returned by the server
+ expect(newCard.id).toEqual(789);
+ });
+ ```
*
- * The object returned from this function execution is a resource "class" which has "static" method
- * for each action in the definition.
+ * The object returned from a call to `$resource` is a resource "class" which has one "static"
+ * method for each action in the definition.
*
- * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and
- * `headers`.
+ * Calling these methods invokes `$http` on the `url` template with the given HTTP `method`,
+ * `params` and `headers`.
*
* @example
*
- * # User resource
+ * ### Accessing the response
*
* When the data is returned from the server then the object is an instance of the resource type and
* all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
* operations (create, read, update, delete) on server-side data.
-
+ *
```js
- var User = $resource('/user/:userId', {userId:'@id'});
- User.get({userId:123}, function(user) {
+ var User = $resource('/users/:userId', {userId: '@id'});
+ User.get({userId: 123}).$promise.then(function(user) {
user.abc = true;
user.$save();
});
```
*
- * It's worth noting that the success callback for `get`, `query` and other methods gets passed
- * in the response that came from the server as well as $http header getter function, so one
- * could rewrite the above example and get access to http headers as:
+ * It's worth noting that the success callback for `get`, `query` and other methods gets called with
+ * the resource instance (populated with the data that came from the server) as well as an `$http`
+ * header getter function, the HTTP status code and the response status text. So one could rewrite
+ * the above example and get access to HTTP headers as follows:
*
```js
- var User = $resource('/user/:userId', {userId:'@id'});
- User.get({userId:123}, function(user, getResponseHeaders){
+ var User = $resource('/users/:userId', {userId: '@id'});
+ User.get({userId: 123}, function(user, getResponseHeaders) {
user.abc = true;
user.$save(function(user, putResponseHeaders) {
- //user => saved user object
- //putResponseHeaders => $http header getter
+ // `user` => saved `User` object
+ // `putResponseHeaders` => `$http` header getter
});
});
```
*
- * You can also access the raw `$http` promise via the `$promise` property on the object returned
- *
- ```
- var User = $resource('/user/:userId', {userId:'@id'});
- User.get({userId:123})
- .$promise.then(function(user) {
- $scope.user = user;
- });
- ```
- *
* @example
*
- * # Creating a custom 'PUT' request
+ * ### Creating custom actions
*
- * In this example we create a custom method on our resource to make a PUT request
- * ```js
- * var app = angular.module('app', ['ngResource', 'ngRoute']);
- *
- * // Some APIs expect a PUT request in the format URL/object/ID
- * // Here we are creating an 'update' method
- * app.factory('Notes', ['$resource', function($resource) {
- * return $resource('/notes/:id', null,
- * {
- * 'update': { method:'PUT' }
- * });
- * }]);
- *
- * // In our controller we get the ID from the URL using ngRoute and $routeParams
- * // We pass in $routeParams and our Notes factory along with $scope
- * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes',
- function($scope, $routeParams, Notes) {
- * // First get a note object from the factory
- * var note = Notes.get({ id:$routeParams.id });
- * $id = note.id;
- *
- * // Now call update passing in the ID first then the object you are updating
- * Notes.update({ id:$id }, note);
- *
- * // This will PUT /notes/ID with the note object in the request payload
- * }]);
- * ```
+ * In this example we create a custom method on our resource to make a PUT request:
+ *
+ ```js
+ var app = angular.module('app', ['ngResource']);
+
+ // Some APIs expect a PUT request in the format URL/object/ID
+ // Here we are creating an 'update' method
+ app.factory('Notes', ['$resource', function($resource) {
+ return $resource('/notes/:id', {id: '@id'}, {
+ update: {method: 'PUT'}
+ });
+ }]);
+
+ // In our controller we get the ID from the URL using `$location`
+ app.controller('NotesCtrl', ['$location', 'Notes', function($location, Notes) {
+ // First, retrieve the corresponding `Note` object from the server
+ // (Assuming a URL of the form `.../notes?id=XYZ`)
+ var noteId = $location.search().id;
+ var note = Notes.get({id: noteId});
+
+ note.$promise.then(function() {
+ note.content = 'Hello, world!';
+
+ // Now call `update` to save the changes on the server
+ Notes.update(note);
+ // This will PUT /notes/ID with the note object as the request payload
+
+ // Since `update` is a non-GET method, it will also be available on the instance
+ // (prefixed with `$`), so we could replace the `Note.update()` call with:
+ //note.$update();
+ });
+ }]);
+ ```
*
* @example
*
- * # Cancelling requests
+ * ### Cancelling requests
*
* If an action's configuration specifies that it is cancellable, you can cancel the request related
* to an instance or collection (as long as it is a result of a "non-instance" call):
*
```js
// ...defining the `Hotel` resource...
- var Hotel = $resource('/api/hotel/:id', {id: '@id'}, {
+ var Hotel = $resource('/api/hotels/:id', {id: '@id'}, {
// Let's make the `query()` method cancellable
query: {method: 'get', isArray: true, cancellable: true}
});
@@ -420,18 +447,60 @@ function shallowClearAndCopy(src, dst) {
this.onDestinationChanged = function onDestinationChanged(destination) {
// We don't care about any pending request for hotels
// in a different destination any more
- this.availableHotels.$cancelRequest();
+ if (this.availableHotels) {
+ this.availableHotels.$cancelRequest();
+ }
- // Let's query for hotels in '<destination>'
- // (calls: /api/hotel?location=<destination>)
+ // Let's query for hotels in `destination`
+ // (calls: /api/hotels?location=<destination>)
this.availableHotels = Hotel.query({location: destination});
};
```
*
+ * @example
+ *
+ * ### Using interceptors
+ *
+ * You can use interceptors to transform the request or response, perform additional operations, and
+ * modify the returned instance/collection. The following example, uses `request` and `response`
+ * interceptors to augment the returned instance with additional info:
+ *
+ ```js
+ var Thing = $resource('/api/things/:id', {id: '@id'}, {
+ save: {
+ method: 'POST',
+ interceptor: {
+ request: function(config) {
+ // Before the request is sent out, store a timestamp on the request config
+ config.requestTimestamp = Date.now();
+ return config;
+ },
+ response: function(response) {
+ // Get the instance from the response object
+ var instance = response.resource;
+
+ // Augment the instance with a custom `saveLatency` property, computed as the time
+ // between sending the request and receiving the response.
+ instance.saveLatency = Date.now() - response.config.requestTimestamp;
+
+ // Return the instance
+ return instance;
+ }
+ }
+ }
+ });
+
+ Thing.save({foo: 'bar'}).$promise.then(function(thing) {
+ console.log('That thing was saved in ' + thing.saveLatency + 'ms.');
+ });
+ ```
+ *
*/
angular.module('ngResource', ['ng']).
- provider('$resource', function() {
- var PROTOCOL_AND_DOMAIN_REGEX = /^https?:\/\/[^\/]*/;
+ info({ angularVersion: '"1.8.2"' }).
+ provider('$resource', function ResourceProvider() {
+ var PROTOCOL_AND_IPV6_REGEX = /^https?:\/\/\[[^\]]*][^/]*/;
+
var provider = this;
/**
@@ -475,11 +544,11 @@ angular.module('ngResource', ['ng']).
* ```js
* angular.
* module('myApp').
- * config(['resourceProvider', function ($resourceProvider) {
+ * config(['$resourceProvider', function ($resourceProvider) {
* $resourceProvider.defaults.actions.update = {
* method: 'PUT'
* };
- * });
+ * }]);
* ```
*
* Or you can even overwrite the whole `actions` list and specify your own:
@@ -487,9 +556,9 @@ angular.module('ngResource', ['ng']).
* ```js
* angular.
* module('myApp').
- * config(['resourceProvider', function ($resourceProvider) {
+ * config(['$resourceProvider', function ($resourceProvider) {
* $resourceProvider.defaults.actions = {
- * create: {method: 'POST'}
+ * create: {method: 'POST'},
* get: {method: 'GET'},
* getAll: {method: 'GET', isArray:true},
* update: {method: 'PUT'},
@@ -519,49 +588,15 @@ angular.module('ngResource', ['ng']).
this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) {
var noop = angular.noop,
- forEach = angular.forEach,
- extend = angular.extend,
- copy = angular.copy,
- isFunction = angular.isFunction;
-
- /**
- * We need our custom method because encodeURIComponent is too aggressive and doesn't follow
- * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set
- * (pchar) allowed in path segments:
- * segment = *pchar
- * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
- * pct-encoded = "%" HEXDIG HEXDIG
- * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
- * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
- * / "*" / "+" / "," / ";" / "="
- */
- function encodeUriSegment(val) {
- return encodeUriQuery(val, true).
- replace(/%26/gi, '&').
- replace(/%3D/gi, '=').
- replace(/%2B/gi, '+');
- }
-
-
- /**
- * This method is intended for encoding *key* or *value* parts of query component. We need a
- * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't
- * have to be encoded per http://tools.ietf.org/html/rfc3986:
- * query = *( pchar / "/" / "?" )
- * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
- * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
- * pct-encoded = "%" HEXDIG HEXDIG
- * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
- * / "*" / "+" / "," / ";" / "="
- */
- function encodeUriQuery(val, pctEncodeSpaces) {
- return encodeURIComponent(val).
- replace(/%40/gi, '@').
- replace(/%3A/gi, ':').
- replace(/%24/g, '$').
- replace(/%2C/gi, ',').
- replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
- }
+ forEach = angular.forEach,
+ extend = angular.extend,
+ copy = angular.copy,
+ isArray = angular.isArray,
+ isDefined = angular.isDefined,
+ isFunction = angular.isFunction,
+ isNumber = angular.isNumber,
+ encodeUriQuery = angular.$$encodeUriQuery,
+ encodeUriSegment = angular.$$encodeUriSegment;
function Route(template, defaults) {
this.template = template;
@@ -575,42 +610,42 @@ angular.module('ngResource', ['ng']).
url = actionUrl || self.template,
val,
encodedVal,
- protocolAndDomain = '';
+ protocolAndIpv6 = '';
- var urlParams = self.urlParams = {};
+ var urlParams = self.urlParams = Object.create(null);
forEach(url.split(/\W/), function(param) {
if (param === 'hasOwnProperty') {
- throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name.");
+ throw $resourceMinErr('badname', 'hasOwnProperty is not a valid parameter name.');
}
- if (!(new RegExp("^\\d+$").test(param)) && param &&
- (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
+ if (!(new RegExp('^\\d+$').test(param)) && param &&
+ (new RegExp('(^|[^\\\\]):' + param + '(\\W|$)').test(url))) {
urlParams[param] = {
- isQueryParamValue: (new RegExp("\\?.*=:" + param + "(?:\\W|$)")).test(url)
+ isQueryParamValue: (new RegExp('\\?.*=:' + param + '(?:\\W|$)')).test(url)
};
}
});
url = url.replace(/\\:/g, ':');
- url = url.replace(PROTOCOL_AND_DOMAIN_REGEX, function(match) {
- protocolAndDomain = match;
+ url = url.replace(PROTOCOL_AND_IPV6_REGEX, function(match) {
+ protocolAndIpv6 = match;
return '';
});
params = params || {};
forEach(self.urlParams, function(paramInfo, urlParam) {
val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
- if (angular.isDefined(val) && val !== null) {
+ if (isDefined(val) && val !== null) {
if (paramInfo.isQueryParamValue) {
encodedVal = encodeUriQuery(val, true);
} else {
encodedVal = encodeUriSegment(val);
}
- url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) {
+ url = url.replace(new RegExp(':' + urlParam + '(\\W|$)', 'g'), function(match, p1) {
return encodedVal + p1;
});
} else {
- url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match,
+ url = url.replace(new RegExp('(/?):' + urlParam + '(\\W|$)', 'g'), function(match,
leadingSlashes, tail) {
- if (tail.charAt(0) == '/') {
+ if (tail.charAt(0) === '/') {
return tail;
} else {
return leadingSlashes + tail;
@@ -624,11 +659,12 @@ angular.module('ngResource', ['ng']).
url = url.replace(/\/+$/, '') || '/';
}
- // then replace collapse `/.` if found in the last URL path segment before the query
- // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x`
+ // Collapse `/.` if found in the last URL path segment before the query.
+ // E.g. `http://url.com/id/.format?q=x` becomes `http://url.com/id.format?q=x`.
url = url.replace(/\/\.(?=\w+($|\?))/, '.');
- // replace escaped `/\.` with `/.`
- config.url = protocolAndDomain + url.replace(/\/\\\./, '/.');
+ // Replace escaped `/\.` with `/.`.
+ // (If `\.` comes from a param value, it will be encoded as `%5C.`.)
+ config.url = protocolAndIpv6 + url.replace(/\/(\\|%5C)\./, '/.');
// set params - delegate param encoding to $http
@@ -652,7 +688,7 @@ angular.module('ngResource', ['ng']).
actionParams = extend({}, paramDefaults, actionParams);
forEach(actionParams, function(value, key) {
if (isFunction(value)) { value = value(data); }
- ids[key] = value && value.charAt && value.charAt(0) == '@' ?
+ ids[key] = value && value.charAt && value.charAt(0) === '@' ?
lookupDottedPath(data, value.substr(1)) : value;
});
return ids;
@@ -670,17 +706,17 @@ angular.module('ngResource', ['ng']).
var data = extend({}, this);
delete data.$promise;
delete data.$resolved;
+ delete data.$cancelRequest;
return data;
};
forEach(actions, function(action, name) {
- var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);
+ var hasBody = action.hasBody === true || (action.hasBody !== false && /^(POST|PUT|PATCH)$/i.test(action.method));
var numericTimeout = action.timeout;
- var cancellable = angular.isDefined(action.cancellable) ? action.cancellable :
- (options && angular.isDefined(options.cancellable)) ? options.cancellable :
- provider.defaults.cancellable;
+ var cancellable = isDefined(action.cancellable) ?
+ action.cancellable : route.defaults.cancellable;
- if (numericTimeout && !angular.isNumber(numericTimeout)) {
+ if (numericTimeout && !isNumber(numericTimeout)) {
$log.debug('ngResource:\n' +
' Only numeric values are allowed as `timeout`.\n' +
' Promises are not supported in $resource, because the same value would ' +
@@ -691,54 +727,61 @@ angular.module('ngResource', ['ng']).
}
Resource[name] = function(a1, a2, a3, a4) {
- var params = {}, data, success, error;
+ var params = {}, data, onSuccess, onError;
- /* jshint -W086 */ /* (purposefully fall through case statements) */
switch (arguments.length) {
case 4:
- error = a4;
- success = a3;
- //fallthrough
+ onError = a4;
+ onSuccess = a3;
+ // falls through
case 3:
case 2:
if (isFunction(a2)) {
if (isFunction(a1)) {
- success = a1;
- error = a2;
+ onSuccess = a1;
+ onError = a2;
break;
}
- success = a2;
- error = a3;
- //fallthrough
+ onSuccess = a2;
+ onError = a3;
+ // falls through
} else {
params = a1;
data = a2;
- success = a3;
+ onSuccess = a3;
break;
}
+ // falls through
case 1:
- if (isFunction(a1)) success = a1;
+ if (isFunction(a1)) onSuccess = a1;
else if (hasBody) data = a1;
else params = a1;
break;
case 0: break;
default:
throw $resourceMinErr('badargs',
- "Expected up to 4 arguments [params, data, success, error], got {0} arguments",
+ 'Expected up to 4 arguments [params, data, success, error], got {0} arguments',
arguments.length);
}
- /* jshint +W086 */ /* (purposefully fall through case statements) */
var isInstanceCall = this instanceof Resource;
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
var httpConfig = {};
+ var requestInterceptor = action.interceptor && action.interceptor.request || undefined;
+ var requestErrorInterceptor = action.interceptor && action.interceptor.requestError ||
+ undefined;
var responseInterceptor = action.interceptor && action.interceptor.response ||
defaultResponseInterceptor;
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
- undefined;
+ $q.reject;
+ var successCallback = onSuccess ? function(val) {
+ onSuccess(val, response.headers, response.status, response.statusText);
+ } : undefined;
+ var errorCallback = onError || undefined;
var timeoutDeferred;
var numericTimeoutPromise;
+ var response;
forEach(action, function(value, key) {
switch (key) {
@@ -767,23 +810,28 @@ angular.module('ngResource', ['ng']).
extend({}, extractParams(data, action.params || {}), params),
action.url);
- var promise = $http(httpConfig).then(function(response) {
- var data = response.data;
+ // Start the promise chain
+ var promise = $q.
+ resolve(httpConfig).
+ then(requestInterceptor).
+ catch(requestErrorInterceptor).
+ then($http);
+
+ promise = promise.then(function(resp) {
+ var data = resp.data;
if (data) {
// Need to convert action.isArray to boolean in case it is undefined
- // jshint -W018
- if (angular.isArray(data) !== (!!action.isArray)) {
+ if (isArray(data) !== (!!action.isArray)) {
throw $resourceMinErr('badcfg',
'Error in resource configuration for action `{0}`. Expected response to ' +
'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object',
- angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url);
+ isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url);
}
- // jshint +W018
if (action.isArray) {
value.length = 0;
forEach(data, function(item) {
- if (typeof item === "object") {
+ if (typeof item === 'object') {
value.push(new Resource(item));
} else {
// Valid JSON values may be string literals, and these should not be converted
@@ -798,30 +846,27 @@ angular.module('ngResource', ['ng']).
value.$promise = promise; // Restore the promise
}
}
- response.resource = value;
- return response;
- }, function(response) {
- (error || noop)(response);
- return $q.reject(response);
+ resp.resource = value;
+ response = resp;
+ return responseInterceptor(resp);
+ }, function(rejectionOrResponse) {
+ rejectionOrResponse.resource = value;
+ response = rejectionOrResponse;
+ return responseErrorInterceptor(rejectionOrResponse);
});
- promise['finally'](function() {
+ promise = promise['finally'](function() {
value.$resolved = true;
if (!isInstanceCall && cancellable) {
- value.$cancelRequest = angular.noop;
+ value.$cancelRequest = noop;
$timeout.cancel(numericTimeoutPromise);
timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null;
}
});
- promise = promise.then(
- function(response) {
- var value = responseInterceptor(response);
- (success || noop)(value, response.headers);
- return value;
- },
- responseErrorInterceptor);
+ // Run the `success`/`error` callbacks, but do not let them affect the returned promise.
+ promise.then(successCallback, errorCallback);
if (!isInstanceCall) {
// we are creating instance / collection
@@ -829,13 +874,20 @@ angular.module('ngResource', ['ng']).
// - return the instance / collection
value.$promise = promise;
value.$resolved = false;
- if (cancellable) value.$cancelRequest = timeoutDeferred.resolve;
+ if (cancellable) value.$cancelRequest = cancelRequest;
return value;
}
// instance call
return promise;
+
+ function cancelRequest(value) {
+ promise.catch(noop);
+ if (timeoutDeferred !== null) {
+ timeoutDeferred.resolve(value);
+ }
+ }
};
@@ -848,10 +900,6 @@ angular.module('ngResource', ['ng']).
};
});
- Resource.bind = function(additionalParamDefaults) {
- return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
- };
-
return Resource;
}
diff --git a/xstatic/pkg/angular/data/angular-route.js b/xstatic/pkg/angular/data/angular-route.js
index 6654d83..738599e 100644
--- a/xstatic/pkg/angular/data/angular-route.js
+++ b/xstatic/pkg/angular/data/angular-route.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -32,43 +32,51 @@ function shallowCopy(src, dst) {
return dst || src;
}
+/* global routeToRegExp: false */
/* global shallowCopy: false */
-// There are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
+// `isArray` and `isObject` are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
// They are initialized inside the `$RouteProvider`, to ensure `window.angular` is available.
var isArray;
var isObject;
+var isDefined;
+var noop;
/**
* @ngdoc module
* @name ngRoute
* @description
*
- * # ngRoute
- *
- * The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
+ * The `ngRoute` module provides routing and deeplinking services and directives for AngularJS apps.
*
* ## Example
- * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
- *
+ * See {@link ngRoute.$route#examples $route} for an example of configuring and using `ngRoute`.
*
- * <div doc-module-components="ngRoute"></div>
*/
- /* global -ngRouteModule */
-var ngRouteModule = angular.module('ngRoute', ['ng']).
- provider('$route', $RouteProvider),
- $routeMinErr = angular.$$minErr('ngRoute');
+/* global -ngRouteModule */
+var ngRouteModule = angular.
+ module('ngRoute', []).
+ info({ angularVersion: '"1.8.2"' }).
+ provider('$route', $RouteProvider).
+ // Ensure `$route` will be instantiated in time to capture the initial `$locationChangeSuccess`
+ // event (unless explicitly disabled). This is necessary in case `ngView` is included in an
+ // asynchronously loaded template.
+ run(instantiateRoute);
+var $routeMinErr = angular.$$minErr('ngRoute');
+var isEagerInstantiationEnabled;
+
/**
* @ngdoc provider
* @name $routeProvider
+ * @this
*
* @description
*
* Used for configuring routes.
*
* ## Example
- * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
+ * See {@link ngRoute.$route#examples $route} for an example of configuring and using `ngRoute`.
*
* ## Dependencies
* Requires the {@link ngRoute `ngRoute`} module to be installed.
@@ -76,6 +84,8 @@ var ngRouteModule = angular.module('ngRoute', ['ng']).
function $RouteProvider() {
isArray = angular.isArray;
isObject = angular.isObject;
+ isDefined = angular.isDefined;
+ noop = angular.noop;
function inherit(parent, extra) {
return angular.extend(Object.create(parent), extra);
@@ -112,12 +122,12 @@ function $RouteProvider() {
*
* Object properties:
*
- * - `controller` – `{(string|function()=}` – Controller fn that should be associated with
+ * - `controller` – `{(string|Function)=}` – Controller fn that should be associated with
* newly created scope or the name of a {@link angular.Module#controller registered
* controller} if passed as a string.
* - `controllerAs` – `{string=}` – An identifier name for a reference to the controller.
* If present, the controller will be published to scope under the `controllerAs` name.
- * - `template` – `{string=|function()=}` – html template as a string or a function that
+ * - `template` – `{(string|Function)=}` – html template as a string or a function that
* returns an html template as a string which should be used by {@link
* ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
* This property takes precedence over `templateUrl`.
@@ -127,7 +137,9 @@ function $RouteProvider() {
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
- * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
+ * One of `template` or `templateUrl` is required.
+ *
+ * - `templateUrl` – `{(string|Function)=}` – path or function that returns a path to an html
* template that should be used by {@link ngRoute.directive:ngView ngView}.
*
* If `templateUrl` is a function, it will be called with the following parameters:
@@ -135,7 +147,9 @@ function $RouteProvider() {
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
- * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
+ * One of `templateUrl` or `template` is required.
+ *
+ * - `resolve` - `{Object.<string, Function>=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, the router
* will wait for them all to be resolved or one to be rejected before the controller is
* instantiated.
@@ -155,7 +169,7 @@ function $RouteProvider() {
* The map object is:
*
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
- * - `factory` - `{string|function}`: If `string` then it is an alias for a service.
+ * - `factory` - `{string|Function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is {@link auto.$injector#invoke injected}
* and the return value is treated as the dependency. If the result is a promise, it is
* resolved before its value is injected into the controller. Be aware that
@@ -165,7 +179,7 @@ function $RouteProvider() {
* - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on
* the scope of the route. If omitted, defaults to `$resolve`.
*
- * - `redirectTo` – `{(string|function())=}` – value to update
+ * - `redirectTo` – `{(string|Function)=}` – value to update
* {@link ng.$location $location} path with and trigger route redirection.
*
* If `redirectTo` is a function, it will be called with the following parameters:
@@ -176,13 +190,48 @@ function $RouteProvider() {
* - `{Object}` - current `$location.search()`
*
* The custom `redirectTo` function is expected to return a string which will be used
- * to update `$location.path()` and `$location.search()`.
+ * to update `$location.url()`. If the function throws an error, no further processing will
+ * take place and the {@link ngRoute.$route#$routeChangeError $routeChangeError} event will
+ * be fired.
+ *
+ * Routes that specify `redirectTo` will not have their controllers, template functions
+ * or resolves called, the `$location` will be changed to the redirect url and route
+ * processing will stop. The exception to this is if the `redirectTo` is a function that
+ * returns `undefined`. In this case the route transition occurs as though there was no
+ * redirection.
+ *
+ * - `resolveRedirectTo` – `{Function=}` – a function that will (eventually) return the value
+ * to update {@link ng.$location $location} URL with and trigger route redirection. In
+ * contrast to `redirectTo`, dependencies can be injected into `resolveRedirectTo` and the
+ * return value can be either a string or a promise that will be resolved to a string.
+ *
+ * Similar to `redirectTo`, if the return value is `undefined` (or a promise that gets
+ * resolved to `undefined`), no redirection takes place and the route transition occurs as
+ * though there was no redirection.
+ *
+ * If the function throws an error or the returned promise gets rejected, no further
+ * processing will take place and the
+ * {@link ngRoute.$route#$routeChangeError $routeChangeError} event will be fired.
+ *
+ * `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same
+ * route definition, will cause the latter to be ignored.
+ *
+ * - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes
+ * (including the path) even if the new URL maps to the same route.
+ *
+ * If the option is set to `false` and the URL in the browser changes, but the new URL maps
+ * to the same route, then a `$routeUpdate` event is broadcasted on the root scope (without
+ * reloading the route).
*
* - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()`
* or `$location.hash()` changes.
*
- * If the option is set to `false` and url in the browser changes, then
- * `$routeUpdate` event is broadcasted on the root scope.
+ * If the option is set to `false` and the URL in the browser changes, then a `$routeUpdate`
+ * event is broadcasted on the root scope (without reloading the route).
+ *
+ * <div class="alert alert-warning">
+ * **Note:** This option has no effect if `reloadOnUrl` is set to `false`.
+ * </div>
*
* - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive
*
@@ -197,6 +246,9 @@ function $RouteProvider() {
this.when = function(path, route) {
//copy original route object to preserve params inherited from proto chain
var routeCopy = shallowCopy(route);
+ if (angular.isUndefined(routeCopy.reloadOnUrl)) {
+ routeCopy.reloadOnUrl = true;
+ }
if (angular.isUndefined(routeCopy.reloadOnSearch)) {
routeCopy.reloadOnSearch = true;
}
@@ -205,18 +257,19 @@ function $RouteProvider() {
}
routes[path] = angular.extend(
routeCopy,
- path && pathRegExp(path, routeCopy)
+ {originalPath: path},
+ path && routeToRegExp(path, routeCopy)
);
// create redirection for trailing slashes
if (path) {
- var redirectPath = (path[path.length - 1] == '/')
+ var redirectPath = (path[path.length - 1] === '/')
? path.substr(0, path.length - 1)
: path + '/';
routes[redirectPath] = angular.extend(
- {redirectTo: path},
- pathRegExp(redirectPath, routeCopy)
+ {originalPath: path, redirectTo: path},
+ routeToRegExp(redirectPath, routeCopy)
);
}
@@ -234,47 +287,6 @@ function $RouteProvider() {
*/
this.caseInsensitiveMatch = false;
- /**
- * @param path {string} path
- * @param opts {Object} options
- * @return {?Object}
- *
- * @description
- * Normalizes the given path, returning a regular expression
- * and the original path.
- *
- * Inspired by pathRexp in visionmedia/express/lib/utils.js.
- */
- function pathRegExp(path, opts) {
- var insensitive = opts.caseInsensitiveMatch,
- ret = {
- originalPath: path,
- regexp: path
- },
- keys = ret.keys = [];
-
- path = path
- .replace(/([().])/g, '\\$1')
- .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) {
- var optional = (option === '?' || option === '*?') ? '?' : null;
- var star = (option === '*' || option === '*?') ? '*' : null;
- keys.push({ name: key, optional: !!optional });
- slash = slash || '';
- return ''
- + (optional ? '' : slash)
- + '(?:'
- + (optional ? slash : '')
- + (star && '(.+?)' || '([^/]+)')
- + (optional || '')
- + ')'
- + (optional || '');
- })
- .replace(/([\/$\*])/g, '\\$1');
-
- ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
- return ret;
- }
-
/**
* @ngdoc method
* @name $routeProvider#otherwise
@@ -295,6 +307,47 @@ function $RouteProvider() {
return this;
};
+ /**
+ * @ngdoc method
+ * @name $routeProvider#eagerInstantiationEnabled
+ * @kind function
+ *
+ * @description
+ * Call this method as a setter to enable/disable eager instantiation of the
+ * {@link ngRoute.$route $route} service upon application bootstrap. You can also call it as a
+ * getter (i.e. without any arguments) to get the current value of the
+ * `eagerInstantiationEnabled` flag.
+ *
+ * Instantiating `$route` early is necessary for capturing the initial
+ * {@link ng.$location#$locationChangeStart $locationChangeStart} event and navigating to the
+ * appropriate route. Usually, `$route` is instantiated in time by the
+ * {@link ngRoute.ngView ngView} directive. Yet, in cases where `ngView` is included in an
+ * asynchronously loaded template (e.g. in another directive's template), the directive factory
+ * might not be called soon enough for `$route` to be instantiated _before_ the initial
+ * `$locationChangeSuccess` event is fired. Eager instantiation ensures that `$route` is always
+ * instantiated in time, regardless of when `ngView` will be loaded.
+ *
+ * The default value is true.
+ *
+ * **Note**:<br />
+ * You may want to disable the default behavior when unit-testing modules that depend on
+ * `ngRoute`, in order to avoid an unexpected request for the default route's template.
+ *
+ * @param {boolean=} enabled - If provided, update the internal `eagerInstantiationEnabled` flag.
+ *
+ * @returns {*} The current value of the `eagerInstantiationEnabled` flag if used as a getter or
+ * itself (for chaining) if used as a setter.
+ */
+ isEagerInstantiationEnabled = true;
+ this.eagerInstantiationEnabled = function eagerInstantiationEnabled(enabled) {
+ if (isDefined(enabled)) {
+ isEagerInstantiationEnabled = enabled;
+ return this;
+ }
+
+ return isEagerInstantiationEnabled;
+ };
+
this.$get = ['$rootScope',
'$location',
@@ -303,7 +356,8 @@ function $RouteProvider() {
'$injector',
'$templateRequest',
'$sce',
- function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) {
+ '$browser',
+ function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce, $browser) {
/**
* @ngdoc service
@@ -388,12 +442,12 @@ function $RouteProvider() {
* })
*
* .controller('BookController', function($scope, $routeParams) {
- * $scope.name = "BookController";
+ * $scope.name = 'BookController';
* $scope.params = $routeParams;
* })
*
* .controller('ChapterController', function($scope, $routeParams) {
- * $scope.name = "ChapterController";
+ * $scope.name = 'ChapterController';
* $scope.params = $routeParams;
* })
*
@@ -426,15 +480,15 @@ function $RouteProvider() {
* it('should load and compile correct template', function() {
* element(by.linkText('Moby: Ch1')).click();
* var content = element(by.css('[ng-view]')).getText();
- * expect(content).toMatch(/controller\: ChapterController/);
- * expect(content).toMatch(/Book Id\: Moby/);
- * expect(content).toMatch(/Chapter Id\: 1/);
+ * expect(content).toMatch(/controller: ChapterController/);
+ * expect(content).toMatch(/Book Id: Moby/);
+ * expect(content).toMatch(/Chapter Id: 1/);
*
* element(by.partialLinkText('Scarlet')).click();
*
* content = element(by.css('[ng-view]')).getText();
- * expect(content).toMatch(/controller\: BookController/);
- * expect(content).toMatch(/Book Id\: Scarlet/);
+ * expect(content).toMatch(/controller: BookController/);
+ * expect(content).toMatch(/Book Id: Scarlet/);
* });
* </file>
* </example>
@@ -482,12 +536,14 @@ function $RouteProvider() {
* @name $route#$routeChangeError
* @eventType broadcast on root scope
* @description
- * Broadcasted if any of the resolve promises are rejected.
+ * Broadcasted if a redirection function fails or any redirection or resolve promises are
+ * rejected.
*
* @param {Object} angularEvent Synthetic event object
* @param {Route} current Current route information.
* @param {Route} previous Previous route information.
- * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
+ * @param {Route} rejection The thrown error or the rejection reason of the promise. Usually
+ * the rejection reason is the error that caused the promise to get rejected.
*/
/**
@@ -495,8 +551,9 @@ function $RouteProvider() {
* @name $route#$routeUpdate
* @eventType broadcast on root scope
* @description
- * The `reloadOnSearch` property has been set to false, and we are reusing the same
- * instance of the Controller.
+ * Broadcasted if the same instance of a route (including template, controller instance,
+ * resolved dependencies, etc.) is being reused. This can happen if either `reloadOnSearch` or
+ * `reloadOnUrl` has been set to `false`.
*
* @param {Object} angularEvent Synthetic event object
* @param {Route} current Current/previous route information.
@@ -556,7 +613,7 @@ function $RouteProvider() {
// interpolate modifies newParams, only query params are left
$location.search(newParams);
} else {
- throw $routeMinErr('norout', 'Tried updating route when with no current route');
+ throw $routeMinErr('norout', 'Tried updating route with no current route');
}
}
};
@@ -604,9 +661,7 @@ function $RouteProvider() {
var lastRoute = $route.current;
preparedRoute = parseRoute();
- preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
- && angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
- && !preparedRoute.reloadOnSearch && !forceReload;
+ preparedRouteIsUpdateOnly = isNavigationUpdateOnly(preparedRoute, lastRoute);
if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
@@ -628,37 +683,112 @@ function $RouteProvider() {
} else if (nextRoute || lastRoute) {
forceReload = false;
$route.current = nextRoute;
- if (nextRoute) {
- if (nextRoute.redirectTo) {
- if (angular.isString(nextRoute.redirectTo)) {
- $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params)
- .replace();
- } else {
- $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search()))
- .replace();
- }
- }
- }
- $q.when(nextRoute).
- then(resolveLocals).
- then(function(locals) {
- // after route change
- if (nextRoute == $route.current) {
- if (nextRoute) {
- nextRoute.locals = locals;
- angular.copy(nextRoute.params, $routeParams);
- }
- $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute);
- }
- }, function(error) {
- if (nextRoute == $route.current) {
+ var nextRoutePromise = $q.resolve(nextRoute);
+
+ $browser.$$incOutstandingRequestCount('$route');
+
+ nextRoutePromise.
+ then(getRedirectionData).
+ then(handlePossibleRedirection).
+ then(function(keepProcessingRoute) {
+ return keepProcessingRoute && nextRoutePromise.
+ then(resolveLocals).
+ then(function(locals) {
+ // after route change
+ if (nextRoute === $route.current) {
+ if (nextRoute) {
+ nextRoute.locals = locals;
+ angular.copy(nextRoute.params, $routeParams);
+ }
+ $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute);
+ }
+ });
+ }).catch(function(error) {
+ if (nextRoute === $route.current) {
$rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error);
}
+ }).finally(function() {
+ // Because `commitRoute()` is called from a `$rootScope.$evalAsync` block (see
+ // `$locationWatch`), this `$$completeOutstandingRequest()` call will not cause
+ // `outstandingRequestCount` to hit zero. This is important in case we are redirecting
+ // to a new route which also requires some asynchronous work.
+
+ $browser.$$completeOutstandingRequest(noop, '$route');
});
}
}
+ function getRedirectionData(route) {
+ var data = {
+ route: route,
+ hasRedirection: false
+ };
+
+ if (route) {
+ if (route.redirectTo) {
+ if (angular.isString(route.redirectTo)) {
+ data.path = interpolate(route.redirectTo, route.params);
+ data.search = route.params;
+ data.hasRedirection = true;
+ } else {
+ var oldPath = $location.path();
+ var oldSearch = $location.search();
+ var newUrl = route.redirectTo(route.pathParams, oldPath, oldSearch);
+
+ if (angular.isDefined(newUrl)) {
+ data.url = newUrl;
+ data.hasRedirection = true;
+ }
+ }
+ } else if (route.resolveRedirectTo) {
+ return $q.
+ resolve($injector.invoke(route.resolveRedirectTo)).
+ then(function(newUrl) {
+ if (angular.isDefined(newUrl)) {
+ data.url = newUrl;
+ data.hasRedirection = true;
+ }
+
+ return data;
+ });
+ }
+ }
+
+ return data;
+ }
+
+ function handlePossibleRedirection(data) {
+ var keepProcessingRoute = true;
+
+ if (data.route !== $route.current) {
+ keepProcessingRoute = false;
+ } else if (data.hasRedirection) {
+ var oldUrl = $location.url();
+ var newUrl = data.url;
+
+ if (newUrl) {
+ $location.
+ url(newUrl).
+ replace();
+ } else {
+ newUrl = $location.
+ path(data.path).
+ search(data.search).
+ replace().
+ url();
+ }
+
+ if (newUrl !== oldUrl) {
+ // Exit out and don't process current next value,
+ // wait for next location change from redirect
+ keepProcessingRoute = false;
+ }
+ }
+
+ return keepProcessingRoute;
+ }
+
function resolveLocals(route) {
if (route) {
var locals = angular.extend({}, route.resolve);
@@ -675,7 +805,6 @@ function $RouteProvider() {
}
}
-
function getTemplateFor(route) {
var template, templateUrl;
if (angular.isDefined(template = route.template)) {
@@ -694,7 +823,6 @@ function $RouteProvider() {
return template;
}
-
/**
* @returns {Object} the current active route, by matching it against the URL
*/
@@ -714,6 +842,29 @@ function $RouteProvider() {
}
/**
+ * @param {Object} newRoute - The new route configuration (as returned by `parseRoute()`).
+ * @param {Object} oldRoute - The previous route configuration (as returned by `parseRoute()`).
+ * @returns {boolean} Whether this is an "update-only" navigation, i.e. the URL maps to the same
+ * route and it can be reused (based on the config and the type of change).
+ */
+ function isNavigationUpdateOnly(newRoute, oldRoute) {
+ // IF this is not a forced reload
+ return !forceReload
+ // AND both `newRoute`/`oldRoute` are defined
+ && newRoute && oldRoute
+ // AND they map to the same Route Definition Object
+ && (newRoute.$$route === oldRoute.$$route)
+ // AND `reloadOnUrl` is disabled
+ && (!newRoute.reloadOnUrl
+ // OR `reloadOnSearch` is disabled
+ || (!newRoute.reloadOnSearch
+ // AND both routes have the same path params
+ && angular.equals(newRoute.pathParams, oldRoute.pathParams)
+ )
+ );
+ }
+
+ /**
* @returns {string} interpolation of the redirect path with the parameters
*/
function interpolate(string, params) {
@@ -734,6 +885,14 @@ function $RouteProvider() {
}];
}
+instantiateRoute.$inject = ['$injector'];
+function instantiateRoute($injector) {
+ if (isEagerInstantiationEnabled) {
+ // Instantiate `$route`
+ $injector.get('$route');
+ }
+}
+
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
@@ -741,6 +900,7 @@ ngRouteModule.provider('$routeParams', $RouteParamsProvider);
* @ngdoc service
* @name $routeParams
* @requires $route
+ * @this
*
* @description
* The `$routeParams` service allows you to retrieve the current set of route parameters.
@@ -784,7 +944,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
* @restrict ECA
*
* @description
- * # Overview
* `ngView` is a directive that complements the {@link ngRoute.$route $route} service by
* including the rendered template of the current route into the main layout (`index.html`) file.
* Every time the current route changes, the included view changes with it according to the
@@ -800,13 +959,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
*
* The enter and leave animation occur concurrently.
*
- * @knownIssue If `ngView` is contained in an asynchronously loaded template (e.g. in another
- * directive's templateUrl or in a template loaded using `ngInclude`), then you need to
- * make sure that `$route` is instantiated in time to capture the initial
- * `$locationChangeStart` event and load the appropriate view. One way to achieve this
- * is to have it as a dependency in a `.run` block:
- * `myModule.run(['$route', function() {}]);`
- *
* @scope
* @priority 400
* @param {string=} onload Expression to evaluate whenever the view updates.
@@ -917,17 +1069,17 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
$locationProvider.html5Mode(true);
}])
.controller('MainCtrl', ['$route', '$routeParams', '$location',
- function($route, $routeParams, $location) {
+ function MainCtrl($route, $routeParams, $location) {
this.$route = $route;
this.$location = $location;
this.$routeParams = $routeParams;
}])
- .controller('BookCtrl', ['$routeParams', function($routeParams) {
- this.name = "BookCtrl";
+ .controller('BookCtrl', ['$routeParams', function BookCtrl($routeParams) {
+ this.name = 'BookCtrl';
this.params = $routeParams;
}])
- .controller('ChapterCtrl', ['$routeParams', function($routeParams) {
- this.name = "ChapterCtrl";
+ .controller('ChapterCtrl', ['$routeParams', function ChapterCtrl($routeParams) {
+ this.name = 'ChapterCtrl';
this.params = $routeParams;
}]);
@@ -937,15 +1089,15 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
it('should load and compile correct template', function() {
element(by.linkText('Moby: Ch1')).click();
var content = element(by.css('[ng-view]')).getText();
- expect(content).toMatch(/controller\: ChapterCtrl/);
- expect(content).toMatch(/Book Id\: Moby/);
- expect(content).toMatch(/Chapter Id\: 1/);
+ expect(content).toMatch(/controller: ChapterCtrl/);
+ expect(content).toMatch(/Book Id: Moby/);
+ expect(content).toMatch(/Chapter Id: 1/);
element(by.partialLinkText('Scarlet')).click();
content = element(by.css('[ng-view]')).getText();
- expect(content).toMatch(/controller\: BookCtrl/);
- expect(content).toMatch(/Book Id\: Scarlet/);
+ expect(content).toMatch(/controller: BookCtrl/);
+ expect(content).toMatch(/Book Id: Scarlet/);
});
</file>
</example>
@@ -988,8 +1140,8 @@ function ngViewFactory($route, $anchorScroll, $animate) {
}
if (currentElement) {
previousLeaveAnimation = $animate.leave(currentElement);
- previousLeaveAnimation.then(function() {
- previousLeaveAnimation = null;
+ previousLeaveAnimation.done(function(response) {
+ if (response !== false) previousLeaveAnimation = null;
});
currentElement = null;
}
@@ -1010,8 +1162,8 @@ function ngViewFactory($route, $anchorScroll, $animate) {
// function is called before linking the content, which would apply child
// directives to non existing elements.
var clone = $transclude(newScope, function(clone) {
- $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() {
- if (angular.isDefined(autoScrollExp)
+ $animate.enter(clone, null, currentElement || $element).done(function onNgViewEnter(response) {
+ if (response !== false && angular.isDefined(autoScrollExp)
&& (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll();
}
diff --git a/xstatic/pkg/angular/data/angular-sanitize.js b/xstatic/pkg/angular/data/angular-sanitize.js
index a283e43..451073f 100644
--- a/xstatic/pkg/angular/data/angular-sanitize.js
+++ b/xstatic/pkg/angular/data/angular-sanitize.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
@@ -20,9 +20,11 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
var bind;
var extend;
var forEach;
+var isArray;
var isDefined;
var lowercase;
var noop;
+var nodeContains;
var htmlParser;
var htmlSanitizeWriter;
@@ -31,13 +33,8 @@ var htmlSanitizeWriter;
* @name ngSanitize
* @description
*
- * # ngSanitize
- *
* The `ngSanitize` module provides functionality to sanitize HTML.
*
- *
- * <div doc-module-components="ngSanitize"></div>
- *
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
*/
@@ -49,13 +46,12 @@ var htmlSanitizeWriter;
* @description
* Sanitizes an html string by stripping all potentially dangerous tokens.
*
- * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
- * then serialized back to properly escaped html string. This means that no unsafe input can make
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a trusted URI list) are
+ * then serialized back to a properly escaped HTML string. This means that no unsafe input can make
* it into the returned string.
*
- * The whitelist for URL sanitization of attribute values is configured using the functions
- * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
- * `$compileProvider`}.
+ * The trusted URIs for URL sanitization of attribute values is configured using the functions
+ * `aHrefSanitizationTrustedUrlList` and `imgSrcSanitizationTrustedUrlList` of {@link $compileProvider}.
*
* The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
*
@@ -63,7 +59,7 @@ var htmlSanitizeWriter;
* @returns {string} Sanitized HTML.
*
* @example
- <example module="sanitizeExample" deps="angular-sanitize.js">
+ <example module="sanitizeExample" deps="angular-sanitize.js" name="sanitize-service">
<file name="index.html">
<script>
angular.module('sanitizeExample', ['ngSanitize'])
@@ -112,19 +108,19 @@ var htmlSanitizeWriter;
</file>
<file name="protractor.js" type="protractor">
it('should sanitize the html snippet by default', function() {
- expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
});
it('should inline raw snippet if bound to a trusted value', function() {
- expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
+ expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should escape snippet without any filter', function() {
- expect(element(by.css('#bind-default div')).getInnerHtml()).
+ expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).
toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
"&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
"snippet&lt;/p&gt;");
@@ -133,11 +129,11 @@ var htmlSanitizeWriter;
it('should update', function() {
element(by.model('snippet')).clear();
element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
- expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
toBe('new <b>text</b>');
- expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
+ expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).toBe(
'new <b onclick="alert(1)">text</b>');
- expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
+ expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).toBe(
"new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
});
</file>
@@ -148,14 +144,17 @@ var htmlSanitizeWriter;
/**
* @ngdoc provider
* @name $sanitizeProvider
+ * @this
*
* @description
* Creates and configures {@link $sanitize} instance.
*/
function $SanitizeProvider() {
+ var hasBeenInstantiated = false;
var svgEnabled = false;
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ hasBeenInstantiated = true;
if (svgEnabled) {
extend(validElements, svgElements);
}
@@ -196,7 +195,7 @@ function $SanitizeProvider() {
* </div>
*
* @param {boolean=} flag Enable or disable SVG support in the sanitizer.
- * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
+ * @returns {boolean|$sanitizeProvider} Returns the currently configured value if called
* without an argument or self for chaining otherwise.
*/
this.enableSvg = function(enableSvg) {
@@ -208,6 +207,105 @@ function $SanitizeProvider() {
}
};
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#addValidElements
+ * @kind function
+ *
+ * @description
+ * Extends the built-in lists of valid HTML/SVG elements, i.e. elements that are considered safe
+ * and are not stripped off during sanitization. You can extend the following lists of elements:
+ *
+ * - `htmlElements`: A list of elements (tag names) to extend the current list of safe HTML
+ * elements. HTML elements considered safe will not be removed during sanitization. All other
+ * elements will be stripped off.
+ *
+ * - `htmlVoidElements`: This is similar to `htmlElements`, but marks the elements as
+ * "void elements" (similar to HTML
+ * [void elements](https://rawgit.com/w3c/html/html5.1-2/single-page.html#void-elements)). These
+ * elements have no end tag and cannot have content.
+ *
+ * - `svgElements`: This is similar to `htmlElements`, but for SVG elements. This list is only
+ * taken into account if SVG is {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for
+ * `$sanitize`.
+ *
+ * <div class="alert alert-info">
+ * This method must be called during the {@link angular.Module#config config} phase. Once the
+ * `$sanitize` service has been instantiated, this method has no effect.
+ * </div>
+ *
+ * <div class="alert alert-warning">
+ * Keep in mind that extending the built-in lists of elements may expose your app to XSS or
+ * other vulnerabilities. Be very mindful of the elements you add.
+ * </div>
+ *
+ * @param {Array<String>|Object} elements - A list of valid HTML elements or an object with one or
+ * more of the following properties:
+ * - **htmlElements** - `{Array<String>}` - A list of elements to extend the current list of
+ * HTML elements.
+ * - **htmlVoidElements** - `{Array<String>}` - A list of elements to extend the current list of
+ * void HTML elements; i.e. elements that do not have an end tag.
+ * - **svgElements** - `{Array<String>}` - A list of elements to extend the current list of SVG
+ * elements. The list of SVG elements is only taken into account if SVG is
+ * {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for `$sanitize`.
+ *
+ * Passing an array (`[...]`) is equivalent to passing `{htmlElements: [...]}`.
+ *
+ * @return {$sanitizeProvider} Returns self for chaining.
+ */
+ this.addValidElements = function(elements) {
+ if (!hasBeenInstantiated) {
+ if (isArray(elements)) {
+ elements = {htmlElements: elements};
+ }
+
+ addElementsTo(svgElements, elements.svgElements);
+ addElementsTo(voidElements, elements.htmlVoidElements);
+ addElementsTo(validElements, elements.htmlVoidElements);
+ addElementsTo(validElements, elements.htmlElements);
+ }
+
+ return this;
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#addValidAttrs
+ * @kind function
+ *
+ * @description
+ * Extends the built-in list of valid attributes, i.e. attributes that are considered safe and are
+ * not stripped off during sanitization.
+ *
+ * **Note**:
+ * The new attributes will not be treated as URI attributes, which means their values will not be
+ * sanitized as URIs using `$compileProvider`'s
+ * {@link ng.$compileProvider#aHrefSanitizationTrustedUrlList aHrefSanitizationTrustedUrlList} and
+ * {@link ng.$compileProvider#imgSrcSanitizationTrustedUrlList imgSrcSanitizationTrustedUrlList}.
+ *
+ * <div class="alert alert-info">
+ * This method must be called during the {@link angular.Module#config config} phase. Once the
+ * `$sanitize` service has been instantiated, this method has no effect.
+ * </div>
+ *
+ * <div class="alert alert-warning">
+ * Keep in mind that extending the built-in list of attributes may expose your app to XSS or
+ * other vulnerabilities. Be very mindful of the attributes you add.
+ * </div>
+ *
+ * @param {Array<String>} attrs - A list of valid attributes.
+ *
+ * @returns {$sanitizeProvider} Returns self for chaining.
+ */
+ this.addValidAttrs = function(attrs) {
+ if (!hasBeenInstantiated) {
+ extend(validAttrs, arrayToMap(attrs, true));
+ }
+ return this;
+ };
+
//////////////////////////////////////////////////////////////////////////////////////////////////
// Private stuff
//////////////////////////////////////////////////////////////////////////////////////////////////
@@ -215,17 +313,23 @@ function $SanitizeProvider() {
bind = angular.bind;
extend = angular.extend;
forEach = angular.forEach;
+ isArray = angular.isArray;
isDefined = angular.isDefined;
- lowercase = angular.lowercase;
+ lowercase = angular.$$lowercase;
noop = angular.noop;
htmlParser = htmlParserImpl;
htmlSanitizeWriter = htmlSanitizeWriterImpl;
+ nodeContains = window.Node.prototype.contains || /** @this */ function(arg) {
+ // eslint-disable-next-line no-bitwise
+ return !!(this.compareDocumentPosition(arg) & 16);
+ };
+
// Regular Expressions for parsing tags and attributes
var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
// Match everything outside of normal chars and " (quote character)
- NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
+ NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g;
// Good source of info about elements and attributes
@@ -234,36 +338,36 @@ function $SanitizeProvider() {
// Safe Void Elements - HTML5
// http://dev.w3.org/html5/spec/Overview.html#void-elements
- var voidElements = toMap("area,br,col,hr,img,wbr");
+ var voidElements = stringToMap('area,br,col,hr,img,wbr');
// Elements that you can, intentionally, leave open (and which close themselves)
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
- var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
- optionalEndTagInlineElements = toMap("rp,rt"),
+ var optionalEndTagBlockElements = stringToMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'),
+ optionalEndTagInlineElements = stringToMap('rp,rt'),
optionalEndTagElements = extend({},
optionalEndTagInlineElements,
optionalEndTagBlockElements);
// Safe Block Elements - HTML5
- var blockElements = extend({}, optionalEndTagBlockElements, toMap("address,article," +
- "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
- "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul"));
+ var blockElements = extend({}, optionalEndTagBlockElements, stringToMap('address,article,' +
+ 'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
+ 'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul'));
// Inline Elements - HTML5
- var inlineElements = extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," +
- "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
- "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
+ var inlineElements = extend({}, optionalEndTagInlineElements, stringToMap('a,abbr,acronym,b,' +
+ 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' +
+ 'samp,small,span,strike,strong,sub,sup,time,tt,u,var'));
// SVG Elements
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
// They can potentially allow for arbitrary javascript to be executed. See #11290
- var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
- "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
- "radialGradient,rect,stop,svg,switch,text,title,tspan");
+ var svgElements = stringToMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' +
+ 'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' +
+ 'radialGradient,rect,stop,svg,switch,text,title,tspan');
// Blocked Elements (will be stripped)
- var blockedElements = toMap("script,style");
+ var blockedElements = stringToMap('script,style');
var validElements = extend({},
voidElements,
@@ -272,9 +376,9 @@ function $SanitizeProvider() {
optionalEndTagElements);
//Attributes that have href and hence need to be sanitized
- var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href");
+ var uriAttrs = stringToMap('background,cite,href,longdesc,src,xlink:href,xml:base');
- var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
+ var htmlAttrs = stringToMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
@@ -282,7 +386,7 @@ function $SanitizeProvider() {
// SVG attributes (without "id" and "name" attributes)
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
- var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
+ var svgAttrs = stringToMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
@@ -303,35 +407,74 @@ function $SanitizeProvider() {
svgAttrs,
htmlAttrs);
- function toMap(str, lowercaseKeys) {
- var obj = {}, items = str.split(','), i;
+ function stringToMap(str, lowercaseKeys) {
+ return arrayToMap(str.split(','), lowercaseKeys);
+ }
+
+ function arrayToMap(items, lowercaseKeys) {
+ var obj = {}, i;
for (i = 0; i < items.length; i++) {
obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true;
}
return obj;
}
- var inertBodyElement;
- (function(window) {
- var doc;
- if (window.document && window.document.implementation) {
- doc = window.document.implementation.createHTMLDocument("inert");
- } else {
- throw $sanitizeMinErr('noinert', "Can't create an inert html document");
+ function addElementsTo(elementsMap, newElements) {
+ if (newElements && newElements.length) {
+ extend(elementsMap, arrayToMap(newElements));
}
- var docElement = doc.documentElement || doc.getDocumentElement();
- var bodyElements = docElement.getElementsByTagName('body');
+ }
- // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one
- if (bodyElements.length === 1) {
- inertBodyElement = bodyElements[0];
- } else {
- var html = doc.createElement('html');
- inertBodyElement = doc.createElement('body');
- html.appendChild(inertBodyElement);
- doc.appendChild(html);
+ /**
+ * Create an inert document that contains the dirty HTML that needs sanitizing.
+ * We use the DOMParser API by default and fall back to createHTMLDocument if DOMParser is not
+ * available.
+ */
+ var getInertBodyElement /* function(html: string): HTMLBodyElement */ = (function(window, document) {
+ if (isDOMParserAvailable()) {
+ return getInertBodyElement_DOMParser;
+ }
+
+ if (!document || !document.implementation) {
+ throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document');
+ }
+ var inertDocument = document.implementation.createHTMLDocument('inert');
+ var inertBodyElement = (inertDocument.documentElement || inertDocument.getDocumentElement()).querySelector('body');
+ return getInertBodyElement_InertDocument;
+
+ function isDOMParserAvailable() {
+ try {
+ return !!getInertBodyElement_DOMParser('');
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function getInertBodyElement_DOMParser(html) {
+ // We add this dummy element to ensure that the rest of the content is parsed as expected
+ // e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
+ html = '<remove></remove>' + html;
+ try {
+ var body = new window.DOMParser().parseFromString(html, 'text/html').body;
+ body.firstChild.remove();
+ return body;
+ } catch (e) {
+ return undefined;
+ }
+ }
+
+ function getInertBodyElement_InertDocument(html) {
+ inertBodyElement.innerHTML = html;
+
+ // Support: IE 9-11 only
+ // strip custom-namespaced attributes on IE<=11
+ if (document.documentMode) {
+ stripCustomNsAttrs(inertBodyElement);
+ }
+
+ return inertBodyElement;
}
- })(window);
+ })(window, window.document);
/**
* @example
@@ -351,22 +494,21 @@ function $SanitizeProvider() {
} else if (typeof html !== 'string') {
html = '' + html;
}
- inertBodyElement.innerHTML = html;
+
+ var inertBodyElement = getInertBodyElement(html);
+ if (!inertBodyElement) return '';
//mXSS protection
var mXSSAttempts = 5;
do {
if (mXSSAttempts === 0) {
- throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable");
+ throw $sanitizeMinErr('uinput', 'Failed to sanitize html because the input is unstable');
}
mXSSAttempts--;
- // strip custom-namespaced attributes on IE<=11
- if (window.document.documentMode) {
- stripCustomNsAttrs(inertBodyElement);
- }
- html = inertBodyElement.innerHTML; //trigger mXSS
- inertBodyElement.innerHTML = html;
+ // trigger mXSS if it is going to happen by reading and writing the innerHTML
+ html = inertBodyElement.innerHTML;
+ inertBodyElement = getInertBodyElement(html);
} while (html !== inertBodyElement.innerHTML);
var node = inertBodyElement.firstChild;
@@ -382,16 +524,16 @@ function $SanitizeProvider() {
var nextNode;
if (!(nextNode = node.firstChild)) {
- if (node.nodeType == 1) {
+ if (node.nodeType === 1) {
handler.end(node.nodeName.toLowerCase());
}
- nextNode = node.nextSibling;
+ nextNode = getNonDescendant('nextSibling', node);
if (!nextNode) {
while (nextNode == null) {
- node = node.parentNode;
+ node = getNonDescendant('parentNode', node);
if (node === inertBodyElement) break;
- nextNode = node.nextSibling;
- if (node.nodeType == 1) {
+ nextNode = getNonDescendant('nextSibling', node);
+ if (node.nodeType === 1) {
handler.end(node.nodeName.toLowerCase());
}
}
@@ -400,7 +542,7 @@ function $SanitizeProvider() {
node = nextNode;
}
- while (node = inertBodyElement.firstChild) {
+ while ((node = inertBodyElement.firstChild)) {
inertBodyElement.removeChild(node);
}
}
@@ -481,6 +623,7 @@ function $SanitizeProvider() {
out(tag);
out('>');
}
+ // eslint-disable-next-line eqeqeq
if (tag == ignoreCurrentElement) {
ignoreCurrentElement = false;
}
@@ -502,28 +645,36 @@ function $SanitizeProvider() {
* @param node Root element to process
*/
function stripCustomNsAttrs(node) {
- if (node.nodeType === window.Node.ELEMENT_NODE) {
- var attrs = node.attributes;
- for (var i = 0, l = attrs.length; i < l; i++) {
- var attrNode = attrs[i];
- var attrName = attrNode.name.toLowerCase();
- if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) {
- node.removeAttributeNode(attrNode);
- i--;
- l--;
+ while (node) {
+ if (node.nodeType === window.Node.ELEMENT_NODE) {
+ var attrs = node.attributes;
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ var attrNode = attrs[i];
+ var attrName = attrNode.name.toLowerCase();
+ if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) {
+ node.removeAttributeNode(attrNode);
+ i--;
+ l--;
+ }
}
}
- }
- var nextNode = node.firstChild;
- if (nextNode) {
- stripCustomNsAttrs(nextNode);
+ var nextNode = node.firstChild;
+ if (nextNode) {
+ stripCustomNsAttrs(nextNode);
+ }
+
+ node = getNonDescendant('nextSibling', node);
}
+ }
- nextNode = node.nextSibling;
- if (nextNode) {
- stripCustomNsAttrs(nextNode);
+ function getNonDescendant(propName, node) {
+ // An element is clobbered if its `propName` property points to one of its descendants
+ var nextNode = node[propName];
+ if (nextNode && nodeContains.call(node, nextNode)) {
+ throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText);
}
+ return nextNode;
}
}
@@ -536,7 +687,9 @@ function sanitizeText(chars) {
// define ngSanitize module and register $sanitize service
-angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
+angular.module('ngSanitize', [])
+ .provider('$sanitize', $SanitizeProvider)
+ .info({ angularVersion: '"1.8.2"' });
/**
* @ngdoc filter
@@ -544,13 +697,13 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
* @kind function
*
* @description
- * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
+ * Finds links in text input and turns them into html links. Supports `http/https/ftp/sftp/mailto` and
* plain email address links.
*
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
*
* @param {string} text Input text.
- * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
+ * @param {string} [target] Window (`_blank|_self|_parent|_top`) or named frame to open links in.
* @param {object|function(url)} [attributes] Add custom attributes to the link element.
*
* Can be one of:
@@ -568,7 +721,7 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
<span ng-bind-html="linky_expression | linky"></span>
*
* @example
- <example module="linkyExample" deps="angular-sanitize.js">
+ <example module="linkyExample" deps="angular-sanitize.js" name="linky-filter">
<file name="index.html">
<div ng-controller="ExampleController">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
@@ -616,10 +769,10 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
angular.module('linkyExample', ['ngSanitize'])
.controller('ExampleController', ['$scope', function($scope) {
$scope.snippet =
- 'Pretty text with some links:\n'+
- 'http://angularjs.org/,\n'+
- 'mailto:us@somewhere.org,\n'+
- 'another@somewhere.org,\n'+
+ 'Pretty text with some links:\n' +
+ 'http://angularjs.org/,\n' +
+ 'mailto:us@somewhere.org,\n' +
+ 'another@somewhere.org,\n' +
'and one more: ftp://127.0.0.1/.';
$scope.snippetWithSingleURL = 'http://angularjs.org/';
}]);
@@ -667,7 +820,7 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
*/
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
- /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
+ /((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
MAILTO_REGEXP = /^mailto:/i;
var linkyMinErr = angular.$$minErr('linky');
diff --git a/xstatic/pkg/angular/data/angular-scenario.js b/xstatic/pkg/angular/data/angular-scenario.js
index 834f3d3..a067100 100644
--- a/xstatic/pkg/angular/data/angular-scenario.js
+++ b/xstatic/pkg/angular/data/angular-scenario.js
@@ -1,20 +1,21 @@
-/*eslint-disable no-unused-vars*/
/*!
- * jQuery JavaScript Library v3.1.0
+ * jQuery JavaScript Library v3.5.1
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
- * Copyright jQuery Foundation and other contributors
+ * Copyright JS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
- * Date: 2016-07-07T21:44Z
+ * Date: 2020-05-04T22:49Z
*/
( function( global, factory ) {
-if ( typeof module === "object" && typeof module.exports === "object" ) {
+ "use strict";
+
+ if ( typeof module === "object" && typeof module.exports === "object" ) {
// For CommonJS and CommonJS-like environments where a proper `window`
// is present, execute the factory and get jQuery.
@@ -42,16 +43,20 @@ if ( typeof module === "object" && typeof module.exports === "object" ) {
// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
// enough that all such attempts are guarded in a try block.
+"use strict";
var arr = [];
-var document = window.document;
-
var getProto = Object.getPrototypeOf;
var slice = arr.slice;
-var concat = arr.concat;
+var flat = arr.flat ? function( array ) {
+ return arr.flat.call( array );
+} : function( array ) {
+ return arr.concat.apply( [], array );
+};
+
var push = arr.push;
@@ -69,24 +74,80 @@ var ObjectFunctionString = fnToString.call( Object );
var support = {};
+var isFunction = function isFunction( obj ) {
+
+ // Support: Chrome <=57, Firefox <=52
+ // In some browsers, typeof returns "function" for HTML <object> elements
+ // (i.e., `typeof document.createElement( "object" ) === "function"`).
+ // We don't want to classify *any* DOM node as a function.
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+ };
+
+
+var isWindow = function isWindow( obj ) {
+ return obj != null && obj === obj.window;
+ };
+
+
+var document = window.document;
+
- function DOMEval( code, doc ) {
+ var preservedScriptAttributes = {
+ type: true,
+ src: true,
+ nonce: true,
+ noModule: true
+ };
+
+ function DOMEval( code, node, doc ) {
doc = doc || document;
- var script = doc.createElement( "script" );
+ var i, val,
+ script = doc.createElement( "script" );
script.text = code;
+ if ( node ) {
+ for ( i in preservedScriptAttributes ) {
+
+ // Support: Firefox 64+, Edge 18+
+ // Some browsers don't support the "nonce" property on scripts.
+ // On the other hand, just using `getAttribute` is not enough as
+ // the `nonce` attribute is reset to an empty string whenever it
+ // becomes browsing-context connected.
+ // See https://github.com/whatwg/html/issues/2369
+ // See https://html.spec.whatwg.org/#nonce-attributes
+ // The `node.getAttribute` check was added for the sake of
+ // `jQuery.globalEval` so that it can fake a nonce-containing node
+ // via an object.
+ val = node[ i ] || node.getAttribute && node.getAttribute( i );
+ if ( val ) {
+ script.setAttribute( i, val );
+ }
+ }
+ }
doc.head.appendChild( script ).parentNode.removeChild( script );
}
+
+
+function toType( obj ) {
+ if ( obj == null ) {
+ return obj + "";
+ }
+
+ // Support: Android <=2.3 only (functionish RegExp)
+ return typeof obj === "object" || typeof obj === "function" ?
+ class2type[ toString.call( obj ) ] || "object" :
+ typeof obj;
+}
/* global Symbol */
-// Defining this global in .eslintrc would create a danger of using the global
+// Defining this global in .eslintrc.json would create a danger of using the global
// unguarded in another place, it seems safer to define global only for this module
var
- version = "3.1.0",
+ version = "3.5.1",
// Define a local copy of jQuery
jQuery = function( selector, context ) {
@@ -94,19 +155,6 @@ var
// The jQuery object is actually just the init constructor 'enhanced'
// Need init if jQuery is called (just allow error to be thrown if not included)
return new jQuery.fn.init( selector, context );
- },
-
- // Support: Android <=4.0 only
- // Make sure we trim BOM and NBSP
- rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
-
- // Matches dashed string for camelizing
- rmsPrefix = /^-ms-/,
- rdashAlpha = /-([a-z])/g,
-
- // Used by jQuery.camelCase as callback to replace()
- fcamelCase = function( all, letter ) {
- return letter.toUpperCase();
};
jQuery.fn = jQuery.prototype = {
@@ -126,13 +174,14 @@ jQuery.fn = jQuery.prototype = {
// Get the Nth element in the matched element set OR
// Get the whole matched element set as a clean array
get: function( num ) {
- return num != null ?
- // Return just the one element from the set
- ( num < 0 ? this[ num + this.length ] : this[ num ] ) :
+ // Return all the elements in a clean array
+ if ( num == null ) {
+ return slice.call( this );
+ }
- // Return all the elements in a clean array
- slice.call( this );
+ // Return just the one element from the set
+ return num < 0 ? this[ num + this.length ] : this[ num ];
},
// Take an array of elements and push it onto the stack
@@ -172,6 +221,18 @@ jQuery.fn = jQuery.prototype = {
return this.eq( -1 );
},
+ even: function() {
+ return this.pushStack( jQuery.grep( this, function( _elem, i ) {
+ return ( i + 1 ) % 2;
+ } ) );
+ },
+
+ odd: function() {
+ return this.pushStack( jQuery.grep( this, function( _elem, i ) {
+ return i % 2;
+ } ) );
+ },
+
eq: function( i ) {
var len = this.length,
j = +i + ( i < 0 ? len : 0 );
@@ -206,7 +267,7 @@ jQuery.extend = jQuery.fn.extend = function() {
}
// Handle case when target is a string or something (possible in deep copy)
- if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
+ if ( typeof target !== "object" && !isFunction( target ) ) {
target = {};
}
@@ -223,25 +284,28 @@ jQuery.extend = jQuery.fn.extend = function() {
// Extend the base object
for ( name in options ) {
- src = target[ name ];
copy = options[ name ];
+ // Prevent Object.prototype pollution
// Prevent never-ending loop
- if ( target === copy ) {
+ if ( name === "__proto__" || target === copy ) {
continue;
}
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
- ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
-
- if ( copyIsArray ) {
- copyIsArray = false;
- clone = src && jQuery.isArray( src ) ? src : [];
-
+ ( copyIsArray = Array.isArray( copy ) ) ) ) {
+ src = target[ name ];
+
+ // Ensure proper type for the source value
+ if ( copyIsArray && !Array.isArray( src ) ) {
+ clone = [];
+ } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
+ clone = {};
} else {
- clone = src && jQuery.isPlainObject( src ) ? src : {};
+ clone = src;
}
+ copyIsArray = false;
// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );
@@ -272,30 +336,6 @@ jQuery.extend( {
noop: function() {},
- isFunction: function( obj ) {
- return jQuery.type( obj ) === "function";
- },
-
- isArray: Array.isArray,
-
- isWindow: function( obj ) {
- return obj != null && obj === obj.window;
- },
-
- isNumeric: function( obj ) {
-
- // As of jQuery 3.0, isNumeric is limited to
- // strings and numbers (primitives or objects)
- // that can be coerced to finite numbers (gh-2662)
- var type = jQuery.type( obj );
- return ( type === "number" || type === "string" ) &&
-
- // parseFloat NaNs numeric-cast false positives ("")
- // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
- // subtraction forces infinities to NaN
- !isNaN( obj - parseFloat( obj ) );
- },
-
isPlainObject: function( obj ) {
var proto, Ctor;
@@ -318,9 +358,6 @@ jQuery.extend( {
},
isEmptyObject: function( obj ) {
-
- /* eslint-disable no-unused-vars */
- // See https://github.com/eslint/eslint/issues/6125
var name;
for ( name in obj ) {
@@ -329,31 +366,10 @@ jQuery.extend( {
return true;
},
- type: function( obj ) {
- if ( obj == null ) {
- return obj + "";
- }
-
- // Support: Android <=2.3 only (functionish RegExp)
- return typeof obj === "object" || typeof obj === "function" ?
- class2type[ toString.call( obj ) ] || "object" :
- typeof obj;
- },
-
- // Evaluates a script in a global context
- globalEval: function( code ) {
- DOMEval( code );
- },
-
- // Convert dashed to camelCase; used by the css and data modules
- // Support: IE <=9 - 11, Edge 12 - 13
- // Microsoft forgot to hump their vendor prefix (#9572)
- camelCase: function( string ) {
- return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
- },
-
- nodeName: function( elem, name ) {
- return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+ // Evaluates a script in a provided context; falls back to the global one
+ // if not specified.
+ globalEval: function( code, options, doc ) {
+ DOMEval( code, { nonce: options && options.nonce }, doc );
},
each: function( obj, callback ) {
@@ -377,13 +393,6 @@ jQuery.extend( {
return obj;
},
- // Support: Android <=4.0 only
- trim: function( text ) {
- return text == null ?
- "" :
- ( text + "" ).replace( rtrim, "" );
- },
-
// results is for internal usage only
makeArray: function( arr, results ) {
var ret = results || [];
@@ -470,43 +479,12 @@ jQuery.extend( {
}
// Flatten any nested arrays
- return concat.apply( [], ret );
+ return flat( ret );
},
// A global GUID counter for objects
guid: 1,
- // Bind a function to a context, optionally partially applying any
- // arguments.
- proxy: function( fn, context ) {
- var tmp, args, proxy;
-
- if ( typeof context === "string" ) {
- tmp = fn[ context ];
- context = fn;
- fn = tmp;
- }
-
- // Quick check to determine if target is callable, in the spec
- // this throws a TypeError, but we will just return undefined.
- if ( !jQuery.isFunction( fn ) ) {
- return undefined;
- }
-
- // Simulated bind
- args = slice.call( arguments, 2 );
- proxy = function() {
- return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
- };
-
- // Set the guid of unique handler to the same of original handler, so it can be removed
- proxy.guid = fn.guid = fn.guid || jQuery.guid++;
-
- return proxy;
- },
-
- now: Date.now,
-
// jQuery.support is not used in Core but other projects attach their
// properties to it so it needs to exist.
support: support
@@ -518,7 +496,7 @@ if ( typeof Symbol === "function" ) {
// Populate the class2type map
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
-function( i, name ) {
+function( _i, name ) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );
@@ -529,9 +507,9 @@ function isArrayLike( obj ) {
// hasOwn isn't used here due to false negatives
// regarding Nodelist length in IE
var length = !!obj && "length" in obj && obj.length,
- type = jQuery.type( obj );
+ type = toType( obj );
- if ( type === "function" || jQuery.isWindow( obj ) ) {
+ if ( isFunction( obj ) || isWindow( obj ) ) {
return false;
}
@@ -540,17 +518,16 @@ function isArrayLike( obj ) {
}
var Sizzle =
/*!
- * Sizzle CSS Selector Engine v2.3.0
+ * Sizzle CSS Selector Engine v2.3.5
* https://sizzlejs.com/
*
- * Copyright jQuery Foundation and other contributors
+ * Copyright JS Foundation and other contributors
* Released under the MIT license
- * http://jquery.org/license
+ * https://js.foundation/
*
- * Date: 2016-01-04
+ * Date: 2020-03-14
*/
-(function( window ) {'use strict';
-
+( function( window ) {
var i,
support,
Expr,
@@ -581,6 +558,7 @@ var i,
classCache = createCache(),
tokenCache = createCache(),
compilerCache = createCache(),
+ nonnativeSelectorCache = createCache(),
sortOrder = function( a, b ) {
if ( a === b ) {
hasDuplicate = true;
@@ -589,61 +567,71 @@ var i,
},
// Instance methods
- hasOwn = ({}).hasOwnProperty,
+ hasOwn = ( {} ).hasOwnProperty,
arr = [],
pop = arr.pop,
- push_native = arr.push,
+ pushNative = arr.push,
push = arr.push,
slice = arr.slice,
+
// Use a stripped-down indexOf as it's faster than native
// https://jsperf.com/thor-indexof-vs-for/5
indexOf = function( list, elem ) {
var i = 0,
len = list.length;
for ( ; i < len; i++ ) {
- if ( list[i] === elem ) {
+ if ( list[ i ] === elem ) {
return i;
}
}
return -1;
},
- booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+ booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" +
+ "ismap|loop|multiple|open|readonly|required|scoped",
// Regular expressions
// http://www.w3.org/TR/css3-selectors/#whitespace
whitespace = "[\\x20\\t\\r\\n\\f]",
- // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
- identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",
+ // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace +
+ "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",
// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+
// Operator (capture 2)
"*([*^$|!~]?=)" + whitespace +
- // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
- "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
- "*\\]",
+
+ // "Attribute values must be CSS identifiers [capture 5]
+ // or strings [capture 3 or capture 4]"
+ "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" +
+ whitespace + "*\\]",
pseudos = ":(" + identifier + ")(?:\\((" +
+
// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
// 1. quoted (capture 3; capture 4 or capture 5)
"('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+
// 2. simple (capture 6)
"((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+
// 3. anything else (capture 2)
".*" +
")\\)|)",
// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
rwhitespace = new RegExp( whitespace + "+", "g" ),
- rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" +
+ whitespace + "+$", "g" ),
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
- rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
-
- rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
+ rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace +
+ "*" ),
+ rdescend = new RegExp( whitespace + "|>" ),
rpseudo = new RegExp( pseudos ),
ridentifier = new RegExp( "^" + identifier + "$" ),
@@ -654,16 +642,19 @@ var i,
"TAG": new RegExp( "^(" + identifier + "|[*])" ),
"ATTR": new RegExp( "^" + attributes ),
"PSEUDO": new RegExp( "^" + pseudos ),
- "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
- "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
- "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" +
+ whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" +
+ whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+
// For use in libraries implementing .is()
// We use this for POS matching in `select`
- "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
- whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ "needsContext": new RegExp( "^" + whitespace +
+ "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace +
+ "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
},
+ rhtml = /HTML$/i,
rinputs = /^(?:input|select|textarea|button)$/i,
rheader = /^h\d$/i,
@@ -676,24 +667,27 @@ var i,
// CSS escapes
// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
- runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
- funescape = function( _, escaped, escapedWhitespace ) {
- var high = "0x" + escaped - 0x10000;
- // NaN means non-codepoint
- // Support: Firefox<24
- // Workaround erroneous numeric interpretation of +"0x"
- return high !== high || escapedWhitespace ?
- escaped :
+ runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ),
+ funescape = function( escape, nonHex ) {
+ var high = "0x" + escape.slice( 1 ) - 0x10000;
+
+ return nonHex ?
+
+ // Strip the backslash prefix from a non-hex escape sequence
+ nonHex :
+
+ // Replace a hexadecimal escape sequence with the encoded Unicode code point
+ // Support: IE <=11+
+ // For values outside the Basic Multilingual Plane (BMP), manually construct a
+ // surrogate pair
high < 0 ?
- // BMP codepoint
String.fromCharCode( high + 0x10000 ) :
- // Supplemental Plane codepoint (surrogate pair)
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
},
// CSS string/identifier serialization
// https://drafts.csswg.org/cssom/#common-serializing-idioms
- rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,
+ rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
fcssescape = function( ch, asCodePoint ) {
if ( asCodePoint ) {
@@ -703,7 +697,8 @@ var i,
}
// Control characters and (dependent upon position) numbers get escaped as code points
- return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
+ return ch.slice( 0, -1 ) + "\\" +
+ ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
}
// Other potentially-special ASCII characters get backslash-escaped
@@ -718,9 +713,9 @@ var i,
setDocument();
},
- disabledAncestor = addCombinator(
+ inDisabledFieldset = addCombinator(
function( elem ) {
- return elem.disabled === true;
+ return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset";
},
{ dir: "parentNode", next: "legend" }
);
@@ -728,18 +723,20 @@ var i,
// Optimize for push.apply( _, NodeList )
try {
push.apply(
- (arr = slice.call( preferredDoc.childNodes )),
+ ( arr = slice.call( preferredDoc.childNodes ) ),
preferredDoc.childNodes
);
+
// Support: Android<4.0
// Detect silently failing push.apply
+ // eslint-disable-next-line no-unused-expressions
arr[ preferredDoc.childNodes.length ].nodeType;
} catch ( e ) {
push = { apply: arr.length ?
// Leverage slice if possible
function( target, els ) {
- push_native.apply( target, slice.call(els) );
+ pushNative.apply( target, slice.call( els ) );
} :
// Support: IE<9
@@ -747,8 +744,9 @@ try {
function( target, els ) {
var j = target.length,
i = 0;
+
// Can't trust NodeList.length
- while ( (target[j++] = els[i++]) ) {}
+ while ( ( target[ j++ ] = els[ i++ ] ) ) {}
target.length = j - 1;
}
};
@@ -772,24 +770,21 @@ function Sizzle( selector, context, results, seed ) {
// Try to shortcut find operations (as opposed to filters) in HTML documents
if ( !seed ) {
-
- if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
- setDocument( context );
- }
+ setDocument( context );
context = context || document;
if ( documentIsHTML ) {
// If the selector is sufficiently simple, try using a "get*By*" DOM method
// (excepting DocumentFragment context, where the methods don't exist)
- if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
+ if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) {
// ID selector
- if ( (m = match[1]) ) {
+ if ( ( m = match[ 1 ] ) ) {
// Document context
if ( nodeType === 9 ) {
- if ( (elem = context.getElementById( m )) ) {
+ if ( ( elem = context.getElementById( m ) ) ) {
// Support: IE, Opera, Webkit
// TODO: identify versions
@@ -808,7 +803,7 @@ function Sizzle( selector, context, results, seed ) {
// Support: IE, Opera, Webkit
// TODO: identify versions
// getElementById can match elements by name instead of ID
- if ( newContext && (elem = newContext.getElementById( m )) &&
+ if ( newContext && ( elem = newContext.getElementById( m ) ) &&
contains( context, elem ) &&
elem.id === m ) {
@@ -818,12 +813,12 @@ function Sizzle( selector, context, results, seed ) {
}
// Type selector
- } else if ( match[2] ) {
+ } else if ( match[ 2 ] ) {
push.apply( results, context.getElementsByTagName( selector ) );
return results;
// Class selector
- } else if ( (m = match[3]) && support.getElementsByClassName &&
+ } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName &&
context.getElementsByClassName ) {
push.apply( results, context.getElementsByClassName( m ) );
@@ -833,50 +828,62 @@ function Sizzle( selector, context, results, seed ) {
// Take advantage of querySelectorAll
if ( support.qsa &&
- !compilerCache[ selector + " " ] &&
- (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
-
- if ( nodeType !== 1 ) {
- newContext = context;
- newSelector = selector;
+ !nonnativeSelectorCache[ selector + " " ] &&
+ ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) &&
- // qSA looks outside Element context, which is not what we want
- // Thanks to Andrew Dupont for this workaround technique
- // Support: IE <=8
+ // Support: IE 8 only
// Exclude object elements
- } else if ( context.nodeName.toLowerCase() !== "object" ) {
+ ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) {
- // Capture the context ID, setting it first if necessary
- if ( (nid = context.getAttribute( "id" )) ) {
- nid = nid.replace( rcssescape, fcssescape );
- } else {
- context.setAttribute( "id", (nid = expando) );
+ newSelector = selector;
+ newContext = context;
+
+ // qSA considers elements outside a scoping root when evaluating child or
+ // descendant combinators, which is not what we want.
+ // In such cases, we work around the behavior by prefixing every selector in the
+ // list with an ID selector referencing the scope context.
+ // The technique has to be used as well when a leading combinator is used
+ // as such selectors are not recognized by querySelectorAll.
+ // Thanks to Andrew Dupont for this technique.
+ if ( nodeType === 1 &&
+ ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) {
+
+ // Expand context for sibling selectors
+ newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+ context;
+
+ // We can use :scope instead of the ID hack if the browser
+ // supports it & if we're not changing the context.
+ if ( newContext !== context || !support.scope ) {
+
+ // Capture the context ID, setting it first if necessary
+ if ( ( nid = context.getAttribute( "id" ) ) ) {
+ nid = nid.replace( rcssescape, fcssescape );
+ } else {
+ context.setAttribute( "id", ( nid = expando ) );
+ }
}
// Prefix every selector in the list
groups = tokenize( selector );
i = groups.length;
while ( i-- ) {
- groups[i] = "#" + nid + " " + toSelector( groups[i] );
+ groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " +
+ toSelector( groups[ i ] );
}
newSelector = groups.join( "," );
-
- // Expand context for sibling selectors
- newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
- context;
}
- if ( newSelector ) {
- try {
- push.apply( results,
- newContext.querySelectorAll( newSelector )
- );
- return results;
- } catch ( qsaError ) {
- } finally {
- if ( nid === expando ) {
- context.removeAttribute( "id" );
- }
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch ( qsaError ) {
+ nonnativeSelectorCache( selector, true );
+ } finally {
+ if ( nid === expando ) {
+ context.removeAttribute( "id" );
}
}
}
@@ -897,12 +904,14 @@ function createCache() {
var keys = [];
function cache( key, value ) {
+
// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
if ( keys.push( key + " " ) > Expr.cacheLength ) {
+
// Only keep the most recent entries
delete cache[ keys.shift() ];
}
- return (cache[ key + " " ] = value);
+ return ( cache[ key + " " ] = value );
}
return cache;
}
@@ -921,17 +930,19 @@ function markFunction( fn ) {
* @param {Function} fn Passed the created element and returns a boolean result
*/
function assert( fn ) {
- var el = document.createElement("fieldset");
+ var el = document.createElement( "fieldset" );
try {
return !!fn( el );
- } catch (e) {
+ } catch ( e ) {
return false;
} finally {
+
// Remove from its parent by default
if ( el.parentNode ) {
el.parentNode.removeChild( el );
}
+
// release memory in IE
el = null;
}
@@ -943,11 +954,11 @@ function assert( fn ) {
* @param {Function} handler The method that will be applied
*/
function addHandle( attrs, handler ) {
- var arr = attrs.split("|"),
+ var arr = attrs.split( "|" ),
i = arr.length;
while ( i-- ) {
- Expr.attrHandle[ arr[i] ] = handler;
+ Expr.attrHandle[ arr[ i ] ] = handler;
}
}
@@ -969,7 +980,7 @@ function siblingCheck( a, b ) {
// Check if b follows a
if ( cur ) {
- while ( (cur = cur.nextSibling) ) {
+ while ( ( cur = cur.nextSibling ) ) {
if ( cur === b ) {
return -1;
}
@@ -997,7 +1008,7 @@ function createInputPseudo( type ) {
function createButtonPseudo( type ) {
return function( elem ) {
var name = elem.nodeName.toLowerCase();
- return (name === "input" || name === "button") && elem.type === type;
+ return ( name === "input" || name === "button" ) && elem.type === type;
};
}
@@ -1006,26 +1017,54 @@ function createButtonPseudo( type ) {
* @param {Boolean} disabled true for :disabled; false for :enabled
*/
function createDisabledPseudo( disabled ) {
- // Known :disabled false positives:
- // IE: *[disabled]:not(button, input, select, textarea, optgroup, option, menuitem, fieldset)
- // not IE: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
+
+ // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
return function( elem ) {
- // Check form elements and option elements for explicit disabling
- return "label" in elem && elem.disabled === disabled ||
- "form" in elem && elem.disabled === disabled ||
+ // Only certain elements can match :enabled or :disabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+ if ( "form" in elem ) {
+
+ // Check for inherited disabledness on relevant non-disabled elements:
+ // * listed form-associated elements in a disabled fieldset
+ // https://html.spec.whatwg.org/multipage/forms.html#category-listed
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+ // * option elements in a disabled optgroup
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+ // All such elements have a "form" property.
+ if ( elem.parentNode && elem.disabled === false ) {
+
+ // Option elements defer to a parent optgroup if present
+ if ( "label" in elem ) {
+ if ( "label" in elem.parentNode ) {
+ return elem.parentNode.disabled === disabled;
+ } else {
+ return elem.disabled === disabled;
+ }
+ }
- // Check non-disabled form elements for fieldset[disabled] ancestors
- "form" in elem && elem.disabled === false && (
- // Support: IE6-11+
- // Ancestry is covered for us
- elem.isDisabled === disabled ||
+ // Support: IE 6 - 11
+ // Use the isDisabled shortcut property to check for disabled fieldset ancestors
+ return elem.isDisabled === disabled ||
- // Otherwise, assume any non-<option> under fieldset[disabled] is disabled
- /* jshint -W018 */
- elem.isDisabled !== !disabled &&
- ("label" in elem || !disabledAncestor( elem )) !== disabled
- );
+ // Where there is no isDisabled, check manually
+ /* jshint -W018 */
+ elem.isDisabled !== !disabled &&
+ inDisabledFieldset( elem ) === disabled;
+ }
+
+ return elem.disabled === disabled;
+
+ // Try to winnow out elements that can't be disabled before trusting the disabled property.
+ // Some victims get caught in our net (label, legend, menu, track), but it shouldn't
+ // even exist on them, let alone have a boolean value.
+ } else if ( "label" in elem ) {
+ return elem.disabled === disabled;
+ }
+
+ // Remaining elements are neither :enabled nor :disabled
+ return false;
};
}
@@ -1034,21 +1073,21 @@ function createDisabledPseudo( disabled ) {
* @param {Function} fn
*/
function createPositionalPseudo( fn ) {
- return markFunction(function( argument ) {
+ return markFunction( function( argument ) {
argument = +argument;
- return markFunction(function( seed, matches ) {
+ return markFunction( function( seed, matches ) {
var j,
matchIndexes = fn( [], seed.length, argument ),
i = matchIndexes.length;
// Match elements found at the specified indexes
while ( i-- ) {
- if ( seed[ (j = matchIndexes[i]) ] ) {
- seed[j] = !(matches[j] = seed[j]);
+ if ( seed[ ( j = matchIndexes[ i ] ) ] ) {
+ seed[ j ] = !( matches[ j ] = seed[ j ] );
}
}
- });
- });
+ } );
+ } );
}
/**
@@ -1069,10 +1108,13 @@ support = Sizzle.support = {};
* @returns {Boolean} True iff elem is a non-HTML XML node
*/
isXML = Sizzle.isXML = function( elem ) {
- // documentElement is verified for cases where it doesn't yet exist
- // (such as loading iframes in IE - #4833)
- var documentElement = elem && (elem.ownerDocument || elem).documentElement;
- return documentElement ? documentElement.nodeName !== "HTML" : false;
+ var namespace = elem.namespaceURI,
+ docElem = ( elem.ownerDocument || elem ).documentElement;
+
+ // Support: IE <=8
+ // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
+ // https://bugs.jquery.com/ticket/4833
+ return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" );
};
/**
@@ -1085,7 +1127,11 @@ setDocument = Sizzle.setDocument = function( node ) {
doc = node ? node.ownerDocument || node : preferredDoc;
// Return early if doc is invalid or already selected
- if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) {
return document;
}
@@ -1094,10 +1140,14 @@ setDocument = Sizzle.setDocument = function( node ) {
docElem = document.documentElement;
documentIsHTML = !isXML( document );
- // Support: IE 9-11, Edge
+ // Support: IE 9 - 11+, Edge 12 - 18+
// Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
- if ( preferredDoc !== document &&
- (subWindow = document.defaultView) && subWindow.top !== subWindow ) {
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( preferredDoc != document &&
+ ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) {
// Support: IE 11, Edge
if ( subWindow.addEventListener ) {
@@ -1109,25 +1159,36 @@ setDocument = Sizzle.setDocument = function( node ) {
}
}
+ // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only,
+ // Safari 4 - 5 only, Opera <=11.6 - 12.x only
+ // IE/Edge & older browsers don't support the :scope pseudo-class.
+ // Support: Safari 6.0 only
+ // Safari 6.0 supports :scope but it's an alias of :root there.
+ support.scope = assert( function( el ) {
+ docElem.appendChild( el ).appendChild( document.createElement( "div" ) );
+ return typeof el.querySelectorAll !== "undefined" &&
+ !el.querySelectorAll( ":scope fieldset div" ).length;
+ } );
+
/* Attributes
---------------------------------------------------------------------- */
// Support: IE<8
// Verify that getAttribute really returns attributes and not properties
// (excepting IE8 booleans)
- support.attributes = assert(function( el ) {
+ support.attributes = assert( function( el ) {
el.className = "i";
- return !el.getAttribute("className");
- });
+ return !el.getAttribute( "className" );
+ } );
/* getElement(s)By*
---------------------------------------------------------------------- */
// Check if getElementsByTagName("*") returns only elements
- support.getElementsByTagName = assert(function( el ) {
- el.appendChild( document.createComment("") );
- return !el.getElementsByTagName("*").length;
- });
+ support.getElementsByTagName = assert( function( el ) {
+ el.appendChild( document.createComment( "" ) );
+ return !el.getElementsByTagName( "*" ).length;
+ } );
// Support: IE<9
support.getElementsByClassName = rnative.test( document.getElementsByClassName );
@@ -1136,42 +1197,68 @@ setDocument = Sizzle.setDocument = function( node ) {
// Check if getElementById returns elements by name
// The broken getElementById methods don't pick up programmatically-set names,
// so use a roundabout getElementsByName test
- support.getById = assert(function( el ) {
+ support.getById = assert( function( el ) {
docElem.appendChild( el ).id = expando;
return !document.getElementsByName || !document.getElementsByName( expando ).length;
- });
+ } );
- // ID find and filter
+ // ID filter and find
if ( support.getById ) {
- Expr.find["ID"] = function( id, context ) {
- if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
- var m = context.getElementById( id );
- return m ? [ m ] : [];
- }
- };
- Expr.filter["ID"] = function( id ) {
+ Expr.filter[ "ID" ] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
- return elem.getAttribute("id") === attrId;
+ return elem.getAttribute( "id" ) === attrId;
};
};
+ Expr.find[ "ID" ] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var elem = context.getElementById( id );
+ return elem ? [ elem ] : [];
+ }
+ };
} else {
- // Support: IE6/7
- // getElementById is not reliable as a find shortcut
- delete Expr.find["ID"];
-
- Expr.filter["ID"] = function( id ) {
+ Expr.filter[ "ID" ] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
var node = typeof elem.getAttributeNode !== "undefined" &&
- elem.getAttributeNode("id");
+ elem.getAttributeNode( "id" );
return node && node.value === attrId;
};
};
+
+ // Support: IE 6 - 7 only
+ // getElementById is not reliable as a find shortcut
+ Expr.find[ "ID" ] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var node, i, elems,
+ elem = context.getElementById( id );
+
+ if ( elem ) {
+
+ // Verify the id attribute
+ node = elem.getAttributeNode( "id" );
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+
+ // Fall back on getElementsByName
+ elems = context.getElementsByName( id );
+ i = 0;
+ while ( ( elem = elems[ i++ ] ) ) {
+ node = elem.getAttributeNode( "id" );
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+ }
+ }
+
+ return [];
+ }
+ };
}
// Tag
- Expr.find["TAG"] = support.getElementsByTagName ?
+ Expr.find[ "TAG" ] = support.getElementsByTagName ?
function( tag, context ) {
if ( typeof context.getElementsByTagName !== "undefined" ) {
return context.getElementsByTagName( tag );
@@ -1186,12 +1273,13 @@ setDocument = Sizzle.setDocument = function( node ) {
var elem,
tmp = [],
i = 0,
+
// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
results = context.getElementsByTagName( tag );
// Filter out possible comments
if ( tag === "*" ) {
- while ( (elem = results[i++]) ) {
+ while ( ( elem = results[ i++ ] ) ) {
if ( elem.nodeType === 1 ) {
tmp.push( elem );
}
@@ -1203,7 +1291,7 @@ setDocument = Sizzle.setDocument = function( node ) {
};
// Class
- Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+ Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) {
if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
return context.getElementsByClassName( className );
}
@@ -1224,10 +1312,14 @@ setDocument = Sizzle.setDocument = function( node ) {
// See https://bugs.jquery.com/ticket/13378
rbuggyQSA = [];
- if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
+ if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) {
+
// Build QSA regex
// Regex strategy adopted from Diego Perini
- assert(function( el ) {
+ assert( function( el ) {
+
+ var input;
+
// Select is set to empty string on purpose
// This is to test IE's treatment of not explicitly
// setting a boolean content attribute,
@@ -1241,78 +1333,98 @@ setDocument = Sizzle.setDocument = function( node ) {
// Nothing should be selected when empty strings follow ^= or $= or *=
// The test attribute must be unknown in Opera but "safe" for WinRT
// https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
- if ( el.querySelectorAll("[msallowcapture^='']").length ) {
+ if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) {
rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
}
// Support: IE8
// Boolean attributes and "value" are not treated correctly
- if ( !el.querySelectorAll("[selected]").length ) {
+ if ( !el.querySelectorAll( "[selected]" ).length ) {
rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
}
// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
- rbuggyQSA.push("~=");
+ rbuggyQSA.push( "~=" );
+ }
+
+ // Support: IE 11+, Edge 15 - 18+
+ // IE 11/Edge don't find elements on a `[name='']` query in some cases.
+ // Adding a temporary attribute to the document before the selection works
+ // around the issue.
+ // Interestingly, IE 10 & older don't seem to have the issue.
+ input = document.createElement( "input" );
+ input.setAttribute( "name", "" );
+ el.appendChild( input );
+ if ( !el.querySelectorAll( "[name='']" ).length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" +
+ whitespace + "*(?:''|\"\")" );
}
// Webkit/Opera - :checked should return selected option elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
// IE8 throws error here and will not see later tests
- if ( !el.querySelectorAll(":checked").length ) {
- rbuggyQSA.push(":checked");
+ if ( !el.querySelectorAll( ":checked" ).length ) {
+ rbuggyQSA.push( ":checked" );
}
// Support: Safari 8+, iOS 8+
// https://bugs.webkit.org/show_bug.cgi?id=136851
// In-page `selector#id sibling-combinator selector` fails
if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
- rbuggyQSA.push(".#.+[+~]");
+ rbuggyQSA.push( ".#.+[+~]" );
}
- });
- assert(function( el ) {
+ // Support: Firefox <=3.6 - 5 only
+ // Old Firefox doesn't throw on a badly-escaped identifier.
+ el.querySelectorAll( "\\\f" );
+ rbuggyQSA.push( "[\\r\\n\\f]" );
+ } );
+
+ assert( function( el ) {
el.innerHTML = "<a href='' disabled='disabled'></a>" +
"<select disabled='disabled'><option/></select>";
// Support: Windows 8 Native Apps
// The type and name attributes are restricted during .innerHTML assignment
- var input = document.createElement("input");
+ var input = document.createElement( "input" );
input.setAttribute( "type", "hidden" );
el.appendChild( input ).setAttribute( "name", "D" );
// Support: IE8
// Enforce case-sensitivity of name attribute
- if ( el.querySelectorAll("[name=d]").length ) {
+ if ( el.querySelectorAll( "[name=d]" ).length ) {
rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
}
// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
// IE8 throws error here and will not see later tests
- if ( el.querySelectorAll(":enabled").length !== 2 ) {
+ if ( el.querySelectorAll( ":enabled" ).length !== 2 ) {
rbuggyQSA.push( ":enabled", ":disabled" );
}
// Support: IE9-11+
// IE's :disabled selector does not pick up the children of disabled fieldsets
docElem.appendChild( el ).disabled = true;
- if ( el.querySelectorAll(":disabled").length !== 2 ) {
+ if ( el.querySelectorAll( ":disabled" ).length !== 2 ) {
rbuggyQSA.push( ":enabled", ":disabled" );
}
+ // Support: Opera 10 - 11 only
// Opera 10-11 does not throw on post-comma invalid pseudos
- el.querySelectorAll("*,:x");
- rbuggyQSA.push(",.*:");
- });
+ el.querySelectorAll( "*,:x" );
+ rbuggyQSA.push( ",.*:" );
+ } );
}
- if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+ if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches ||
docElem.webkitMatchesSelector ||
docElem.mozMatchesSelector ||
docElem.oMatchesSelector ||
- docElem.msMatchesSelector) )) ) {
+ docElem.msMatchesSelector ) ) ) ) {
+
+ assert( function( el ) {
- assert(function( el ) {
// Check to see if it's possible to do matchesSelector
// on a disconnected node (IE 9)
support.disconnectedMatch = matches.call( el, "*" );
@@ -1321,11 +1433,11 @@ setDocument = Sizzle.setDocument = function( node ) {
// Gecko does not error, returns false instead
matches.call( el, "[s!='']:x" );
rbuggyMatches.push( "!=", pseudos );
- });
+ } );
}
- rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
- rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+ rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) );
/* Contains
---------------------------------------------------------------------- */
@@ -1342,11 +1454,11 @@ setDocument = Sizzle.setDocument = function( node ) {
adown.contains ?
adown.contains( bup ) :
a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
- ));
+ ) );
} :
function( a, b ) {
if ( b ) {
- while ( (b = b.parentNode) ) {
+ while ( ( b = b.parentNode ) ) {
if ( b === a ) {
return true;
}
@@ -1375,7 +1487,11 @@ setDocument = Sizzle.setDocument = function( node ) {
}
// Calculate position if both inputs belong to the same document
- compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ?
a.compareDocumentPosition( b ) :
// Otherwise we know they are disconnected
@@ -1383,13 +1499,24 @@ setDocument = Sizzle.setDocument = function( node ) {
// Disconnected nodes
if ( compare & 1 ||
- (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+ ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) {
// Choose the first element that is related to our preferred document
- if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( a == document || a.ownerDocument == preferredDoc &&
+ contains( preferredDoc, a ) ) {
return -1;
}
- if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( b == document || b.ownerDocument == preferredDoc &&
+ contains( preferredDoc, b ) ) {
return 1;
}
@@ -1402,6 +1529,7 @@ setDocument = Sizzle.setDocument = function( node ) {
return compare & 4 ? -1 : 1;
} :
function( a, b ) {
+
// Exit early if the nodes are identical
if ( a === b ) {
hasDuplicate = true;
@@ -1417,8 +1545,14 @@ setDocument = Sizzle.setDocument = function( node ) {
// Parentless nodes are either documents or disconnected
if ( !aup || !bup ) {
- return a === document ? -1 :
- b === document ? 1 :
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ /* eslint-disable eqeqeq */
+ return a == document ? -1 :
+ b == document ? 1 :
+ /* eslint-enable eqeqeq */
aup ? -1 :
bup ? 1 :
sortInput ?
@@ -1432,26 +1566,32 @@ setDocument = Sizzle.setDocument = function( node ) {
// Otherwise we need full lists of their ancestors for comparison
cur = a;
- while ( (cur = cur.parentNode) ) {
+ while ( ( cur = cur.parentNode ) ) {
ap.unshift( cur );
}
cur = b;
- while ( (cur = cur.parentNode) ) {
+ while ( ( cur = cur.parentNode ) ) {
bp.unshift( cur );
}
// Walk down the tree looking for a discrepancy
- while ( ap[i] === bp[i] ) {
+ while ( ap[ i ] === bp[ i ] ) {
i++;
}
return i ?
+
// Do a sibling check if the nodes have a common ancestor
- siblingCheck( ap[i], bp[i] ) :
+ siblingCheck( ap[ i ], bp[ i ] ) :
// Otherwise nodes in our document sort first
- ap[i] === preferredDoc ? -1 :
- bp[i] === preferredDoc ? 1 :
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ /* eslint-disable eqeqeq */
+ ap[ i ] == preferredDoc ? -1 :
+ bp[ i ] == preferredDoc ? 1 :
+ /* eslint-enable eqeqeq */
0;
};
@@ -1463,16 +1603,10 @@ Sizzle.matches = function( expr, elements ) {
};
Sizzle.matchesSelector = function( elem, expr ) {
- // Set document vars if needed
- if ( ( elem.ownerDocument || elem ) !== document ) {
- setDocument( elem );
- }
-
- // Make sure that attribute selectors are quoted
- expr = expr.replace( rattributeQuotes, "='$1']" );
+ setDocument( elem );
if ( support.matchesSelector && documentIsHTML &&
- !compilerCache[ expr + " " ] &&
+ !nonnativeSelectorCache[ expr + " " ] &&
( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
@@ -1481,32 +1615,46 @@ Sizzle.matchesSelector = function( elem, expr ) {
// IE 9's matchesSelector returns false on disconnected nodes
if ( ret || support.disconnectedMatch ||
- // As well, disconnected nodes are said to be in a document
- // fragment in IE 9
- elem.document && elem.document.nodeType !== 11 ) {
+
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
return ret;
}
- } catch (e) {}
+ } catch ( e ) {
+ nonnativeSelectorCache( expr, true );
+ }
}
return Sizzle( expr, document, null, [ elem ] ).length > 0;
};
Sizzle.contains = function( context, elem ) {
+
// Set document vars if needed
- if ( ( context.ownerDocument || context ) !== document ) {
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( ( context.ownerDocument || context ) != document ) {
setDocument( context );
}
return contains( context, elem );
};
Sizzle.attr = function( elem, name ) {
+
// Set document vars if needed
- if ( ( elem.ownerDocument || elem ) !== document ) {
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( ( elem.ownerDocument || elem ) != document ) {
setDocument( elem );
}
var fn = Expr.attrHandle[ name.toLowerCase() ],
+
// Don't get fooled by Object.prototype properties (jQuery #13807)
val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
fn( elem, name, !documentIsHTML ) :
@@ -1516,13 +1664,13 @@ Sizzle.attr = function( elem, name ) {
val :
support.attributes || !documentIsHTML ?
elem.getAttribute( name ) :
- (val = elem.getAttributeNode(name)) && val.specified ?
+ ( val = elem.getAttributeNode( name ) ) && val.specified ?
val.value :
null;
};
Sizzle.escape = function( sel ) {
- return (sel + "").replace( rcssescape, fcssescape );
+ return ( sel + "" ).replace( rcssescape, fcssescape );
};
Sizzle.error = function( msg ) {
@@ -1545,7 +1693,7 @@ Sizzle.uniqueSort = function( results ) {
results.sort( sortOrder );
if ( hasDuplicate ) {
- while ( (elem = results[i++]) ) {
+ while ( ( elem = results[ i++ ] ) ) {
if ( elem === results[ i ] ) {
j = duplicates.push( i );
}
@@ -1573,17 +1721,21 @@ getText = Sizzle.getText = function( elem ) {
nodeType = elem.nodeType;
if ( !nodeType ) {
+
// If no nodeType, this is expected to be an array
- while ( (node = elem[i++]) ) {
+ while ( ( node = elem[ i++ ] ) ) {
+
// Do not traverse comment nodes
ret += getText( node );
}
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+
// Use textContent for elements
// innerText usage removed for consistency of new lines (jQuery #11153)
if ( typeof elem.textContent === "string" ) {
return elem.textContent;
} else {
+
// Traverse its children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
ret += getText( elem );
@@ -1592,6 +1744,7 @@ getText = Sizzle.getText = function( elem ) {
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
}
+
// Do not include comment or processing instruction nodes
return ret;
@@ -1619,19 +1772,21 @@ Expr = Sizzle.selectors = {
preFilter: {
"ATTR": function( match ) {
- match[1] = match[1].replace( runescape, funescape );
+ match[ 1 ] = match[ 1 ].replace( runescape, funescape );
// Move the given value to match[3] whether quoted or unquoted
- match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+ match[ 3 ] = ( match[ 3 ] || match[ 4 ] ||
+ match[ 5 ] || "" ).replace( runescape, funescape );
- if ( match[2] === "~=" ) {
- match[3] = " " + match[3] + " ";
+ if ( match[ 2 ] === "~=" ) {
+ match[ 3 ] = " " + match[ 3 ] + " ";
}
return match.slice( 0, 4 );
},
"CHILD": function( match ) {
+
/* matches from matchExpr["CHILD"]
1 type (only|nth|...)
2 what (child|of-type)
@@ -1642,22 +1797,25 @@ Expr = Sizzle.selectors = {
7 sign of y-component
8 y of y-component
*/
- match[1] = match[1].toLowerCase();
+ match[ 1 ] = match[ 1 ].toLowerCase();
+
+ if ( match[ 1 ].slice( 0, 3 ) === "nth" ) {
- if ( match[1].slice( 0, 3 ) === "nth" ) {
// nth-* requires argument
- if ( !match[3] ) {
- Sizzle.error( match[0] );
+ if ( !match[ 3 ] ) {
+ Sizzle.error( match[ 0 ] );
}
// numeric x and y parameters for Expr.filter.CHILD
// remember that false/true cast respectively to 0/1
- match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
- match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
-
- // other types prohibit arguments
- } else if ( match[3] ) {
- Sizzle.error( match[0] );
+ match[ 4 ] = +( match[ 4 ] ?
+ match[ 5 ] + ( match[ 6 ] || 1 ) :
+ 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) );
+ match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[ 3 ] ) {
+ Sizzle.error( match[ 0 ] );
}
return match;
@@ -1665,26 +1823,28 @@ Expr = Sizzle.selectors = {
"PSEUDO": function( match ) {
var excess,
- unquoted = !match[6] && match[2];
+ unquoted = !match[ 6 ] && match[ 2 ];
- if ( matchExpr["CHILD"].test( match[0] ) ) {
+ if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) {
return null;
}
// Accept quoted arguments as-is
- if ( match[3] ) {
- match[2] = match[4] || match[5] || "";
+ if ( match[ 3 ] ) {
+ match[ 2 ] = match[ 4 ] || match[ 5 ] || "";
// Strip excess characters from unquoted arguments
} else if ( unquoted && rpseudo.test( unquoted ) &&
+
// Get excess from tokenize (recursively)
- (excess = tokenize( unquoted, true )) &&
+ ( excess = tokenize( unquoted, true ) ) &&
+
// advance to the next closing parenthesis
- (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+ ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) {
// excess is a negative index
- match[0] = match[0].slice( 0, excess );
- match[2] = unquoted.slice( 0, excess );
+ match[ 0 ] = match[ 0 ].slice( 0, excess );
+ match[ 2 ] = unquoted.slice( 0, excess );
}
// Return only captures needed by the pseudo filter method (type and argument)
@@ -1697,7 +1857,9 @@ Expr = Sizzle.selectors = {
"TAG": function( nodeNameSelector ) {
var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
return nodeNameSelector === "*" ?
- function() { return true; } :
+ function() {
+ return true;
+ } :
function( elem ) {
return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
};
@@ -1707,10 +1869,16 @@ Expr = Sizzle.selectors = {
var pattern = classCache[ className + " " ];
return pattern ||
- (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
- classCache( className, function( elem ) {
- return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
- });
+ ( pattern = new RegExp( "(^|" + whitespace +
+ ")" + className + "(" + whitespace + "|$)" ) ) && classCache(
+ className, function( elem ) {
+ return pattern.test(
+ typeof elem.className === "string" && elem.className ||
+ typeof elem.getAttribute !== "undefined" &&
+ elem.getAttribute( "class" ) ||
+ ""
+ );
+ } );
},
"ATTR": function( name, operator, check ) {
@@ -1726,6 +1894,8 @@ Expr = Sizzle.selectors = {
result += "";
+ /* eslint-disable max-len */
+
return operator === "=" ? result === check :
operator === "!=" ? result !== check :
operator === "^=" ? check && result.indexOf( check ) === 0 :
@@ -1734,10 +1904,12 @@ Expr = Sizzle.selectors = {
operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
false;
+ /* eslint-enable max-len */
+
};
},
- "CHILD": function( type, what, argument, first, last ) {
+ "CHILD": function( type, what, _argument, first, last ) {
var simple = type.slice( 0, 3 ) !== "nth",
forward = type.slice( -4 ) !== "last",
ofType = what === "of-type";
@@ -1749,7 +1921,7 @@ Expr = Sizzle.selectors = {
return !!elem.parentNode;
} :
- function( elem, context, xml ) {
+ function( elem, _context, xml ) {
var cache, uniqueCache, outerCache, node, nodeIndex, start,
dir = simple !== forward ? "nextSibling" : "previousSibling",
parent = elem.parentNode,
@@ -1763,7 +1935,7 @@ Expr = Sizzle.selectors = {
if ( simple ) {
while ( dir ) {
node = elem;
- while ( (node = node[ dir ]) ) {
+ while ( ( node = node[ dir ] ) ) {
if ( ofType ?
node.nodeName.toLowerCase() === name :
node.nodeType === 1 ) {
@@ -1771,6 +1943,7 @@ Expr = Sizzle.selectors = {
return false;
}
}
+
// Reverse direction for :only-* (if we haven't yet done so)
start = dir = type === "only" && !start && "nextSibling";
}
@@ -1786,22 +1959,22 @@ Expr = Sizzle.selectors = {
// ...in a gzip-friendly way
node = parent;
- outerCache = node[ expando ] || (node[ expando ] = {});
+ outerCache = node[ expando ] || ( node[ expando ] = {} );
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
uniqueCache = outerCache[ node.uniqueID ] ||
- (outerCache[ node.uniqueID ] = {});
+ ( outerCache[ node.uniqueID ] = {} );
cache = uniqueCache[ type ] || [];
nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
diff = nodeIndex && cache[ 2 ];
node = nodeIndex && parent.childNodes[ nodeIndex ];
- while ( (node = ++nodeIndex && node && node[ dir ] ||
+ while ( ( node = ++nodeIndex && node && node[ dir ] ||
// Fallback to seeking `elem` from the start
- (diff = nodeIndex = 0) || start.pop()) ) {
+ ( diff = nodeIndex = 0 ) || start.pop() ) ) {
// When found, cache indexes on `parent` and break
if ( node.nodeType === 1 && ++diff && node === elem ) {
@@ -1811,16 +1984,18 @@ Expr = Sizzle.selectors = {
}
} else {
+
// Use previously-cached element index if available
if ( useCache ) {
+
// ...in a gzip-friendly way
node = elem;
- outerCache = node[ expando ] || (node[ expando ] = {});
+ outerCache = node[ expando ] || ( node[ expando ] = {} );
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
uniqueCache = outerCache[ node.uniqueID ] ||
- (outerCache[ node.uniqueID ] = {});
+ ( outerCache[ node.uniqueID ] = {} );
cache = uniqueCache[ type ] || [];
nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
@@ -1830,9 +2005,10 @@ Expr = Sizzle.selectors = {
// xml :nth-child(...)
// or :nth-last-child(...) or :nth(-last)?-of-type(...)
if ( diff === false ) {
+
// Use the same loop as above to seek `elem` from the start
- while ( (node = ++nodeIndex && node && node[ dir ] ||
- (diff = nodeIndex = 0) || start.pop()) ) {
+ while ( ( node = ++nodeIndex && node && node[ dir ] ||
+ ( diff = nodeIndex = 0 ) || start.pop() ) ) {
if ( ( ofType ?
node.nodeName.toLowerCase() === name :
@@ -1841,12 +2017,13 @@ Expr = Sizzle.selectors = {
// Cache the index of each encountered element
if ( useCache ) {
- outerCache = node[ expando ] || (node[ expando ] = {});
+ outerCache = node[ expando ] ||
+ ( node[ expando ] = {} );
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
uniqueCache = outerCache[ node.uniqueID ] ||
- (outerCache[ node.uniqueID ] = {});
+ ( outerCache[ node.uniqueID ] = {} );
uniqueCache[ type ] = [ dirruns, diff ];
}
@@ -1867,6 +2044,7 @@ Expr = Sizzle.selectors = {
},
"PSEUDO": function( pseudo, argument ) {
+
// pseudo-class names are case-insensitive
// http://www.w3.org/TR/selectors/#pseudo-classes
// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
@@ -1886,15 +2064,15 @@ Expr = Sizzle.selectors = {
if ( fn.length > 1 ) {
args = [ pseudo, pseudo, "", argument ];
return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
- markFunction(function( seed, matches ) {
+ markFunction( function( seed, matches ) {
var idx,
matched = fn( seed, argument ),
i = matched.length;
while ( i-- ) {
- idx = indexOf( seed, matched[i] );
- seed[ idx ] = !( matches[ idx ] = matched[i] );
+ idx = indexOf( seed, matched[ i ] );
+ seed[ idx ] = !( matches[ idx ] = matched[ i ] );
}
- }) :
+ } ) :
function( elem ) {
return fn( elem, 0, args );
};
@@ -1905,8 +2083,10 @@ Expr = Sizzle.selectors = {
},
pseudos: {
+
// Potentially complex pseudos
- "not": markFunction(function( selector ) {
+ "not": markFunction( function( selector ) {
+
// Trim the selector passed to compile
// to avoid treating leading and trailing
// spaces as combinators
@@ -1915,39 +2095,40 @@ Expr = Sizzle.selectors = {
matcher = compile( selector.replace( rtrim, "$1" ) );
return matcher[ expando ] ?
- markFunction(function( seed, matches, context, xml ) {
+ markFunction( function( seed, matches, _context, xml ) {
var elem,
unmatched = matcher( seed, null, xml, [] ),
i = seed.length;
// Match elements unmatched by `matcher`
while ( i-- ) {
- if ( (elem = unmatched[i]) ) {
- seed[i] = !(matches[i] = elem);
+ if ( ( elem = unmatched[ i ] ) ) {
+ seed[ i ] = !( matches[ i ] = elem );
}
}
- }) :
- function( elem, context, xml ) {
- input[0] = elem;
+ } ) :
+ function( elem, _context, xml ) {
+ input[ 0 ] = elem;
matcher( input, null, xml, results );
+
// Don't keep the element (issue #299)
- input[0] = null;
+ input[ 0 ] = null;
return !results.pop();
};
- }),
+ } ),
- "has": markFunction(function( selector ) {
+ "has": markFunction( function( selector ) {
return function( elem ) {
return Sizzle( selector, elem ).length > 0;
};
- }),
+ } ),
- "contains": markFunction(function( text ) {
+ "contains": markFunction( function( text ) {
text = text.replace( runescape, funescape );
return function( elem ) {
- return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+ return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;
};
- }),
+ } ),
// "Whether an element is represented by a :lang() selector
// is based solely on the element's language value
@@ -1957,25 +2138,26 @@ Expr = Sizzle.selectors = {
// The identifier C does not have to be a valid language name."
// http://www.w3.org/TR/selectors/#lang-pseudo
"lang": markFunction( function( lang ) {
+
// lang value must be a valid identifier
- if ( !ridentifier.test(lang || "") ) {
+ if ( !ridentifier.test( lang || "" ) ) {
Sizzle.error( "unsupported lang: " + lang );
}
lang = lang.replace( runescape, funescape ).toLowerCase();
return function( elem ) {
var elemLang;
do {
- if ( (elemLang = documentIsHTML ?
+ if ( ( elemLang = documentIsHTML ?
elem.lang :
- elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+ elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) {
elemLang = elemLang.toLowerCase();
return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
}
- } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+ } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 );
return false;
};
- }),
+ } ),
// Miscellaneous
"target": function( elem ) {
@@ -1988,7 +2170,9 @@ Expr = Sizzle.selectors = {
},
"focus": function( elem ) {
- return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+ return elem === document.activeElement &&
+ ( !document.hasFocus || document.hasFocus() ) &&
+ !!( elem.type || elem.href || ~elem.tabIndex );
},
// Boolean properties
@@ -1996,16 +2180,20 @@ Expr = Sizzle.selectors = {
"disabled": createDisabledPseudo( true ),
"checked": function( elem ) {
+
// In CSS3, :checked should return both checked and selected elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
var nodeName = elem.nodeName.toLowerCase();
- return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+ return ( nodeName === "input" && !!elem.checked ) ||
+ ( nodeName === "option" && !!elem.selected );
},
"selected": function( elem ) {
+
// Accessing this property makes selected-by-default
// options in Safari work properly
if ( elem.parentNode ) {
+ // eslint-disable-next-line no-unused-expressions
elem.parentNode.selectedIndex;
}
@@ -2014,6 +2202,7 @@ Expr = Sizzle.selectors = {
// Contents
"empty": function( elem ) {
+
// http://www.w3.org/TR/selectors/#empty-pseudo
// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
// but not by others (comment: 8; processing instruction: 7; etc.)
@@ -2027,7 +2216,7 @@ Expr = Sizzle.selectors = {
},
"parent": function( elem ) {
- return !Expr.pseudos["empty"]( elem );
+ return !Expr.pseudos[ "empty" ]( elem );
},
// Element/input types
@@ -2051,57 +2240,62 @@ Expr = Sizzle.selectors = {
// Support: IE<8
// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
- ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+ ( ( attr = elem.getAttribute( "type" ) ) == null ||
+ attr.toLowerCase() === "text" );
},
// Position-in-collection
- "first": createPositionalPseudo(function() {
+ "first": createPositionalPseudo( function() {
return [ 0 ];
- }),
+ } ),
- "last": createPositionalPseudo(function( matchIndexes, length ) {
+ "last": createPositionalPseudo( function( _matchIndexes, length ) {
return [ length - 1 ];
- }),
+ } ),
- "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) {
return [ argument < 0 ? argument + length : argument ];
- }),
+ } ),
- "even": createPositionalPseudo(function( matchIndexes, length ) {
+ "even": createPositionalPseudo( function( matchIndexes, length ) {
var i = 0;
for ( ; i < length; i += 2 ) {
matchIndexes.push( i );
}
return matchIndexes;
- }),
+ } ),
- "odd": createPositionalPseudo(function( matchIndexes, length ) {
+ "odd": createPositionalPseudo( function( matchIndexes, length ) {
var i = 1;
for ( ; i < length; i += 2 ) {
matchIndexes.push( i );
}
return matchIndexes;
- }),
+ } ),
- "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
- var i = argument < 0 ? argument + length : argument;
+ "lt": createPositionalPseudo( function( matchIndexes, length, argument ) {
+ var i = argument < 0 ?
+ argument + length :
+ argument > length ?
+ length :
+ argument;
for ( ; --i >= 0; ) {
matchIndexes.push( i );
}
return matchIndexes;
- }),
+ } ),
- "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ "gt": createPositionalPseudo( function( matchIndexes, length, argument ) {
var i = argument < 0 ? argument + length : argument;
for ( ; ++i < length; ) {
matchIndexes.push( i );
}
return matchIndexes;
- })
+ } )
}
};
-Expr.pseudos["nth"] = Expr.pseudos["eq"];
+Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ];
// Add button/input type pseudos
for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
@@ -2132,37 +2326,39 @@ tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
while ( soFar ) {
// Comma and first run
- if ( !matched || (match = rcomma.exec( soFar )) ) {
+ if ( !matched || ( match = rcomma.exec( soFar ) ) ) {
if ( match ) {
+
// Don't consume trailing commas as valid
- soFar = soFar.slice( match[0].length ) || soFar;
+ soFar = soFar.slice( match[ 0 ].length ) || soFar;
}
- groups.push( (tokens = []) );
+ groups.push( ( tokens = [] ) );
}
matched = false;
// Combinators
- if ( (match = rcombinators.exec( soFar )) ) {
+ if ( ( match = rcombinators.exec( soFar ) ) ) {
matched = match.shift();
- tokens.push({
+ tokens.push( {
value: matched,
+
// Cast descendant combinators to space
- type: match[0].replace( rtrim, " " )
- });
+ type: match[ 0 ].replace( rtrim, " " )
+ } );
soFar = soFar.slice( matched.length );
}
// Filters
for ( type in Expr.filter ) {
- if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
- (match = preFilters[ type ]( match ))) ) {
+ if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] ||
+ ( match = preFilters[ type ]( match ) ) ) ) {
matched = match.shift();
- tokens.push({
+ tokens.push( {
value: matched,
type: type,
matches: match
- });
+ } );
soFar = soFar.slice( matched.length );
}
}
@@ -2179,6 +2375,7 @@ tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
soFar.length :
soFar ?
Sizzle.error( selector ) :
+
// Cache the tokens
tokenCache( selector, groups ).slice( 0 );
};
@@ -2188,7 +2385,7 @@ function toSelector( tokens ) {
len = tokens.length,
selector = "";
for ( ; i < len; i++ ) {
- selector += tokens[i].value;
+ selector += tokens[ i ].value;
}
return selector;
}
@@ -2201,13 +2398,15 @@ function addCombinator( matcher, combinator, base ) {
doneName = done++;
return combinator.first ?
+
// Check against closest ancestor/preceding element
function( elem, context, xml ) {
- while ( (elem = elem[ dir ]) ) {
+ while ( ( elem = elem[ dir ] ) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
return matcher( elem, context, xml );
}
}
+ return false;
} :
// Check against all ancestor/preceding elements
@@ -2217,7 +2416,7 @@ function addCombinator( matcher, combinator, base ) {
// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
if ( xml ) {
- while ( (elem = elem[ dir ]) ) {
+ while ( ( elem = elem[ dir ] ) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
if ( matcher( elem, context, xml ) ) {
return true;
@@ -2225,33 +2424,36 @@ function addCombinator( matcher, combinator, base ) {
}
}
} else {
- while ( (elem = elem[ dir ]) ) {
+ while ( ( elem = elem[ dir ] ) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
- outerCache = elem[ expando ] || (elem[ expando ] = {});
+ outerCache = elem[ expando ] || ( elem[ expando ] = {} );
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
- uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
+ uniqueCache = outerCache[ elem.uniqueID ] ||
+ ( outerCache[ elem.uniqueID ] = {} );
if ( skip && skip === elem.nodeName.toLowerCase() ) {
elem = elem[ dir ] || elem;
- } else if ( (oldCache = uniqueCache[ key ]) &&
+ } else if ( ( oldCache = uniqueCache[ key ] ) &&
oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
// Assign to newCache so results back-propagate to previous elements
- return (newCache[ 2 ] = oldCache[ 2 ]);
+ return ( newCache[ 2 ] = oldCache[ 2 ] );
} else {
+
// Reuse newcache so results back-propagate to previous elements
uniqueCache[ key ] = newCache;
// A match means we're done; a fail means we have to keep checking
- if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+ if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) {
return true;
}
}
}
}
}
+ return false;
};
}
@@ -2260,20 +2462,20 @@ function elementMatcher( matchers ) {
function( elem, context, xml ) {
var i = matchers.length;
while ( i-- ) {
- if ( !matchers[i]( elem, context, xml ) ) {
+ if ( !matchers[ i ]( elem, context, xml ) ) {
return false;
}
}
return true;
} :
- matchers[0];
+ matchers[ 0 ];
}
function multipleContexts( selector, contexts, results ) {
var i = 0,
len = contexts.length;
for ( ; i < len; i++ ) {
- Sizzle( selector, contexts[i], results );
+ Sizzle( selector, contexts[ i ], results );
}
return results;
}
@@ -2286,7 +2488,7 @@ function condense( unmatched, map, filter, context, xml ) {
mapped = map != null;
for ( ; i < len; i++ ) {
- if ( (elem = unmatched[i]) ) {
+ if ( ( elem = unmatched[ i ] ) ) {
if ( !filter || filter( elem, context, xml ) ) {
newUnmatched.push( elem );
if ( mapped ) {
@@ -2306,14 +2508,18 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
if ( postFinder && !postFinder[ expando ] ) {
postFinder = setMatcher( postFinder, postSelector );
}
- return markFunction(function( seed, results, context, xml ) {
+ return markFunction( function( seed, results, context, xml ) {
var temp, i, elem,
preMap = [],
postMap = [],
preexisting = results.length,
// Get initial elements from seed or context
- elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+ elems = seed || multipleContexts(
+ selector || "*",
+ context.nodeType ? [ context ] : context,
+ []
+ ),
// Prefilter to get matcher input, preserving a map for seed-results synchronization
matcherIn = preFilter && ( seed || !selector ) ?
@@ -2321,6 +2527,7 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
elems,
matcherOut = matcher ?
+
// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
@@ -2344,8 +2551,8 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
// Un-match failing elements by moving them back to matcherIn
i = temp.length;
while ( i-- ) {
- if ( (elem = temp[i]) ) {
- matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+ if ( ( elem = temp[ i ] ) ) {
+ matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem );
}
}
}
@@ -2353,25 +2560,27 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
if ( seed ) {
if ( postFinder || preFilter ) {
if ( postFinder ) {
+
// Get the final matcherOut by condensing this intermediate into postFinder contexts
temp = [];
i = matcherOut.length;
while ( i-- ) {
- if ( (elem = matcherOut[i]) ) {
+ if ( ( elem = matcherOut[ i ] ) ) {
+
// Restore matcherIn since elem is not yet a final match
- temp.push( (matcherIn[i] = elem) );
+ temp.push( ( matcherIn[ i ] = elem ) );
}
}
- postFinder( null, (matcherOut = []), temp, xml );
+ postFinder( null, ( matcherOut = [] ), temp, xml );
}
// Move matched elements from seed to results to keep them synchronized
i = matcherOut.length;
while ( i-- ) {
- if ( (elem = matcherOut[i]) &&
- (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
+ if ( ( elem = matcherOut[ i ] ) &&
+ ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) {
- seed[temp] = !(results[temp] = elem);
+ seed[ temp ] = !( results[ temp ] = elem );
}
}
}
@@ -2389,14 +2598,14 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
push.apply( results, matcherOut );
}
}
- });
+ } );
}
function matcherFromTokens( tokens ) {
var checkContext, matcher, j,
len = tokens.length,
- leadingRelative = Expr.relative[ tokens[0].type ],
- implicitRelative = leadingRelative || Expr.relative[" "],
+ leadingRelative = Expr.relative[ tokens[ 0 ].type ],
+ implicitRelative = leadingRelative || Expr.relative[ " " ],
i = leadingRelative ? 1 : 0,
// The foundational matcher ensures that elements are reachable from top-level context(s)
@@ -2408,38 +2617,43 @@ function matcherFromTokens( tokens ) {
}, implicitRelative, true ),
matchers = [ function( elem, context, xml ) {
var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
- (checkContext = context).nodeType ?
+ ( checkContext = context ).nodeType ?
matchContext( elem, context, xml ) :
matchAnyContext( elem, context, xml ) );
+
// Avoid hanging onto element (issue #299)
checkContext = null;
return ret;
} ];
for ( ; i < len; i++ ) {
- if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
- matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+ if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) {
+ matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];
} else {
- matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+ matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches );
// Return special upon seeing a positional matcher
if ( matcher[ expando ] ) {
+
// Find the next relative operator (if any) for proper handling
j = ++i;
for ( ; j < len; j++ ) {
- if ( Expr.relative[ tokens[j].type ] ) {
+ if ( Expr.relative[ tokens[ j ].type ] ) {
break;
}
}
return setMatcher(
i > 1 && elementMatcher( matchers ),
i > 1 && toSelector(
- // If the preceding token was a descendant combinator, insert an implicit any-element `*`
- tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+
+ // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+ tokens
+ .slice( 0, i - 1 )
+ .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } )
).replace( rtrim, "$1" ),
matcher,
i < j && matcherFromTokens( tokens.slice( i, j ) ),
- j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+ j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ),
j < len && toSelector( tokens )
);
}
@@ -2460,28 +2674,40 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
unmatched = seed && [],
setMatched = [],
contextBackup = outermostContext,
+
// We must always have either seed elements or outermost context
- elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+ elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ),
+
// Use integer dirruns iff this is the outermost matcher
- dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+ dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ),
len = elems.length;
if ( outermost ) {
- outermostContext = context === document || context || outermost;
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ outermostContext = context == document || context || outermost;
}
// Add elements passing elementMatchers directly to results
// Support: IE<9, Safari
// Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
- for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+ for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) {
if ( byElement && elem ) {
j = 0;
- if ( !context && elem.ownerDocument !== document ) {
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( !context && elem.ownerDocument != document ) {
setDocument( elem );
xml = !documentIsHTML;
}
- while ( (matcher = elementMatchers[j++]) ) {
- if ( matcher( elem, context || document, xml) ) {
+ while ( ( matcher = elementMatchers[ j++ ] ) ) {
+ if ( matcher( elem, context || document, xml ) ) {
results.push( elem );
break;
}
@@ -2493,8 +2719,9 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
// Track unmatched elements for set filters
if ( bySet ) {
+
// They will have gone through all possible matchers
- if ( (elem = !matcher && elem) ) {
+ if ( ( elem = !matcher && elem ) ) {
matchedCount--;
}
@@ -2518,16 +2745,17 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
// numerically zero.
if ( bySet && i !== matchedCount ) {
j = 0;
- while ( (matcher = setMatchers[j++]) ) {
+ while ( ( matcher = setMatchers[ j++ ] ) ) {
matcher( unmatched, setMatched, context, xml );
}
if ( seed ) {
+
// Reintegrate element matches to eliminate the need for sorting
if ( matchedCount > 0 ) {
while ( i-- ) {
- if ( !(unmatched[i] || setMatched[i]) ) {
- setMatched[i] = pop.call( results );
+ if ( !( unmatched[ i ] || setMatched[ i ] ) ) {
+ setMatched[ i ] = pop.call( results );
}
}
}
@@ -2568,13 +2796,14 @@ compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
cached = compilerCache[ selector + " " ];
if ( !cached ) {
+
// Generate a function of recursive functions that can be used to check each element
if ( !match ) {
match = tokenize( selector );
}
i = match.length;
while ( i-- ) {
- cached = matcherFromTokens( match[i] );
+ cached = matcherFromTokens( match[ i ] );
if ( cached[ expando ] ) {
setMatchers.push( cached );
} else {
@@ -2583,7 +2812,10 @@ compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
}
// Cache the compiled function
- cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+ cached = compilerCache(
+ selector,
+ matcherFromGroupMatchers( elementMatchers, setMatchers )
+ );
// Save selector and tokenization
cached.selector = selector;
@@ -2603,7 +2835,7 @@ compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
select = Sizzle.select = function( selector, context, results, seed ) {
var i, tokens, token, type, find,
compiled = typeof selector === "function" && selector,
- match = !seed && tokenize( (selector = compiled.selector || selector) );
+ match = !seed && tokenize( ( selector = compiled.selector || selector ) );
results = results || [];
@@ -2612,12 +2844,12 @@ select = Sizzle.select = function( selector, context, results, seed ) {
if ( match.length === 1 ) {
// Reduce context if the leading compound selector is an ID
- tokens = match[0] = match[0].slice( 0 );
- if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
- support.getById && context.nodeType === 9 && documentIsHTML &&
- Expr.relative[ tokens[1].type ] ) {
+ tokens = match[ 0 ] = match[ 0 ].slice( 0 );
+ if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" &&
+ context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) {
- context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+ context = ( Expr.find[ "ID" ]( token.matches[ 0 ]
+ .replace( runescape, funescape ), context ) || [] )[ 0 ];
if ( !context ) {
return results;
@@ -2630,20 +2862,22 @@ select = Sizzle.select = function( selector, context, results, seed ) {
}
// Fetch a seed set for right-to-left matching
- i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+ i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length;
while ( i-- ) {
- token = tokens[i];
+ token = tokens[ i ];
// Abort if we hit a combinator
- if ( Expr.relative[ (type = token.type) ] ) {
+ if ( Expr.relative[ ( type = token.type ) ] ) {
break;
}
- if ( (find = Expr.find[ type ]) ) {
+ if ( ( find = Expr.find[ type ] ) ) {
+
// Search, expanding context for leading sibling combinators
- if ( (seed = find(
- token.matches[0].replace( runescape, funescape ),
- rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
- )) ) {
+ if ( ( seed = find(
+ token.matches[ 0 ].replace( runescape, funescape ),
+ rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) ||
+ context
+ ) ) ) {
// If seed is empty or no tokens remain, we can return early
tokens.splice( i, 1 );
@@ -2674,7 +2908,7 @@ select = Sizzle.select = function( selector, context, results, seed ) {
// One-time assignments
// Sort stability
-support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando;
// Support: Chrome 14-35+
// Always assume duplicates if they aren't passed to the comparison function
@@ -2685,58 +2919,59 @@ setDocument();
// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
// Detached nodes confoundingly follow *each other*
-support.sortDetached = assert(function( el ) {
+support.sortDetached = assert( function( el ) {
+
// Should return 1, but returns 4 (following)
- return el.compareDocumentPosition( document.createElement("fieldset") ) & 1;
-});
+ return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1;
+} );
// Support: IE<8
// Prevent attribute/property "interpolation"
// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
-if ( !assert(function( el ) {
+if ( !assert( function( el ) {
el.innerHTML = "<a href='#'></a>";
- return el.firstChild.getAttribute("href") === "#" ;
-}) ) {
+ return el.firstChild.getAttribute( "href" ) === "#";
+} ) ) {
addHandle( "type|href|height|width", function( elem, name, isXML ) {
if ( !isXML ) {
return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
}
- });
+ } );
}
// Support: IE<9
// Use defaultValue in place of getAttribute("value")
-if ( !support.attributes || !assert(function( el ) {
+if ( !support.attributes || !assert( function( el ) {
el.innerHTML = "<input/>";
el.firstChild.setAttribute( "value", "" );
return el.firstChild.getAttribute( "value" ) === "";
-}) ) {
- addHandle( "value", function( elem, name, isXML ) {
+} ) ) {
+ addHandle( "value", function( elem, _name, isXML ) {
if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
return elem.defaultValue;
}
- });
+ } );
}
// Support: IE<9
// Use getAttributeNode to fetch booleans when getAttribute lies
-if ( !assert(function( el ) {
- return el.getAttribute("disabled") == null;
-}) ) {
+if ( !assert( function( el ) {
+ return el.getAttribute( "disabled" ) == null;
+} ) ) {
addHandle( booleans, function( elem, name, isXML ) {
var val;
if ( !isXML ) {
return elem[ name ] === true ? name.toLowerCase() :
- (val = elem.getAttributeNode( name )) && val.specified ?
+ ( val = elem.getAttributeNode( name ) ) && val.specified ?
val.value :
- null;
+ null;
}
- });
+ } );
}
return Sizzle;
-})( window );
+} )( window );
@@ -2785,39 +3020,41 @@ var siblings = function( n, elem ) {
var rneedsContext = jQuery.expr.match.needsContext;
-var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
+function nodeName( elem, name ) {
+
+ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+
+};
+var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
+
-var risSimple = /^.[^:#\[\.,]*$/;
// Implement the identical functionality for filter and not
function winnow( elements, qualifier, not ) {
- if ( jQuery.isFunction( qualifier ) ) {
+ if ( isFunction( qualifier ) ) {
return jQuery.grep( elements, function( elem, i ) {
return !!qualifier.call( elem, i, elem ) !== not;
} );
-
}
+ // Single element
if ( qualifier.nodeType ) {
return jQuery.grep( elements, function( elem ) {
return ( elem === qualifier ) !== not;
} );
-
}
- if ( typeof qualifier === "string" ) {
- if ( risSimple.test( qualifier ) ) {
- return jQuery.filter( qualifier, elements, not );
- }
-
- qualifier = jQuery.filter( qualifier, elements );
+ // Arraylike of elements (jQuery, arguments, Array)
+ if ( typeof qualifier !== "string" ) {
+ return jQuery.grep( elements, function( elem ) {
+ return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
+ } );
}
- return jQuery.grep( elements, function( elem ) {
- return ( indexOf.call( qualifier, elem ) > -1 ) !== not && elem.nodeType === 1;
- } );
+ // Filtered directly for both simple and complex selectors
+ return jQuery.filter( qualifier, elements, not );
}
jQuery.filter = function( expr, elems, not ) {
@@ -2827,11 +3064,13 @@ jQuery.filter = function( expr, elems, not ) {
expr = ":not(" + expr + ")";
}
- return elems.length === 1 && elem.nodeType === 1 ?
- jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
- jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
- return elem.nodeType === 1;
- } ) );
+ if ( elems.length === 1 && elem.nodeType === 1 ) {
+ return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
+ }
+
+ return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+ return elem.nodeType === 1;
+ } ) );
};
jQuery.fn.extend( {
@@ -2936,7 +3175,7 @@ var rootjQuery,
for ( match in context ) {
// Properties of context are called as methods if possible
- if ( jQuery.isFunction( this[ match ] ) ) {
+ if ( isFunction( this[ match ] ) ) {
this[ match ]( context[ match ] );
// ...and otherwise set as attributes
@@ -2979,7 +3218,7 @@ var rootjQuery,
// HANDLE: $(function)
// Shortcut for document ready
- } else if ( jQuery.isFunction( selector ) ) {
+ } else if ( isFunction( selector ) ) {
return root.ready !== undefined ?
root.ready( selector ) :
@@ -3101,7 +3340,7 @@ jQuery.each( {
parents: function( elem ) {
return dir( elem, "parentNode" );
},
- parentsUntil: function( elem, i, until ) {
+ parentsUntil: function( elem, _i, until ) {
return dir( elem, "parentNode", until );
},
next: function( elem ) {
@@ -3116,10 +3355,10 @@ jQuery.each( {
prevAll: function( elem ) {
return dir( elem, "previousSibling" );
},
- nextUntil: function( elem, i, until ) {
+ nextUntil: function( elem, _i, until ) {
return dir( elem, "nextSibling", until );
},
- prevUntil: function( elem, i, until ) {
+ prevUntil: function( elem, _i, until ) {
return dir( elem, "previousSibling", until );
},
siblings: function( elem ) {
@@ -3129,7 +3368,24 @@ jQuery.each( {
return siblings( elem.firstChild );
},
contents: function( elem ) {
- return elem.contentDocument || jQuery.merge( [], elem.childNodes );
+ if ( elem.contentDocument != null &&
+
+ // Support: IE 11+
+ // <object> elements with no `data` attribute has an object
+ // `contentDocument` with a `null` prototype.
+ getProto( elem.contentDocument ) ) {
+
+ return elem.contentDocument;
+ }
+
+ // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
+ // Treat the template element as a regular one in browsers that
+ // don't support it.
+ if ( nodeName( elem, "template" ) ) {
+ elem = elem.content || elem;
+ }
+
+ return jQuery.merge( [], elem.childNodes );
}
}, function( name, fn ) {
jQuery.fn[ name ] = function( until, selector ) {
@@ -3159,14 +3415,14 @@ jQuery.each( {
return this.pushStack( matched );
};
} );
-var rnotwhite = ( /\S+/g );
+var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g );
// Convert String-formatted options into Object-formatted ones
function createOptions( options ) {
var object = {};
- jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
+ jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
object[ flag ] = true;
} );
return object;
@@ -3227,7 +3483,7 @@ jQuery.Callbacks = function( options ) {
fire = function() {
// Enforce single-firing
- locked = options.once;
+ locked = locked || options.once;
// Execute callbacks for all pending executions,
// respecting firingIndex overrides and runtime changes
@@ -3283,11 +3539,11 @@ jQuery.Callbacks = function( options ) {
( function add( args ) {
jQuery.each( args, function( _, arg ) {
- if ( jQuery.isFunction( arg ) ) {
+ if ( isFunction( arg ) ) {
if ( !options.unique || !self.has( arg ) ) {
list.push( arg );
}
- } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {
+ } else if ( arg && arg.length && toType( arg ) !== "string" ) {
// Inspect recursively
add( arg );
@@ -3396,25 +3652,26 @@ function Thrower( ex ) {
throw ex;
}
-function adoptValue( value, resolve, reject ) {
+function adoptValue( value, resolve, reject, noValue ) {
var method;
try {
// Check for promise aspect first to privilege synchronous behavior
- if ( value && jQuery.isFunction( ( method = value.promise ) ) ) {
+ if ( value && isFunction( ( method = value.promise ) ) ) {
method.call( value ).done( resolve ).fail( reject );
// Other thenables
- } else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {
+ } else if ( value && isFunction( ( method = value.then ) ) ) {
method.call( value, resolve, reject );
// Other non-thenables
} else {
- // Support: Android 4.0 only
- // Strict mode functions invoked without .call/.apply get global-object context
- resolve.call( undefined, value );
+ // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
+ // * false: [ value ].slice( 0 ) => resolve( value )
+ // * true: [ value ].slice( 1 ) => resolve()
+ resolve.apply( undefined, [ value ].slice( noValue ) );
}
// For Promises/A+, convert exceptions into rejections
@@ -3424,7 +3681,7 @@ function adoptValue( value, resolve, reject ) {
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
- reject.call( undefined, value );
+ reject.apply( undefined, [ value ] );
}
}
@@ -3460,17 +3717,17 @@ jQuery.extend( {
var fns = arguments;
return jQuery.Deferred( function( newDefer ) {
- jQuery.each( tuples, function( i, tuple ) {
+ jQuery.each( tuples, function( _i, tuple ) {
// Map tuples (progress, done, fail) to arguments (done, fail, progress)
- var fn = jQuery.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];
+ var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];
// deferred.progress(function() { bind to newDefer or newDefer.notify })
// deferred.done(function() { bind to newDefer or newDefer.resolve })
// deferred.fail(function() { bind to newDefer or newDefer.reject })
deferred[ tuple[ 1 ] ]( function() {
var returned = fn && fn.apply( this, arguments );
- if ( returned && jQuery.isFunction( returned.promise ) ) {
+ if ( returned && isFunction( returned.promise ) ) {
returned.promise()
.progress( newDefer.notify )
.done( newDefer.resolve )
@@ -3524,7 +3781,7 @@ jQuery.extend( {
returned.then;
// Handle a returned thenable
- if ( jQuery.isFunction( then ) ) {
+ if ( isFunction( then ) ) {
// Special processors (notify) just wait for resolution
if ( special ) {
@@ -3620,7 +3877,7 @@ jQuery.extend( {
resolve(
0,
newDefer,
- jQuery.isFunction( onProgress ) ?
+ isFunction( onProgress ) ?
onProgress :
Identity,
newDefer.notifyWith
@@ -3632,7 +3889,7 @@ jQuery.extend( {
resolve(
0,
newDefer,
- jQuery.isFunction( onFulfilled ) ?
+ isFunction( onFulfilled ) ?
onFulfilled :
Identity
)
@@ -3643,7 +3900,7 @@ jQuery.extend( {
resolve(
0,
newDefer,
- jQuery.isFunction( onRejected ) ?
+ isFunction( onRejected ) ?
onRejected :
Thrower
)
@@ -3683,8 +3940,15 @@ jQuery.extend( {
// fulfilled_callbacks.disable
tuples[ 3 - i ][ 2 ].disable,
+ // rejected_handlers.disable
+ // fulfilled_handlers.disable
+ tuples[ 3 - i ][ 3 ].disable,
+
// progress_callbacks.lock
- tuples[ 0 ][ 2 ].lock
+ tuples[ 0 ][ 2 ].lock,
+
+ // progress_handlers.lock
+ tuples[ 0 ][ 3 ].lock
);
}
@@ -3749,11 +4013,12 @@ jQuery.extend( {
// Single- and empty arguments are adopted like Promise.resolve
if ( remaining <= 1 ) {
- adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject );
+ adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
+ !remaining );
// Use .then() to unwrap secondary thenables (cf. gh-3000)
if ( master.state() === "pending" ||
- jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
+ isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
return master.then();
}
@@ -3821,15 +4086,6 @@ jQuery.extend( {
// the ready event fires. See #6781
readyWait: 1,
- // Hold (or release) the ready event
- holdReady: function( hold ) {
- if ( hold ) {
- jQuery.readyWait++;
- } else {
- jQuery.ready( true );
- }
- },
-
// Handle when the DOM is ready
ready: function( wait ) {
@@ -3890,7 +4146,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
bulk = key == null;
// Sets many values
- if ( jQuery.type( key ) === "object" ) {
+ if ( toType( key ) === "object" ) {
chainable = true;
for ( i in key ) {
access( elems, fn, i, key[ i ], true, emptyGet, raw );
@@ -3900,7 +4156,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
} else if ( value !== undefined ) {
chainable = true;
- if ( !jQuery.isFunction( value ) ) {
+ if ( !isFunction( value ) ) {
raw = true;
}
@@ -3914,7 +4170,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
// ...except when executing function values
} else {
bulk = fn;
- fn = function( elem, key, value ) {
+ fn = function( elem, _key, value ) {
return bulk.call( jQuery( elem ), value );
};
}
@@ -3931,14 +4187,34 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
}
}
- return chainable ?
- elems :
+ if ( chainable ) {
+ return elems;
+ }
+
+ // Gets
+ if ( bulk ) {
+ return fn.call( elems );
+ }
- // Gets
- bulk ?
- fn.call( elems ) :
- len ? fn( elems[ 0 ], key ) : emptyGet;
+ return len ? fn( elems[ 0 ], key ) : emptyGet;
};
+
+
+// Matches dashed string for camelizing
+var rmsPrefix = /^-ms-/,
+ rdashAlpha = /-([a-z])/g;
+
+// Used by camelCase as callback to replace()
+function fcamelCase( _all, letter ) {
+ return letter.toUpperCase();
+}
+
+// Convert dashed to camelCase; used by the css and data modules
+// Support: IE <=9 - 11, Edge 12 - 15
+// Microsoft forgot to hump their vendor prefix (#9572)
+function camelCase( string ) {
+ return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+}
var acceptData = function( owner ) {
// Accepts only:
@@ -4001,14 +4277,14 @@ Data.prototype = {
// Handle: [ owner, key, value ] args
// Always use camelCase key (gh-2257)
if ( typeof data === "string" ) {
- cache[ jQuery.camelCase( data ) ] = value;
+ cache[ camelCase( data ) ] = value;
// Handle: [ owner, { properties } ] args
} else {
// Copy the properties one-by-one to the cache object
for ( prop in data ) {
- cache[ jQuery.camelCase( prop ) ] = data[ prop ];
+ cache[ camelCase( prop ) ] = data[ prop ];
}
}
return cache;
@@ -4018,7 +4294,7 @@ Data.prototype = {
this.cache( owner ) :
// Always use camelCase key (gh-2257)
- owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
+ owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];
},
access: function( owner, key, value ) {
@@ -4062,19 +4338,19 @@ Data.prototype = {
if ( key !== undefined ) {
// Support array or space separated string of keys
- if ( jQuery.isArray( key ) ) {
+ if ( Array.isArray( key ) ) {
// If key is an array of keys...
// We always set camelCase keys, so remove that.
- key = key.map( jQuery.camelCase );
+ key = key.map( camelCase );
} else {
- key = jQuery.camelCase( key );
+ key = camelCase( key );
// If a key with the spaces exists, use it.
// Otherwise, create an array by matching non-whitespace
key = key in cache ?
[ key ] :
- ( key.match( rnotwhite ) || [] );
+ ( key.match( rnothtmlwhite ) || [] );
}
i = key.length;
@@ -4122,6 +4398,31 @@ var dataUser = new Data();
var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
rmultiDash = /[A-Z]/g;
+function getData( data ) {
+ if ( data === "true" ) {
+ return true;
+ }
+
+ if ( data === "false" ) {
+ return false;
+ }
+
+ if ( data === "null" ) {
+ return null;
+ }
+
+ // Only convert to a number if it doesn't change the string
+ if ( data === +data + "" ) {
+ return +data;
+ }
+
+ if ( rbrace.test( data ) ) {
+ return JSON.parse( data );
+ }
+
+ return data;
+}
+
function dataAttr( elem, key, data ) {
var name;
@@ -4133,14 +4434,7 @@ function dataAttr( elem, key, data ) {
if ( typeof data === "string" ) {
try {
- data = data === "true" ? true :
- data === "false" ? false :
- data === "null" ? null :
-
- // Only convert to a number if it doesn't change the string
- +data + "" === data ? +data :
- rbrace.test( data ) ? JSON.parse( data ) :
- data;
+ data = getData( data );
} catch ( e ) {}
// Make sure we set the data so it isn't changed later
@@ -4196,7 +4490,7 @@ jQuery.fn.extend( {
if ( attrs[ i ] ) {
name = attrs[ i ].name;
if ( name.indexOf( "data-" ) === 0 ) {
- name = jQuery.camelCase( name.slice( 5 ) );
+ name = camelCase( name.slice( 5 ) );
dataAttr( elem, name, data[ name ] );
}
}
@@ -4270,7 +4564,7 @@ jQuery.extend( {
// Speed up dequeue by getting out quickly if this is just a lookup
if ( data ) {
- if ( !queue || jQuery.isArray( data ) ) {
+ if ( !queue || Array.isArray( data ) ) {
queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
} else {
queue.push( data );
@@ -4400,6 +4694,26 @@ var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
+var documentElement = document.documentElement;
+
+
+
+ var isAttached = function( elem ) {
+ return jQuery.contains( elem.ownerDocument, elem );
+ },
+ composed = { composed: true };
+
+ // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only
+ // Check attachment across shadow DOM boundaries when possible (gh-3504)
+ // Support: iOS 10.0-10.2 only
+ // Early iOS 10 versions support `attachShadow` but not `getRootNode`,
+ // leading to errors. We need to check for `getRootNode`.
+ if ( documentElement.getRootNode ) {
+ isAttached = function( elem ) {
+ return jQuery.contains( elem.ownerDocument, elem ) ||
+ elem.getRootNode( composed ) === elem.ownerDocument;
+ };
+ }
var isHiddenWithinTree = function( elem, el ) {
// isHiddenWithinTree might be called from jQuery#filter function;
@@ -4414,37 +4728,15 @@ var isHiddenWithinTree = function( elem, el ) {
// Support: Firefox <=43 - 45
// Disconnected elements can have computed display: none, so first confirm that elem is
// in the document.
- jQuery.contains( elem.ownerDocument, elem ) &&
+ isAttached( elem ) &&
jQuery.css( elem, "display" ) === "none";
};
-var swap = function( elem, options, callback, args ) {
- var ret, name,
- old = {};
-
- // Remember the old values, and insert the new ones
- for ( name in options ) {
- old[ name ] = elem.style[ name ];
- elem.style[ name ] = options[ name ];
- }
-
- ret = callback.apply( elem, args || [] );
-
- // Revert the old values
- for ( name in options ) {
- elem.style[ name ] = old[ name ];
- }
-
- return ret;
-};
-
-
function adjustCSS( elem, prop, valueParts, tween ) {
- var adjusted,
- scale = 1,
+ var adjusted, scale,
maxIterations = 20,
currentValue = tween ?
function() {
@@ -4457,35 +4749,39 @@ function adjustCSS( elem, prop, valueParts, tween ) {
unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
// Starting value computation is required for potential unit mismatches
- initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
+ initialInUnit = elem.nodeType &&
+ ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
rcssNum.exec( jQuery.css( elem, prop ) );
if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
+ // Support: Firefox <=54
+ // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)
+ initial = initial / 2;
+
// Trust units reported by jQuery.css
unit = unit || initialInUnit[ 3 ];
- // Make sure we update the tween properties later on
- valueParts = valueParts || [];
-
// Iteratively approximate from a nonzero starting point
initialInUnit = +initial || 1;
- do {
-
- // If previous iteration zeroed out, double until we get *something*.
- // Use string for doubling so we don't accidentally see scale as unchanged below
- scale = scale || ".5";
+ while ( maxIterations-- ) {
- // Adjust and apply
- initialInUnit = initialInUnit / scale;
+ // Evaluate and update our best guess (doubling guesses that zero out).
+ // Finish if the scale equals or crosses 1 (making the old*new product non-positive).
jQuery.style( elem, prop, initialInUnit + unit );
+ if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {
+ maxIterations = 0;
+ }
+ initialInUnit = initialInUnit / scale;
- // Update scale, tolerating zero or NaN from tween.cur()
- // Break the loop if scale is unchanged or perfect, or if we've just had enough.
- } while (
- scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
- );
+ }
+
+ initialInUnit = initialInUnit * 2;
+ jQuery.style( elem, prop, initialInUnit + unit );
+
+ // Make sure we update the tween properties later on
+ valueParts = valueParts || [];
}
if ( valueParts ) {
@@ -4517,7 +4813,7 @@ function getDefaultDisplay( elem ) {
return display;
}
- temp = doc.body.appendChild( doc.createElement( nodeName ) ),
+ temp = doc.body.appendChild( doc.createElement( nodeName ) );
display = jQuery.css( temp, "display" );
temp.parentNode.removeChild( temp );
@@ -4601,17 +4897,46 @@ jQuery.fn.extend( {
} );
var rcheckableType = ( /^(?:checkbox|radio)$/i );
-var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i );
+var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i );
-var rscriptType = ( /^$|\/(?:java|ecma)script/i );
+var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i );
-// We have to close these tags to support XHTML (#13200)
-var wrapMap = {
+( function() {
+ var fragment = document.createDocumentFragment(),
+ div = fragment.appendChild( document.createElement( "div" ) ),
+ input = document.createElement( "input" );
+
+ // Support: Android 4.0 - 4.3 only
+ // Check state lost if the name is set (#11217)
+ // Support: Windows Web Apps (WWA)
+ // `name` and `type` must use .setAttribute for WWA (#14901)
+ input.setAttribute( "type", "radio" );
+ input.setAttribute( "checked", "checked" );
+ input.setAttribute( "name", "t" );
+
+ div.appendChild( input );
+
+ // Support: Android <=4.1 only
+ // Older WebKit doesn't clone checked state correctly in fragments
+ support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+ // Support: IE <=11 only
+ // Make sure textarea (and checkbox) defaultValue is properly cloned
+ div.innerHTML = "<textarea>x</textarea>";
+ support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
// Support: IE <=9 only
- option: [ 1, "<select multiple='multiple'>", "</select>" ],
+ // IE <=9 replaces <option> tags with their contents when inserted outside of
+ // the select element.
+ div.innerHTML = "<option></option>";
+ support.option = !!div.lastChild;
+} )();
+
+
+// We have to close these tags to support XHTML (#13200)
+var wrapMap = {
// XHTML parsers do not magically insert elements in the
// same way that tag soup parsers do. So we cannot shorten
@@ -4624,26 +4949,36 @@ var wrapMap = {
_default: [ 0, "", "" ]
};
-// Support: IE <=9 only
-wrapMap.optgroup = wrapMap.option;
-
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
+// Support: IE <=9 only
+if ( !support.option ) {
+ wrapMap.optgroup = wrapMap.option = [ 1, "<select multiple='multiple'>", "</select>" ];
+}
+
function getAll( context, tag ) {
// Support: IE <=9 - 11 only
// Use typeof to avoid zero-argument method invocation on host objects (#15151)
- var ret = typeof context.getElementsByTagName !== "undefined" ?
- context.getElementsByTagName( tag || "*" ) :
- typeof context.querySelectorAll !== "undefined" ?
- context.querySelectorAll( tag || "*" ) :
- [];
-
- return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
- jQuery.merge( [ context ], ret ) :
- ret;
+ var ret;
+
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ ret = context.getElementsByTagName( tag || "*" );
+
+ } else if ( typeof context.querySelectorAll !== "undefined" ) {
+ ret = context.querySelectorAll( tag || "*" );
+
+ } else {
+ ret = [];
+ }
+
+ if ( tag === undefined || tag && nodeName( context, tag ) ) {
+ return jQuery.merge( [ context ], ret );
+ }
+
+ return ret;
}
@@ -4665,7 +5000,7 @@ function setGlobalEval( elems, refElements ) {
var rhtml = /<|&#?\w+;/;
function buildFragment( elems, context, scripts, selection, ignored ) {
- var elem, tmp, tag, wrap, contains, j,
+ var elem, tmp, tag, wrap, attached, j,
fragment = context.createDocumentFragment(),
nodes = [],
i = 0,
@@ -4677,7 +5012,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
if ( elem || elem === 0 ) {
// Add nodes directly
- if ( jQuery.type( elem ) === "object" ) {
+ if ( toType( elem ) === "object" ) {
// Support: Android <=4.0 only, PhantomJS 1 only
// push.apply(_, arraylike) throws on ancient WebKit
@@ -4729,13 +5064,13 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
continue;
}
- contains = jQuery.contains( elem.ownerDocument, elem );
+ attached = isAttached( elem );
// Append to fragment
tmp = getAll( fragment.appendChild( elem ), "script" );
// Preserve script evaluation history
- if ( contains ) {
+ if ( attached ) {
setGlobalEval( tmp );
}
@@ -4754,34 +5089,6 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
}
-( function() {
- var fragment = document.createDocumentFragment(),
- div = fragment.appendChild( document.createElement( "div" ) ),
- input = document.createElement( "input" );
-
- // Support: Android 4.0 - 4.3 only
- // Check state lost if the name is set (#11217)
- // Support: Windows Web Apps (WWA)
- // `name` and `type` must use .setAttribute for WWA (#14901)
- input.setAttribute( "type", "radio" );
- input.setAttribute( "checked", "checked" );
- input.setAttribute( "name", "t" );
-
- div.appendChild( input );
-
- // Support: Android <=4.1 only
- // Older WebKit doesn't clone checked state correctly in fragments
- support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
-
- // Support: IE <=11 only
- // Make sure textarea (and checkbox) defaultValue is properly cloned
- div.innerHTML = "<textarea>x</textarea>";
- support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
-} )();
-var documentElement = document.documentElement;
-
-
-
var
rkeyEvent = /^key/,
rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
@@ -4795,8 +5102,19 @@ function returnFalse() {
return false;
}
+// Support: IE <=9 - 11+
+// focus() and blur() are asynchronous, except when they are no-op.
+// So expect focus to be synchronous when the element is already active,
+// and blur to be synchronous when the element is not already active.
+// (focus and blur are always synchronous in other supported browsers,
+// this just defines when we can count on it).
+function expectSync( elem, type ) {
+ return ( elem === safeActiveElement() ) === ( type === "focus" );
+}
+
// Support: IE <=9 only
-// See #13393 for more info
+// Accessing document.activeElement can throw unexpectedly
+// https://bugs.jquery.com/ticket/13393
function safeActiveElement() {
try {
return document.activeElement;
@@ -4879,8 +5197,8 @@ jQuery.event = {
special, handlers, type, namespaces, origType,
elemData = dataPriv.get( elem );
- // Don't attach events to noData or text/comment nodes (but allow plain objects)
- if ( !elemData ) {
+ // Only attach events to objects that accept data
+ if ( !acceptData( elem ) ) {
return;
}
@@ -4904,7 +5222,7 @@ jQuery.event = {
// Init the element's event structure and main handler, if this is the first
if ( !( events = elemData.events ) ) {
- events = elemData.events = {};
+ events = elemData.events = Object.create( null );
}
if ( !( eventHandle = elemData.handle ) ) {
eventHandle = elemData.handle = function( e ) {
@@ -4917,7 +5235,7 @@ jQuery.event = {
}
// Handle multiple events separated by a space
- types = ( types || "" ).match( rnotwhite ) || [ "" ];
+ types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
t = types.length;
while ( t-- ) {
tmp = rtypenamespace.exec( types[ t ] ) || [];
@@ -4999,7 +5317,7 @@ jQuery.event = {
}
// Once for each type.namespace in types; type may be omitted
- types = ( types || "" ).match( rnotwhite ) || [ "" ];
+ types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
t = types.length;
while ( t-- ) {
tmp = rtypenamespace.exec( types[ t ] ) || [];
@@ -5062,12 +5380,15 @@ jQuery.event = {
dispatch: function( nativeEvent ) {
- // Make a writable jQuery.Event from the native event object
- var event = jQuery.event.fix( nativeEvent );
-
var i, j, ret, matched, handleObj, handlerQueue,
args = new Array( arguments.length ),
- handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
+
+ // Make a writable jQuery.Event from the native event object
+ event = jQuery.event.fix( nativeEvent ),
+
+ handlers = (
+ dataPriv.get( this, "events" ) || Object.create( null )
+ )[ event.type ] || [],
special = jQuery.event.special[ event.type ] || {};
// Use the fix-ed jQuery.Event rather than the (read-only) native event
@@ -5096,9 +5417,10 @@ jQuery.event = {
while ( ( handleObj = matched.handlers[ j++ ] ) &&
!event.isImmediatePropagationStopped() ) {
- // Triggered event must either 1) have no namespace, or 2) have namespace(s)
- // a subset or equal to those in the bound event (both can have no namespace).
- if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {
+ // If the event is namespaced, then each handler is only invoked if it is
+ // specially universal or its namespaces are a superset of the event's.
+ if ( !event.rnamespace || handleObj.namespace === false ||
+ event.rnamespace.test( handleObj.namespace ) ) {
event.handleObj = handleObj;
event.data = handleObj.data;
@@ -5125,51 +5447,58 @@ jQuery.event = {
},
handlers: function( event, handlers ) {
- var i, matches, sel, handleObj,
+ var i, handleObj, sel, matchedHandlers, matchedSelectors,
handlerQueue = [],
delegateCount = handlers.delegateCount,
cur = event.target;
- // Support: IE <=9
// Find delegate handlers
- // Black-hole SVG <use> instance trees (#13180)
- //
- // Support: Firefox <=42
- // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343)
- if ( delegateCount && cur.nodeType &&
- ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) {
+ if ( delegateCount &&
+
+ // Support: IE <=9
+ // Black-hole SVG <use> instance trees (trac-13180)
+ cur.nodeType &&
+
+ // Support: Firefox <=42
+ // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)
+ // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click
+ // Support: IE 11 only
+ // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343)
+ !( event.type === "click" && event.button >= 1 ) ) {
for ( ; cur !== this; cur = cur.parentNode || this ) {
// Don't check non-elements (#13208)
// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
- if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) {
- matches = [];
+ if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
+ matchedHandlers = [];
+ matchedSelectors = {};
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
// Don't conflict with Object.prototype properties (#13203)
sel = handleObj.selector + " ";
- if ( matches[ sel ] === undefined ) {
- matches[ sel ] = handleObj.needsContext ?
+ if ( matchedSelectors[ sel ] === undefined ) {
+ matchedSelectors[ sel ] = handleObj.needsContext ?
jQuery( sel, this ).index( cur ) > -1 :
jQuery.find( sel, this, null, [ cur ] ).length;
}
- if ( matches[ sel ] ) {
- matches.push( handleObj );
+ if ( matchedSelectors[ sel ] ) {
+ matchedHandlers.push( handleObj );
}
}
- if ( matches.length ) {
- handlerQueue.push( { elem: cur, handlers: matches } );
+ if ( matchedHandlers.length ) {
+ handlerQueue.push( { elem: cur, handlers: matchedHandlers } );
}
}
}
}
// Add the remaining (directly-bound) handlers
+ cur = this;
if ( delegateCount < handlers.length ) {
- handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } );
+ handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );
}
return handlerQueue;
@@ -5180,7 +5509,7 @@ jQuery.event = {
enumerable: true,
configurable: true,
- get: jQuery.isFunction( hook ) ?
+ get: isFunction( hook ) ?
function() {
if ( this.originalEvent ) {
return hook( this.originalEvent );
@@ -5215,39 +5544,51 @@ jQuery.event = {
// Prevent triggered image.load events from bubbling to window.load
noBubble: true
},
- focus: {
+ click: {
- // Fire native event if possible so blur/focus sequence is correct
- trigger: function() {
- if ( this !== safeActiveElement() && this.focus ) {
- this.focus();
- return false;
- }
- },
- delegateType: "focusin"
- },
- blur: {
- trigger: function() {
- if ( this === safeActiveElement() && this.blur ) {
- this.blur();
- return false;
+ // Utilize native event to ensure correct state for checkable inputs
+ setup: function( data ) {
+
+ // For mutual compressibility with _default, replace `this` access with a local var.
+ // `|| data` is dead code meant only to preserve the variable through minification.
+ var el = this || data;
+
+ // Claim the first handler
+ if ( rcheckableType.test( el.type ) &&
+ el.click && nodeName( el, "input" ) ) {
+
+ // dataPriv.set( el, "click", ... )
+ leverageNative( el, "click", returnTrue );
}
+
+ // Return false to allow normal processing in the caller
+ return false;
},
- delegateType: "focusout"
- },
- click: {
+ trigger: function( data ) {
- // For checkbox, fire native event so checked state will be right
- trigger: function() {
- if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) {
- this.click();
- return false;
+ // For mutual compressibility with _default, replace `this` access with a local var.
+ // `|| data` is dead code meant only to preserve the variable through minification.
+ var el = this || data;
+
+ // Force setup before triggering a click
+ if ( rcheckableType.test( el.type ) &&
+ el.click && nodeName( el, "input" ) ) {
+
+ leverageNative( el, "click" );
}
+
+ // Return non-false to allow normal event-path propagation
+ return true;
},
- // For cross-browser consistency, don't fire native .click() on links
+ // For cross-browser consistency, suppress native .click() on links
+ // Also prevent it if we're currently inside a leveraged native-event stack
_default: function( event ) {
- return jQuery.nodeName( event.target, "a" );
+ var target = event.target;
+ return rcheckableType.test( target.type ) &&
+ target.click && nodeName( target, "input" ) &&
+ dataPriv.get( target, "click" ) ||
+ nodeName( target, "a" );
}
},
@@ -5264,6 +5605,93 @@ jQuery.event = {
}
};
+// Ensure the presence of an event listener that handles manually-triggered
+// synthetic events by interrupting progress until reinvoked in response to
+// *native* events that it fires directly, ensuring that state changes have
+// already occurred before other listeners are invoked.
+function leverageNative( el, type, expectSync ) {
+
+ // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add
+ if ( !expectSync ) {
+ if ( dataPriv.get( el, type ) === undefined ) {
+ jQuery.event.add( el, type, returnTrue );
+ }
+ return;
+ }
+
+ // Register the controller as a special universal handler for all event namespaces
+ dataPriv.set( el, type, false );
+ jQuery.event.add( el, type, {
+ namespace: false,
+ handler: function( event ) {
+ var notAsync, result,
+ saved = dataPriv.get( this, type );
+
+ if ( ( event.isTrigger & 1 ) && this[ type ] ) {
+
+ // Interrupt processing of the outer synthetic .trigger()ed event
+ // Saved data should be false in such cases, but might be a leftover capture object
+ // from an async native handler (gh-4350)
+ if ( !saved.length ) {
+
+ // Store arguments for use when handling the inner native event
+ // There will always be at least one argument (an event object), so this array
+ // will not be confused with a leftover capture object.
+ saved = slice.call( arguments );
+ dataPriv.set( this, type, saved );
+
+ // Trigger the native event and capture its result
+ // Support: IE <=9 - 11+
+ // focus() and blur() are asynchronous
+ notAsync = expectSync( this, type );
+ this[ type ]();
+ result = dataPriv.get( this, type );
+ if ( saved !== result || notAsync ) {
+ dataPriv.set( this, type, false );
+ } else {
+ result = {};
+ }
+ if ( saved !== result ) {
+
+ // Cancel the outer synthetic event
+ event.stopImmediatePropagation();
+ event.preventDefault();
+ return result.value;
+ }
+
+ // If this is an inner synthetic event for an event with a bubbling surrogate
+ // (focus or blur), assume that the surrogate already propagated from triggering the
+ // native event and prevent that from happening again here.
+ // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the
+ // bubbling surrogate propagates *after* the non-bubbling base), but that seems
+ // less bad than duplication.
+ } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {
+ event.stopPropagation();
+ }
+
+ // If this is a native event triggered above, everything is now in order
+ // Fire an inner synthetic event with the original arguments
+ } else if ( saved.length ) {
+
+ // ...and capture the result
+ dataPriv.set( this, type, {
+ value: jQuery.event.trigger(
+
+ // Support: IE <=9 - 11+
+ // Extend with the prototype to reset the above stopImmediatePropagation()
+ jQuery.extend( saved[ 0 ], jQuery.Event.prototype ),
+ saved.slice( 1 ),
+ this
+ )
+ } );
+
+ // Abort handling of the native event
+ event.stopImmediatePropagation();
+ }
+ }
+ } );
+}
+
jQuery.removeEvent = function( elem, type, handle ) {
// This "if" is needed for plain objects
@@ -5315,7 +5743,7 @@ jQuery.Event = function( src, props ) {
}
// Create a timestamp if incoming event doesn't have one
- this.timeStamp = src && src.timeStamp || jQuery.now();
+ this.timeStamp = src && src.timeStamp || Date.now();
// Mark it as fixed
this[ jQuery.expando ] = true;
@@ -5376,6 +5804,7 @@ jQuery.each( {
shiftKey: true,
view: true,
"char": true,
+ code: true,
charCode: true,
key: true,
keyCode: true,
@@ -5403,13 +5832,52 @@ jQuery.each( {
// Add which for click: 1 === left; 2 === middle; 3 === right
if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
- return ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+ if ( button & 1 ) {
+ return 1;
+ }
+
+ if ( button & 2 ) {
+ return 3;
+ }
+
+ if ( button & 4 ) {
+ return 2;
+ }
+
+ return 0;
}
return event.which;
}
}, jQuery.event.addProp );
+jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {
+ jQuery.event.special[ type ] = {
+
+ // Utilize native event if possible so blur/focus sequence is correct
+ setup: function() {
+
+ // Claim the first handler
+ // dataPriv.set( this, "focus", ... )
+ // dataPriv.set( this, "blur", ... )
+ leverageNative( this, type, expectSync );
+
+ // Return false to allow normal processing in the caller
+ return false;
+ },
+ trigger: function() {
+
+ // Force setup before trigger
+ leverageNative( this, type );
+
+ // Return non-false to allow normal event-path propagation
+ return true;
+ },
+
+ delegateType: delegateType
+ };
+} );
+
// Create mouseenter/leave events using mouseover/out and event-time checks
// so that event delegation works in jQuery.
// Do the same for pointerenter/pointerleave and pointerover/pointerout
@@ -5495,28 +5963,21 @@ jQuery.fn.extend( {
var
- /* eslint-disable max-len */
-
- // See https://github.com/eslint/eslint/issues/3229
- rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,
-
- /* eslint-enable */
-
- // Support: IE <=10 - 11, Edge 12 - 13
+ // Support: IE <=10 - 11, Edge 12 - 13 only
// In IE/Edge using regex groups here causes severe slowdowns.
// See https://connect.microsoft.com/IE/feedback/details/1736512/
rnoInnerhtml = /<script|<style|<link/i,
// checked="checked" or checked
rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
- rscriptTypeMasked = /^true\/(.*)/,
rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;
+// Prefer a tbody over its parent table for containing new rows
function manipulationTarget( elem, content ) {
- if ( jQuery.nodeName( elem, "table" ) &&
- jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) {
+ if ( nodeName( elem, "table" ) &&
+ nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) {
- return elem.getElementsByTagName( "tbody" )[ 0 ] || elem;
+ return jQuery( elem ).children( "tbody" )[ 0 ] || elem;
}
return elem;
@@ -5528,10 +5989,8 @@ function disableScript( elem ) {
return elem;
}
function restoreScript( elem ) {
- var match = rscriptTypeMasked.exec( elem.type );
-
- if ( match ) {
- elem.type = match[ 1 ];
+ if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) {
+ elem.type = elem.type.slice( 5 );
} else {
elem.removeAttribute( "type" );
}
@@ -5540,7 +5999,7 @@ function restoreScript( elem ) {
}
function cloneCopyEvent( src, dest ) {
- var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
+ var i, l, type, pdataOld, udataOld, udataCur, events;
if ( dest.nodeType !== 1 ) {
return;
@@ -5548,13 +6007,11 @@ function cloneCopyEvent( src, dest ) {
// 1. Copy private data: events, handlers, etc.
if ( dataPriv.hasData( src ) ) {
- pdataOld = dataPriv.access( src );
- pdataCur = dataPriv.set( dest, pdataOld );
+ pdataOld = dataPriv.get( src );
events = pdataOld.events;
if ( events ) {
- delete pdataCur.handle;
- pdataCur.events = {};
+ dataPriv.remove( dest, "handle events" );
for ( type in events ) {
for ( i = 0, l = events[ type ].length; i < l; i++ ) {
@@ -5590,22 +6047,22 @@ function fixInput( src, dest ) {
function domManip( collection, args, callback, ignored ) {
// Flatten any nested arrays
- args = concat.apply( [], args );
+ args = flat( args );
var fragment, first, scripts, hasScripts, node, doc,
i = 0,
l = collection.length,
iNoClone = l - 1,
value = args[ 0 ],
- isFunction = jQuery.isFunction( value );
+ valueIsFunction = isFunction( value );
// We can't cloneNode fragments that contain checked, in WebKit
- if ( isFunction ||
+ if ( valueIsFunction ||
( l > 1 && typeof value === "string" &&
!support.checkClone && rchecked.test( value ) ) ) {
return collection.each( function( index ) {
var self = collection.eq( index );
- if ( isFunction ) {
+ if ( valueIsFunction ) {
args[ 0 ] = value.call( this, index, self.html() );
}
domManip( self, args, callback, ignored );
@@ -5659,14 +6116,16 @@ function domManip( collection, args, callback, ignored ) {
!dataPriv.access( node, "globalEval" ) &&
jQuery.contains( doc, node ) ) {
- if ( node.src ) {
+ if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) {
// Optional AJAX dependency, but won't run scripts if not present
- if ( jQuery._evalUrl ) {
- jQuery._evalUrl( node.src );
+ if ( jQuery._evalUrl && !node.noModule ) {
+ jQuery._evalUrl( node.src, {
+ nonce: node.nonce || node.getAttribute( "nonce" )
+ }, doc );
}
} else {
- DOMEval( node.textContent.replace( rcleanScript, "" ), doc );
+ DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc );
}
}
}
@@ -5688,7 +6147,7 @@ function remove( elem, selector, keepData ) {
}
if ( node.parentNode ) {
- if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
+ if ( keepData && isAttached( node ) ) {
setGlobalEval( getAll( node, "script" ) );
}
node.parentNode.removeChild( node );
@@ -5700,13 +6159,13 @@ function remove( elem, selector, keepData ) {
jQuery.extend( {
htmlPrefilter: function( html ) {
- return html.replace( rxhtmlTag, "<$1></$2>" );
+ return html;
},
clone: function( elem, dataAndEvents, deepDataAndEvents ) {
var i, l, srcElements, destElements,
clone = elem.cloneNode( true ),
- inPage = jQuery.contains( elem.ownerDocument, elem );
+ inPage = isAttached( elem );
// Fix IE cloning issues
if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
@@ -5946,8 +6405,6 @@ jQuery.each( {
return this.pushStack( ret );
};
} );
-var rmargin = ( /^margin/ );
-
var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
var getStyles = function( elem ) {
@@ -5964,6 +6421,29 @@ var getStyles = function( elem ) {
return view.getComputedStyle( elem );
};
+var swap = function( elem, options, callback ) {
+ var ret, name,
+ old = {};
+
+ // Remember the old values, and insert the new ones
+ for ( name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ ret = callback.call( elem );
+
+ // Revert the old values
+ for ( name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
+
+ return ret;
+};
+
+
+var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
+
( function() {
@@ -5977,25 +6457,35 @@ var getStyles = function( elem ) {
return;
}
+ container.style.cssText = "position:absolute;left:-11111px;width:60px;" +
+ "margin-top:1px;padding:0;border:0";
div.style.cssText =
- "box-sizing:border-box;" +
- "position:relative;display:block;" +
+ "position:relative;display:block;box-sizing:border-box;overflow:scroll;" +
"margin:auto;border:1px;padding:1px;" +
- "top:1%;width:50%";
- div.innerHTML = "";
- documentElement.appendChild( container );
+ "width:60%;top:1%";
+ documentElement.appendChild( container ).appendChild( div );
var divStyle = window.getComputedStyle( div );
pixelPositionVal = divStyle.top !== "1%";
// Support: Android 4.0 - 4.3 only, Firefox <=3 - 44
- reliableMarginLeftVal = divStyle.marginLeft === "2px";
- boxSizingReliableVal = divStyle.width === "4px";
+ reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;
- // Support: Android 4.0 - 4.3 only
+ // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3
// Some styles come back with percentage values, even though they shouldn't
- div.style.marginRight = "50%";
- pixelMarginRightVal = divStyle.marginRight === "4px";
+ div.style.right = "60%";
+ pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;
+
+ // Support: IE 9 - 11 only
+ // Detect misreporting of content dimensions for box-sizing:border-box elements
+ boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;
+
+ // Support: IE 9 only
+ // Detect overflow:scroll screwiness (gh-3699)
+ // Support: Chrome <=64
+ // Don't get tricked when zoom affects offsetWidth (gh-4029)
+ div.style.position = "absolute";
+ scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12;
documentElement.removeChild( container );
@@ -6004,7 +6494,12 @@ var getStyles = function( elem ) {
div = null;
}
- var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal,
+ function roundPixelMeasures( measure ) {
+ return Math.round( parseFloat( measure ) );
+ }
+
+ var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,
+ reliableTrDimensionsVal, reliableMarginLeftVal,
container = document.createElement( "div" ),
div = document.createElement( "div" );
@@ -6019,26 +6514,55 @@ var getStyles = function( elem ) {
div.cloneNode( true ).style.backgroundClip = "";
support.clearCloneStyle = div.style.backgroundClip === "content-box";
- container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" +
- "padding:0;margin-top:1px;position:absolute";
- container.appendChild( div );
-
jQuery.extend( support, {
- pixelPosition: function() {
- computeStyleTests();
- return pixelPositionVal;
- },
boxSizingReliable: function() {
computeStyleTests();
return boxSizingReliableVal;
},
- pixelMarginRight: function() {
+ pixelBoxStyles: function() {
computeStyleTests();
- return pixelMarginRightVal;
+ return pixelBoxStylesVal;
+ },
+ pixelPosition: function() {
+ computeStyleTests();
+ return pixelPositionVal;
},
reliableMarginLeft: function() {
computeStyleTests();
return reliableMarginLeftVal;
+ },
+ scrollboxSize: function() {
+ computeStyleTests();
+ return scrollboxSizeVal;
+ },
+
+ // Support: IE 9 - 11+, Edge 15 - 18+
+ // IE/Edge misreport `getComputedStyle` of table rows with width/height
+ // set in CSS while `offset*` properties report correct values.
+ // Behavior in IE 9 is more subtle than in newer versions & it passes
+ // some versions of this test; make sure not to make it pass there!
+ reliableTrDimensions: function() {
+ var table, tr, trChild, trStyle;
+ if ( reliableTrDimensionsVal == null ) {
+ table = document.createElement( "table" );
+ tr = document.createElement( "tr" );
+ trChild = document.createElement( "div" );
+
+ table.style.cssText = "position:absolute;left:-11111px";
+ tr.style.height = "1px";
+ trChild.style.height = "9px";
+
+ documentElement
+ .appendChild( table )
+ .appendChild( tr )
+ .appendChild( trChild );
+
+ trStyle = window.getComputedStyle( tr );
+ reliableTrDimensionsVal = parseInt( trStyle.height ) > 3;
+
+ documentElement.removeChild( table );
+ }
+ return reliableTrDimensionsVal;
}
} );
} )();
@@ -6046,16 +6570,22 @@ var getStyles = function( elem ) {
function curCSS( elem, name, computed ) {
var width, minWidth, maxWidth, ret,
+
+ // Support: Firefox 51+
+ // Retrieving style before computed somehow
+ // fixes an issue with getting wrong values
+ // on detached elements
style = elem.style;
computed = computed || getStyles( elem );
- // Support: IE <=9 only
- // getPropertyValue is only needed for .css('filter') (#12537)
+ // getPropertyValue is needed for:
+ // .css('filter') (IE 9 only, #12537)
+ // .css('--customProperty) (#3144)
if ( computed ) {
ret = computed.getPropertyValue( name ) || computed[ name ];
- if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+ if ( ret === "" && !isAttached( elem ) ) {
ret = jQuery.style( elem, name );
}
@@ -6064,7 +6594,7 @@ function curCSS( elem, name, computed ) {
// but width seems to be reliably pixels.
// This is against the CSSOM draft spec:
// https://drafts.csswg.org/cssom/#resolved-values
- if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+ if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {
// Remember the original values
width = style.width;
@@ -6111,29 +6641,13 @@ function addGetHookIf( conditionFn, hookFn ) {
}
-var
-
- // Swappable if display is none or starts with table
- // except "table", "table-cell", or "table-caption"
- // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
- rdisplayswap = /^(none|table(?!-c[ea]).+)/,
- cssShow = { position: "absolute", visibility: "hidden", display: "block" },
- cssNormalTransform = {
- letterSpacing: "0",
- fontWeight: "400"
- },
-
- cssPrefixes = [ "Webkit", "Moz", "ms" ],
- emptyStyle = document.createElement( "div" ).style;
+var cssPrefixes = [ "Webkit", "Moz", "ms" ],
+ emptyStyle = document.createElement( "div" ).style,
+ vendorProps = {};
-// Return a css property mapped to a potentially vendor prefixed property
+// Return a vendor-prefixed property or undefined
function vendorPropName( name ) {
- // Shortcut for names that are not vendor prefixed
- if ( name in emptyStyle ) {
- return name;
- }
-
// Check for vendor prefixed names
var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
i = cssPrefixes.length;
@@ -6146,7 +6660,34 @@ function vendorPropName( name ) {
}
}
-function setPositiveNumber( elem, value, subtract ) {
+// Return a potentially-mapped jQuery.cssProps or vendor prefixed property
+function finalPropName( name ) {
+ var final = jQuery.cssProps[ name ] || vendorProps[ name ];
+
+ if ( final ) {
+ return final;
+ }
+ if ( name in emptyStyle ) {
+ return name;
+ }
+ return vendorProps[ name ] = vendorPropName( name ) || name;
+}
+
+
+var
+
+ // Swappable if display is none or starts with table
+ // except "table", "table-cell", or "table-caption"
+ // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+ rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+ rcustomProp = /^--/,
+ cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+ cssNormalTransform = {
+ letterSpacing: "0",
+ fontWeight: "400"
+ };
+
+function setPositiveNumber( _elem, value, subtract ) {
// Any relative (+/-) values have already been
// normalized at this point
@@ -6158,98 +6699,146 @@ function setPositiveNumber( elem, value, subtract ) {
value;
}
-function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
- var i = extra === ( isBorderBox ? "border" : "content" ) ?
-
- // If we already have the right measurement, avoid augmentation
- 4 :
-
- // Otherwise initialize for horizontal or vertical properties
- name === "width" ? 1 : 0,
+function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {
+ var i = dimension === "width" ? 1 : 0,
+ extra = 0,
+ delta = 0;
- val = 0;
+ // Adjustment may not be necessary
+ if ( box === ( isBorderBox ? "border" : "content" ) ) {
+ return 0;
+ }
for ( ; i < 4; i += 2 ) {
- // Both box models exclude margin, so add it if we want it
- if ( extra === "margin" ) {
- val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
+ // Both box models exclude margin
+ if ( box === "margin" ) {
+ delta += jQuery.css( elem, box + cssExpand[ i ], true, styles );
}
- if ( isBorderBox ) {
+ // If we get here with a content-box, we're seeking "padding" or "border" or "margin"
+ if ( !isBorderBox ) {
- // border-box includes padding, so remove it if we want content
- if ( extra === "content" ) {
- val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
- }
+ // Add padding
+ delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
- // At this point, extra isn't border nor margin, so remove border
- if ( extra !== "margin" ) {
- val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ // For "border" or "margin", add border
+ if ( box !== "padding" ) {
+ delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+
+ // But still keep track of it otherwise
+ } else {
+ extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
+
+ // If we get here with a border-box (content + padding + border), we're seeking "content" or
+ // "padding" or "margin"
} else {
- // At this point, extra isn't content, so add padding
- val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+ // For "content", subtract padding
+ if ( box === "content" ) {
+ delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+ }
- // At this point, extra isn't content nor padding, so add border
- if ( extra !== "padding" ) {
- val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ // For "content" or "padding", subtract border
+ if ( box !== "margin" ) {
+ delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
}
}
- return val;
+ // Account for positive content-box scroll gutter when requested by providing computedVal
+ if ( !isBorderBox && computedVal >= 0 ) {
+
+ // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border
+ // Assuming integer scroll gutter, subtract the rest and round down
+ delta += Math.max( 0, Math.ceil(
+ elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+ computedVal -
+ delta -
+ extra -
+ 0.5
+
+ // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter
+ // Use an explicit zero to avoid NaN (gh-3964)
+ ) ) || 0;
+ }
+
+ return delta;
}
-function getWidthOrHeight( elem, name, extra ) {
+function getWidthOrHeight( elem, dimension, extra ) {
- // Start with offset property, which is equivalent to the border-box value
- var val,
- valueIsBorderBox = true,
- styles = getStyles( elem ),
- isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+ // Start with computed style
+ var styles = getStyles( elem ),
- // Support: IE <=11 only
- // Running getBoundingClientRect on a disconnected node
- // in IE throws an error.
- if ( elem.getClientRects().length ) {
- val = elem.getBoundingClientRect()[ name ];
- }
-
- // Some non-html elements return undefined for offsetWidth, so check for null/undefined
- // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
- // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
- if ( val <= 0 || val == null ) {
+ // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).
+ // Fake content-box until we know it's needed to know the true value.
+ boxSizingNeeded = !support.boxSizingReliable() || extra,
+ isBorderBox = boxSizingNeeded &&
+ jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+ valueIsBorderBox = isBorderBox,
- // Fall back to computed then uncomputed css if necessary
- val = curCSS( elem, name, styles );
- if ( val < 0 || val == null ) {
- val = elem.style[ name ];
- }
+ val = curCSS( elem, dimension, styles ),
+ offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );
- // Computed unit is not pixels. Stop here and return.
- if ( rnumnonpx.test( val ) ) {
+ // Support: Firefox <=54
+ // Return a confounding non-pixel value or feign ignorance, as appropriate.
+ if ( rnumnonpx.test( val ) ) {
+ if ( !extra ) {
return val;
}
+ val = "auto";
+ }
+
+
+ // Support: IE 9 - 11 only
+ // Use offsetWidth/offsetHeight for when box sizing is unreliable.
+ // In those cases, the computed value can be trusted to be border-box.
+ if ( ( !support.boxSizingReliable() && isBorderBox ||
- // Check for style in case a browser which returns unreliable values
- // for getComputedStyle silently falls back to the reliable elem.style
- valueIsBorderBox = isBorderBox &&
- ( support.boxSizingReliable() || val === elem.style[ name ] );
+ // Support: IE 10 - 11+, Edge 15 - 18+
+ // IE/Edge misreport `getComputedStyle` of table rows with width/height
+ // set in CSS while `offset*` properties report correct values.
+ // Interestingly, in some cases IE 9 doesn't suffer from this issue.
+ !support.reliableTrDimensions() && nodeName( elem, "tr" ) ||
- // Normalize "", auto, and prepare for extra
- val = parseFloat( val ) || 0;
+ // Fall back to offsetWidth/offsetHeight when value is "auto"
+ // This happens for inline elements with no explicit setting (gh-3571)
+ val === "auto" ||
+
+ // Support: Android <=4.1 - 4.3 only
+ // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)
+ !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) &&
+
+ // Make sure the element is visible & connected
+ elem.getClientRects().length ) {
+
+ isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+ // Where available, offsetWidth/offsetHeight approximate border box dimensions.
+ // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the
+ // retrieved value as a content box dimension.
+ valueIsBorderBox = offsetProp in elem;
+ if ( valueIsBorderBox ) {
+ val = elem[ offsetProp ];
+ }
}
- // Use the active box-sizing model to add/subtract irrelevant styles
+ // Normalize "" and auto
+ val = parseFloat( val ) || 0;
+
+ // Adjust for the element's box model
return ( val +
- augmentWidthOrHeight(
+ boxModelAdjustment(
elem,
- name,
+ dimension,
extra || ( isBorderBox ? "border" : "content" ),
valueIsBorderBox,
- styles
+ styles,
+
+ // Provide the current computed size to request scroll gutter calculation (gh-3589)
+ val
)
) + "px";
}
@@ -6279,6 +6868,13 @@ jQuery.extend( {
"flexGrow": true,
"flexShrink": true,
"fontWeight": true,
+ "gridArea": true,
+ "gridColumn": true,
+ "gridColumnEnd": true,
+ "gridColumnStart": true,
+ "gridRow": true,
+ "gridRowEnd": true,
+ "gridRowStart": true,
"lineHeight": true,
"opacity": true,
"order": true,
@@ -6290,9 +6886,7 @@ jQuery.extend( {
// Add in properties whose names you wish to fix before
// setting or getting the value
- cssProps: {
- "float": "cssFloat"
- },
+ cssProps: {},
// Get and set the style property on a DOM Node
style: function( elem, name, value, extra ) {
@@ -6304,11 +6898,16 @@ jQuery.extend( {
// Make sure that we're working with the right name
var ret, type, hooks,
- origName = jQuery.camelCase( name ),
+ origName = camelCase( name ),
+ isCustomProp = rcustomProp.test( name ),
style = elem.style;
- name = jQuery.cssProps[ origName ] ||
- ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
+ // Make sure that we're working with the right name. We don't
+ // want to query the value if it is a CSS custom property
+ // since they are user-defined.
+ if ( !isCustomProp ) {
+ name = finalPropName( origName );
+ }
// Gets hook for the prefixed version, then unprefixed version
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
@@ -6331,7 +6930,9 @@ jQuery.extend( {
}
// If a number was passed in, add the unit (except for certain CSS properties)
- if ( type === "number" ) {
+ // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append
+ // "px" to a few hardcoded values.
+ if ( type === "number" && !isCustomProp ) {
value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
}
@@ -6344,7 +6945,11 @@ jQuery.extend( {
if ( !hooks || !( "set" in hooks ) ||
( value = hooks.set( elem, value, extra ) ) !== undefined ) {
- style[ name ] = value;
+ if ( isCustomProp ) {
+ style.setProperty( name, value );
+ } else {
+ style[ name ] = value;
+ }
}
} else {
@@ -6363,11 +6968,15 @@ jQuery.extend( {
css: function( elem, name, extra, styles ) {
var val, num, hooks,
- origName = jQuery.camelCase( name );
+ origName = camelCase( name ),
+ isCustomProp = rcustomProp.test( name );
- // Make sure that we're working with the right name
- name = jQuery.cssProps[ origName ] ||
- ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
+ // Make sure that we're working with the right name. We don't
+ // want to modify the value if it is a CSS custom property
+ // since they are user-defined.
+ if ( !isCustomProp ) {
+ name = finalPropName( origName );
+ }
// Try prefixed name followed by the unprefixed name
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
@@ -6392,12 +7001,13 @@ jQuery.extend( {
num = parseFloat( val );
return extra === true || isFinite( num ) ? num || 0 : val;
}
+
return val;
}
} );
-jQuery.each( [ "height", "width" ], function( i, name ) {
- jQuery.cssHooks[ name ] = {
+jQuery.each( [ "height", "width" ], function( _i, dimension ) {
+ jQuery.cssHooks[ dimension ] = {
get: function( elem, computed, extra ) {
if ( computed ) {
@@ -6413,29 +7023,52 @@ jQuery.each( [ "height", "width" ], function( i, name ) {
// in IE throws an error.
( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
swap( elem, cssShow, function() {
- return getWidthOrHeight( elem, name, extra );
+ return getWidthOrHeight( elem, dimension, extra );
} ) :
- getWidthOrHeight( elem, name, extra );
+ getWidthOrHeight( elem, dimension, extra );
}
},
set: function( elem, value, extra ) {
var matches,
- styles = extra && getStyles( elem ),
- subtract = extra && augmentWidthOrHeight(
- elem,
- name,
- extra,
+ styles = getStyles( elem ),
+
+ // Only read styles.position if the test has a chance to fail
+ // to avoid forcing a reflow.
+ scrollboxSizeBuggy = !support.scrollboxSize() &&
+ styles.position === "absolute",
+
+ // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)
+ boxSizingNeeded = scrollboxSizeBuggy || extra,
+ isBorderBox = boxSizingNeeded &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
- styles
+ subtract = extra ?
+ boxModelAdjustment(
+ elem,
+ dimension,
+ extra,
+ isBorderBox,
+ styles
+ ) :
+ 0;
+
+ // Account for unreliable border-box dimensions by comparing offset* to computed and
+ // faking a content-box to get border and padding (gh-3699)
+ if ( isBorderBox && scrollboxSizeBuggy ) {
+ subtract -= Math.ceil(
+ elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+ parseFloat( styles[ dimension ] ) -
+ boxModelAdjustment( elem, dimension, "border", false, styles ) -
+ 0.5
);
+ }
// Convert to pixels if value adjustment is needed
if ( subtract && ( matches = rcssNum.exec( value ) ) &&
( matches[ 3 ] || "px" ) !== "px" ) {
- elem.style[ name ] = value;
- value = jQuery.css( elem, name );
+ elem.style[ dimension ] = value;
+ value = jQuery.css( elem, dimension );
}
return setPositiveNumber( elem, value, subtract );
@@ -6479,7 +7112,7 @@ jQuery.each( {
}
};
- if ( !rmargin.test( prefix ) ) {
+ if ( prefix !== "margin" ) {
jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
}
} );
@@ -6491,7 +7124,7 @@ jQuery.fn.extend( {
map = {},
i = 0;
- if ( jQuery.isArray( name ) ) {
+ if ( Array.isArray( name ) ) {
styles = getStyles( elem );
len = name.length;
@@ -6589,9 +7222,9 @@ Tween.propHooks = {
// Use .style if available and use plain properties where available.
if ( jQuery.fx.step[ tween.prop ] ) {
jQuery.fx.step[ tween.prop ]( tween );
- } else if ( tween.elem.nodeType === 1 &&
- ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||
- jQuery.cssHooks[ tween.prop ] ) ) {
+ } else if ( tween.elem.nodeType === 1 && (
+ jQuery.cssHooks[ tween.prop ] ||
+ tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {
jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
} else {
tween.elem[ tween.prop ] = tween.now;
@@ -6629,13 +7262,18 @@ jQuery.fx.step = {};
var
- fxNow, timerId,
+ fxNow, inProgress,
rfxtypes = /^(?:toggle|show|hide)$/,
rrun = /queueHooks$/;
-function raf() {
- if ( timerId ) {
- window.requestAnimationFrame( raf );
+function schedule() {
+ if ( inProgress ) {
+ if ( document.hidden === false && window.requestAnimationFrame ) {
+ window.requestAnimationFrame( schedule );
+ } else {
+ window.setTimeout( schedule, jQuery.fx.interval );
+ }
+
jQuery.fx.tick();
}
}
@@ -6645,7 +7283,7 @@ function createFxNow() {
window.setTimeout( function() {
fxNow = undefined;
} );
- return ( fxNow = jQuery.now() );
+ return ( fxNow = Date.now() );
}
// Generate parameters to create a standard animation
@@ -6749,9 +7387,10 @@ function defaultPrefilter( elem, props, opts ) {
// Restrict "overflow" and "display" styles during box animations
if ( isBox && elem.nodeType === 1 ) {
- // Support: IE <=9 - 11, Edge 12 - 13
+ // Support: IE <=9 - 11, Edge 12 - 15
// Record all 3 overflow attributes because IE does not infer the shorthand
- // from identically-valued overflowX and overflowY
+ // from identically-valued overflowX and overflowY and Edge just mirrors
+ // the overflowX value there.
opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
// Identify a display type, preferring old show/hide data over the CSS cascade
@@ -6859,10 +7498,10 @@ function propFilter( props, specialEasing ) {
// camelCase, specialEasing and expand cssHook pass
for ( index in props ) {
- name = jQuery.camelCase( index );
+ name = camelCase( index );
easing = specialEasing[ name ];
value = props[ index ];
- if ( jQuery.isArray( value ) ) {
+ if ( Array.isArray( value ) ) {
easing = value[ 1 ];
value = props[ index ] = value[ 0 ];
}
@@ -6921,12 +7560,19 @@ function Animation( elem, properties, options ) {
deferred.notifyWith( elem, [ animation, percent, remaining ] );
+ // If there's more to do, yield
if ( percent < 1 && length ) {
return remaining;
- } else {
- deferred.resolveWith( elem, [ animation ] );
- return false;
}
+
+ // If this was an empty animation, synthesize a final progress notification
+ if ( !length ) {
+ deferred.notifyWith( elem, [ animation, 1, 0 ] );
+ }
+
+ // Resolve the animation and report its conclusion
+ deferred.resolveWith( elem, [ animation ] );
+ return false;
},
animation = deferred.promise( {
elem: elem,
@@ -6977,9 +7623,9 @@ function Animation( elem, properties, options ) {
for ( ; index < length; index++ ) {
result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
if ( result ) {
- if ( jQuery.isFunction( result.stop ) ) {
+ if ( isFunction( result.stop ) ) {
jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
- jQuery.proxy( result.stop, result );
+ result.stop.bind( result );
}
return result;
}
@@ -6987,10 +7633,17 @@ function Animation( elem, properties, options ) {
jQuery.map( props, createTween, animation );
- if ( jQuery.isFunction( animation.opts.start ) ) {
+ if ( isFunction( animation.opts.start ) ) {
animation.opts.start.call( elem, animation );
}
+ // Attach callbacks from options
+ animation
+ .progress( animation.opts.progress )
+ .done( animation.opts.done, animation.opts.complete )
+ .fail( animation.opts.fail )
+ .always( animation.opts.always );
+
jQuery.fx.timer(
jQuery.extend( tick, {
elem: elem,
@@ -6999,11 +7652,7 @@ function Animation( elem, properties, options ) {
} )
);
- // attach callbacks from options
- return animation.progress( animation.opts.progress )
- .done( animation.opts.done, animation.opts.complete )
- .fail( animation.opts.fail )
- .always( animation.opts.always );
+ return animation;
}
jQuery.Animation = jQuery.extend( Animation, {
@@ -7017,11 +7666,11 @@ jQuery.Animation = jQuery.extend( Animation, {
},
tweener: function( props, callback ) {
- if ( jQuery.isFunction( props ) ) {
+ if ( isFunction( props ) ) {
callback = props;
props = [ "*" ];
} else {
- props = props.match( rnotwhite );
+ props = props.match( rnothtmlwhite );
}
var prop,
@@ -7049,19 +7698,24 @@ jQuery.Animation = jQuery.extend( Animation, {
jQuery.speed = function( speed, easing, fn ) {
var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
complete: fn || !fn && easing ||
- jQuery.isFunction( speed ) && speed,
+ isFunction( speed ) && speed,
duration: speed,
- easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+ easing: fn && easing || easing && !isFunction( easing ) && easing
};
- // Go to the end state if fx are off or if document is hidden
- if ( jQuery.fx.off || document.hidden ) {
+ // Go to the end state if fx are off
+ if ( jQuery.fx.off ) {
opt.duration = 0;
} else {
- opt.duration = typeof opt.duration === "number" ?
- opt.duration : opt.duration in jQuery.fx.speeds ?
- jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+ if ( typeof opt.duration !== "number" ) {
+ if ( opt.duration in jQuery.fx.speeds ) {
+ opt.duration = jQuery.fx.speeds[ opt.duration ];
+
+ } else {
+ opt.duration = jQuery.fx.speeds._default;
+ }
+ }
}
// Normalize opt.queue - true/undefined/null -> "fx"
@@ -7073,7 +7727,7 @@ jQuery.speed = function( speed, easing, fn ) {
opt.old = opt.complete;
opt.complete = function() {
- if ( jQuery.isFunction( opt.old ) ) {
+ if ( isFunction( opt.old ) ) {
opt.old.call( this );
}
@@ -7125,7 +7779,7 @@ jQuery.fn.extend( {
clearQueue = type;
type = undefined;
}
- if ( clearQueue && type !== false ) {
+ if ( clearQueue ) {
this.queue( type || "fx", [] );
}
@@ -7208,7 +7862,7 @@ jQuery.fn.extend( {
}
} );
-jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
+jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) {
var cssFn = jQuery.fn[ name ];
jQuery.fn[ name ] = function( speed, easing, callback ) {
return speed == null || typeof speed === "boolean" ?
@@ -7237,12 +7891,12 @@ jQuery.fx.tick = function() {
i = 0,
timers = jQuery.timers;
- fxNow = jQuery.now();
+ fxNow = Date.now();
for ( ; i < timers.length; i++ ) {
timer = timers[ i ];
- // Checks the timer has not already been removed
+ // Run the timer and safely remove it when done (allowing for external removal)
if ( !timer() && timers[ i ] === timer ) {
timers.splice( i--, 1 );
}
@@ -7256,30 +7910,21 @@ jQuery.fx.tick = function() {
jQuery.fx.timer = function( timer ) {
jQuery.timers.push( timer );
- if ( timer() ) {
- jQuery.fx.start();
- } else {
- jQuery.timers.pop();
- }
+ jQuery.fx.start();
};
jQuery.fx.interval = 13;
jQuery.fx.start = function() {
- if ( !timerId ) {
- timerId = window.requestAnimationFrame ?
- window.requestAnimationFrame( raf ) :
- window.setInterval( jQuery.fx.tick, jQuery.fx.interval );
+ if ( inProgress ) {
+ return;
}
+
+ inProgress = true;
+ schedule();
};
jQuery.fx.stop = function() {
- if ( window.cancelAnimationFrame ) {
- window.cancelAnimationFrame( timerId );
- } else {
- window.clearInterval( timerId );
- }
-
- timerId = null;
+ inProgress = null;
};
jQuery.fx.speeds = {
@@ -7396,7 +8041,7 @@ jQuery.extend( {
type: {
set: function( elem, value ) {
if ( !support.radioValue && value === "radio" &&
- jQuery.nodeName( elem, "input" ) ) {
+ nodeName( elem, "input" ) ) {
var val = elem.value;
elem.setAttribute( "type", value );
if ( val ) {
@@ -7411,7 +8056,10 @@ jQuery.extend( {
removeAttr: function( elem, value ) {
var name,
i = 0,
- attrNames = value && value.match( rnotwhite );
+
+ // Attribute names can contain non-HTML whitespace characters
+ // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
+ attrNames = value && value.match( rnothtmlwhite );
if ( attrNames && elem.nodeType === 1 ) {
while ( ( name = attrNames[ i++ ] ) ) {
@@ -7435,7 +8083,7 @@ boolHook = {
}
};
-jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) {
var getter = attrHandle[ name ] || jQuery.find.attr;
attrHandle[ name ] = function( elem, name, isXML ) {
@@ -7518,12 +8166,19 @@ jQuery.extend( {
// Use proper attribute retrieval(#12072)
var tabindex = jQuery.find.attr( elem, "tabindex" );
- return tabindex ?
- parseInt( tabindex, 10 ) :
+ if ( tabindex ) {
+ return parseInt( tabindex, 10 );
+ }
+
+ if (
rfocusable.test( elem.nodeName ) ||
- rclickable.test( elem.nodeName ) && elem.href ?
- 0 :
- -1;
+ rclickable.test( elem.nodeName ) &&
+ elem.href
+ ) {
+ return 0;
+ }
+
+ return -1;
}
}
},
@@ -7540,9 +8195,14 @@ jQuery.extend( {
// on the option
// The getter ensures a default option is selected
// when in an optgroup
+// eslint rule "no-unused-expressions" is disabled for this code
+// since it considers such accessions noop
if ( !support.optSelected ) {
jQuery.propHooks.selected = {
get: function( elem ) {
+
+ /* eslint no-unused-expressions: "off" */
+
var parent = elem.parentNode;
if ( parent && parent.parentNode ) {
parent.parentNode.selectedIndex;
@@ -7550,6 +8210,9 @@ if ( !support.optSelected ) {
return null;
},
set: function( elem ) {
+
+ /* eslint no-unused-expressions: "off" */
+
var parent = elem.parentNode;
if ( parent ) {
parent.selectedIndex;
@@ -7580,30 +8243,45 @@ jQuery.each( [
-var rclass = /[\t\r\n\f]/g;
+ // Strip and collapse whitespace according to HTML spec
+ // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace
+ function stripAndCollapse( value ) {
+ var tokens = value.match( rnothtmlwhite ) || [];
+ return tokens.join( " " );
+ }
+
function getClass( elem ) {
return elem.getAttribute && elem.getAttribute( "class" ) || "";
}
+function classesToArray( value ) {
+ if ( Array.isArray( value ) ) {
+ return value;
+ }
+ if ( typeof value === "string" ) {
+ return value.match( rnothtmlwhite ) || [];
+ }
+ return [];
+}
+
jQuery.fn.extend( {
addClass: function( value ) {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
- if ( jQuery.isFunction( value ) ) {
+ if ( isFunction( value ) ) {
return this.each( function( j ) {
jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
} );
}
- if ( typeof value === "string" && value ) {
- classes = value.match( rnotwhite ) || [];
+ classes = classesToArray( value );
+ if ( classes.length ) {
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
- cur = elem.nodeType === 1 &&
- ( " " + curValue + " " ).replace( rclass, " " );
+ cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
if ( cur ) {
j = 0;
@@ -7614,7 +8292,7 @@ jQuery.fn.extend( {
}
// Only assign if different to avoid unneeded rendering.
- finalValue = jQuery.trim( cur );
+ finalValue = stripAndCollapse( cur );
if ( curValue !== finalValue ) {
elem.setAttribute( "class", finalValue );
}
@@ -7629,7 +8307,7 @@ jQuery.fn.extend( {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
- if ( jQuery.isFunction( value ) ) {
+ if ( isFunction( value ) ) {
return this.each( function( j ) {
jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
} );
@@ -7639,15 +8317,14 @@ jQuery.fn.extend( {
return this.attr( "class", "" );
}
- if ( typeof value === "string" && value ) {
- classes = value.match( rnotwhite ) || [];
+ classes = classesToArray( value );
+ if ( classes.length ) {
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
// This expression is here for better compressibility (see addClass)
- cur = elem.nodeType === 1 &&
- ( " " + curValue + " " ).replace( rclass, " " );
+ cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
if ( cur ) {
j = 0;
@@ -7660,7 +8337,7 @@ jQuery.fn.extend( {
}
// Only assign if different to avoid unneeded rendering.
- finalValue = jQuery.trim( cur );
+ finalValue = stripAndCollapse( cur );
if ( curValue !== finalValue ) {
elem.setAttribute( "class", finalValue );
}
@@ -7672,13 +8349,14 @@ jQuery.fn.extend( {
},
toggleClass: function( value, stateVal ) {
- var type = typeof value;
+ var type = typeof value,
+ isValidValue = type === "string" || Array.isArray( value );
- if ( typeof stateVal === "boolean" && type === "string" ) {
+ if ( typeof stateVal === "boolean" && isValidValue ) {
return stateVal ? this.addClass( value ) : this.removeClass( value );
}
- if ( jQuery.isFunction( value ) ) {
+ if ( isFunction( value ) ) {
return this.each( function( i ) {
jQuery( this ).toggleClass(
value.call( this, i, getClass( this ), stateVal ),
@@ -7690,12 +8368,12 @@ jQuery.fn.extend( {
return this.each( function() {
var className, i, self, classNames;
- if ( type === "string" ) {
+ if ( isValidValue ) {
// Toggle individual class names
i = 0;
self = jQuery( this );
- classNames = value.match( rnotwhite ) || [];
+ classNames = classesToArray( value );
while ( ( className = classNames[ i++ ] ) ) {
@@ -7738,10 +8416,8 @@ jQuery.fn.extend( {
className = " " + selector + " ";
while ( ( elem = this[ i++ ] ) ) {
if ( elem.nodeType === 1 &&
- ( " " + getClass( elem ) + " " ).replace( rclass, " " )
- .indexOf( className ) > -1
- ) {
- return true;
+ ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
+ return true;
}
}
@@ -7752,12 +8428,11 @@ jQuery.fn.extend( {
-var rreturn = /\r/g,
- rspaces = /[\x20\t\r\n\f]+/g;
+var rreturn = /\r/g;
jQuery.fn.extend( {
val: function( value ) {
- var hooks, ret, isFunction,
+ var hooks, ret, valueIsFunction,
elem = this[ 0 ];
if ( !arguments.length ) {
@@ -7774,19 +8449,19 @@ jQuery.fn.extend( {
ret = elem.value;
- return typeof ret === "string" ?
-
- // Handle most common string cases
- ret.replace( rreturn, "" ) :
+ // Handle most common string cases
+ if ( typeof ret === "string" ) {
+ return ret.replace( rreturn, "" );
+ }
- // Handle cases where value is null/undef or number
- ret == null ? "" : ret;
+ // Handle cases where value is null/undef or number
+ return ret == null ? "" : ret;
}
return;
}
- isFunction = jQuery.isFunction( value );
+ valueIsFunction = isFunction( value );
return this.each( function( i ) {
var val;
@@ -7795,7 +8470,7 @@ jQuery.fn.extend( {
return;
}
- if ( isFunction ) {
+ if ( valueIsFunction ) {
val = value.call( this, i, jQuery( this ).val() );
} else {
val = value;
@@ -7808,7 +8483,7 @@ jQuery.fn.extend( {
} else if ( typeof val === "number" ) {
val += "";
- } else if ( jQuery.isArray( val ) ) {
+ } else if ( Array.isArray( val ) ) {
val = jQuery.map( val, function( value ) {
return value == null ? "" : value + "";
} );
@@ -7837,20 +8512,24 @@ jQuery.extend( {
// option.text throws exceptions (#14686, #14858)
// Strip and collapse whitespace
// https://html.spec.whatwg.org/#strip-and-collapse-whitespace
- jQuery.trim( jQuery.text( elem ) ).replace( rspaces, " " );
+ stripAndCollapse( jQuery.text( elem ) );
}
},
select: {
get: function( elem ) {
- var value, option,
+ var value, option, i,
options = elem.options,
index = elem.selectedIndex,
one = elem.type === "select-one",
values = one ? null : [],
- max = one ? index + 1 : options.length,
- i = index < 0 ?
- max :
- one ? index : 0;
+ max = one ? index + 1 : options.length;
+
+ if ( index < 0 ) {
+ i = max;
+
+ } else {
+ i = one ? index : 0;
+ }
// Loop through all the selected options
for ( ; i < max; i++ ) {
@@ -7863,7 +8542,7 @@ jQuery.extend( {
// Don't return options that are disabled or in a disabled optgroup
!option.disabled &&
( !option.parentNode.disabled ||
- !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
+ !nodeName( option.parentNode, "optgroup" ) ) ) {
// Get the specific value for the option
value = jQuery( option ).val();
@@ -7915,7 +8594,7 @@ jQuery.extend( {
jQuery.each( [ "radio", "checkbox" ], function() {
jQuery.valHooks[ this ] = {
set: function( elem, value ) {
- if ( jQuery.isArray( value ) ) {
+ if ( Array.isArray( value ) ) {
return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
}
}
@@ -7933,18 +8612,24 @@ jQuery.each( [ "radio", "checkbox" ], function() {
// Return jQuery for attributes-only inclusion
-var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/;
+support.focusin = "onfocusin" in window;
+
+
+var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+ stopPropagationCallback = function( e ) {
+ e.stopPropagation();
+ };
jQuery.extend( jQuery.event, {
trigger: function( event, data, elem, onlyHandlers ) {
- var i, cur, tmp, bubbleType, ontype, handle, special,
+ var i, cur, tmp, bubbleType, ontype, handle, special, lastElement,
eventPath = [ elem || document ],
type = hasOwn.call( event, "type" ) ? event.type : event,
namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];
- cur = tmp = elem = elem || document;
+ cur = lastElement = tmp = elem = elem || document;
// Don't do events on text and comment nodes
if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
@@ -7996,7 +8681,7 @@ jQuery.extend( jQuery.event, {
// Determine event propagation path in advance, per W3C events spec (#9951)
// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
- if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+ if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {
bubbleType = special.delegateType || type;
if ( !rfocusMorph.test( bubbleType + type ) ) {
@@ -8016,13 +8701,15 @@ jQuery.extend( jQuery.event, {
// Fire handlers on the event path
i = 0;
while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
-
+ lastElement = cur;
event.type = i > 1 ?
bubbleType :
special.bindType || type;
// jQuery handler
- handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
+ handle = (
+ dataPriv.get( cur, "events" ) || Object.create( null )
+ )[ event.type ] &&
dataPriv.get( cur, "handle" );
if ( handle ) {
handle.apply( cur, data );
@@ -8048,7 +8735,7 @@ jQuery.extend( jQuery.event, {
// Call a native DOM method on the target with the same name as the event.
// Don't do default actions on window, that's where global variables be (#6170)
- if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {
+ if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {
// Don't re-trigger an onFOO event when we call its FOO() method
tmp = elem[ ontype ];
@@ -8059,7 +8746,17 @@ jQuery.extend( jQuery.event, {
// Prevent re-triggering of the same event, since we already bubbled it above
jQuery.event.triggered = type;
+
+ if ( event.isPropagationStopped() ) {
+ lastElement.addEventListener( type, stopPropagationCallback );
+ }
+
elem[ type ]();
+
+ if ( event.isPropagationStopped() ) {
+ lastElement.removeEventListener( type, stopPropagationCallback );
+ }
+
jQuery.event.triggered = undefined;
if ( tmp ) {
@@ -8105,31 +8802,6 @@ jQuery.fn.extend( {
} );
-jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
- "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
- "change select submit keydown keypress keyup contextmenu" ).split( " " ),
- function( i, name ) {
-
- // Handle event binding
- jQuery.fn[ name ] = function( data, fn ) {
- return arguments.length > 0 ?
- this.on( name, null, data, fn ) :
- this.trigger( name );
- };
-} );
-
-jQuery.fn.extend( {
- hover: function( fnOver, fnOut ) {
- return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
- }
-} );
-
-
-
-
-support.focusin = "onfocusin" in window;
-
-
// Support: Firefox <=44
// Firefox doesn't have focus(in | out) events
// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
@@ -8148,7 +8820,10 @@ if ( !support.focusin ) {
jQuery.event.special[ fix ] = {
setup: function() {
- var doc = this.ownerDocument || this,
+
+ // Handle: regular nodes (via `this.ownerDocument`), window
+ // (via `this.document`) & document (via `this`).
+ var doc = this.ownerDocument || this.document || this,
attaches = dataPriv.access( doc, fix );
if ( !attaches ) {
@@ -8157,7 +8832,7 @@ if ( !support.focusin ) {
dataPriv.access( doc, fix, ( attaches || 0 ) + 1 );
},
teardown: function() {
- var doc = this.ownerDocument || this,
+ var doc = this.ownerDocument || this.document || this,
attaches = dataPriv.access( doc, fix ) - 1;
if ( !attaches ) {
@@ -8173,7 +8848,7 @@ if ( !support.focusin ) {
}
var location = window.location;
-var nonce = jQuery.now();
+var nonce = { guid: Date.now() };
var rquery = ( /\?/ );
@@ -8210,7 +8885,7 @@ var
function buildParams( prefix, obj, traditional, add ) {
var name;
- if ( jQuery.isArray( obj ) ) {
+ if ( Array.isArray( obj ) ) {
// Serialize array item.
jQuery.each( obj, function( i, v ) {
@@ -8231,7 +8906,7 @@ function buildParams( prefix, obj, traditional, add ) {
}
} );
- } else if ( !traditional && jQuery.type( obj ) === "object" ) {
+ } else if ( !traditional && toType( obj ) === "object" ) {
// Serialize object item.
for ( name in obj ) {
@@ -8253,7 +8928,7 @@ jQuery.param = function( a, traditional ) {
add = function( key, valueOrFunction ) {
// If value is a function, invoke it and use its return value
- var value = jQuery.isFunction( valueOrFunction ) ?
+ var value = isFunction( valueOrFunction ) ?
valueOrFunction() :
valueOrFunction;
@@ -8261,8 +8936,12 @@ jQuery.param = function( a, traditional ) {
encodeURIComponent( value == null ? "" : value );
};
+ if ( a == null ) {
+ return "";
+ }
+
// If an array was passed in, assume that it is an array of form elements.
- if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+ if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
// Serialize the form elements
jQuery.each( a, function() {
@@ -8301,16 +8980,20 @@ jQuery.fn.extend( {
rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
( this.checked || !rcheckableType.test( type ) );
} )
- .map( function( i, elem ) {
+ .map( function( _i, elem ) {
var val = jQuery( this ).val();
- return val == null ?
- null :
- jQuery.isArray( val ) ?
- jQuery.map( val, function( val ) {
- return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
- } ) :
- { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ if ( val == null ) {
+ return null;
+ }
+
+ if ( Array.isArray( val ) ) {
+ return jQuery.map( val, function( val ) {
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ } );
+ }
+
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
} ).get();
}
} );
@@ -8319,7 +9002,7 @@ jQuery.fn.extend( {
var
r20 = /%20/g,
rhash = /#.*$/,
- rts = /([?&])_=[^&]*/,
+ rantiCache = /([?&])_=[^&]*/,
rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
// #7653, #8125, #8152: local protocol detection
@@ -8365,9 +9048,9 @@ function addToPrefiltersOrTransports( structure ) {
var dataType,
i = 0,
- dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];
+ dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];
- if ( jQuery.isFunction( func ) ) {
+ if ( isFunction( func ) ) {
// For each dataType in the dataTypeExpression
while ( ( dataType = dataTypes[ i++ ] ) ) {
@@ -8759,12 +9442,14 @@ jQuery.extend( {
if ( !responseHeaders ) {
responseHeaders = {};
while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
- responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
+ responseHeaders[ match[ 1 ].toLowerCase() + " " ] =
+ ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] )
+ .concat( match[ 2 ] );
}
}
- match = responseHeaders[ key.toLowerCase() ];
+ match = responseHeaders[ key.toLowerCase() + " " ];
}
- return match == null ? null : match;
+ return match == null ? null : match.join( ", " );
},
// Raw string
@@ -8833,13 +9518,13 @@ jQuery.extend( {
s.type = options.method || options.type || s.method || s.type;
// Extract dataTypes list
- s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ];
+ s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ];
// A cross-domain request is in order when the origin doesn't match the current origin.
if ( s.crossDomain == null ) {
urlAnchor = document.createElement( "a" );
- // Support: IE <=8 - 11, Edge 12 - 13
+ // Support: IE <=8 - 11, Edge 12 - 15
// IE throws exception on accessing the href property if url is malformed,
// e.g. http://example.com:80x/
try {
@@ -8897,18 +9582,19 @@ jQuery.extend( {
// Remember the hash so we can put it back
uncached = s.url.slice( cacheURL.length );
- // If data is available, append data to url
- if ( s.data ) {
+ // If data is available and should be processed, append data to url
+ if ( s.data && ( s.processData || typeof s.data === "string" ) ) {
cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data;
// #9682: remove data so that it's not used in an eventual retry
delete s.data;
}
- // Add anti-cache in uncached url if needed
+ // Add or update anti-cache param if needed
if ( s.cache === false ) {
- cacheURL = cacheURL.replace( rts, "" );
- uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached;
+ cacheURL = cacheURL.replace( rantiCache, "$1" );
+ uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) +
+ uncached;
}
// Put hash and anti-cache on the URL that will be requested (gh-1732)
@@ -9041,6 +9727,11 @@ jQuery.extend( {
response = ajaxHandleResponses( s, jqXHR, responses );
}
+ // Use a noop converter for missing script
+ if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) {
+ s.converters[ "text script" ] = function() {};
+ }
+
// Convert no matter what (that way responseXXX fields are always set)
response = ajaxConvert( s, response, jqXHR, isSuccess );
@@ -9131,11 +9822,11 @@ jQuery.extend( {
}
} );
-jQuery.each( [ "get", "post" ], function( i, method ) {
+jQuery.each( [ "get", "post" ], function( _i, method ) {
jQuery[ method ] = function( url, data, callback, type ) {
// Shift arguments if data argument was omitted
- if ( jQuery.isFunction( data ) ) {
+ if ( isFunction( data ) ) {
type = type || callback;
callback = data;
data = undefined;
@@ -9152,8 +9843,17 @@ jQuery.each( [ "get", "post" ], function( i, method ) {
};
} );
+jQuery.ajaxPrefilter( function( s ) {
+ var i;
+ for ( i in s.headers ) {
+ if ( i.toLowerCase() === "content-type" ) {
+ s.contentType = s.headers[ i ] || "";
+ }
+ }
+} );
-jQuery._evalUrl = function( url ) {
+
+jQuery._evalUrl = function( url, options, doc ) {
return jQuery.ajax( {
url: url,
@@ -9163,7 +9863,16 @@ jQuery._evalUrl = function( url ) {
cache: true,
async: false,
global: false,
- "throws": true
+
+ // Only evaluate the response if it is successful (gh-4126)
+ // dataFilter is not invoked for failure responses, so using it instead
+ // of the default converter is kludgy but it works.
+ converters: {
+ "text script": function() {}
+ },
+ dataFilter: function( response ) {
+ jQuery.globalEval( response, options, doc );
+ }
} );
};
@@ -9173,7 +9882,7 @@ jQuery.fn.extend( {
var wrap;
if ( this[ 0 ] ) {
- if ( jQuery.isFunction( html ) ) {
+ if ( isFunction( html ) ) {
html = html.call( this[ 0 ] );
}
@@ -9199,7 +9908,7 @@ jQuery.fn.extend( {
},
wrapInner: function( html ) {
- if ( jQuery.isFunction( html ) ) {
+ if ( isFunction( html ) ) {
return this.each( function( i ) {
jQuery( this ).wrapInner( html.call( this, i ) );
} );
@@ -9219,10 +9928,10 @@ jQuery.fn.extend( {
},
wrap: function( html ) {
- var isFunction = jQuery.isFunction( html );
+ var htmlIsFunction = isFunction( html );
return this.each( function( i ) {
- jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html );
+ jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );
} );
},
@@ -9314,7 +10023,8 @@ jQuery.ajaxTransport( function( options ) {
return function() {
if ( callback ) {
callback = errorCallback = xhr.onload =
- xhr.onerror = xhr.onabort = xhr.onreadystatechange = null;
+ xhr.onerror = xhr.onabort = xhr.ontimeout =
+ xhr.onreadystatechange = null;
if ( type === "abort" ) {
xhr.abort();
@@ -9354,7 +10064,7 @@ jQuery.ajaxTransport( function( options ) {
// Listen to events
xhr.onload = callback();
- errorCallback = xhr.onerror = callback( "error" );
+ errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" );
// Support: IE 9 only
// Use onreadystatechange to replace onabort
@@ -9445,24 +10155,21 @@ jQuery.ajaxPrefilter( "script", function( s ) {
// Bind script tag hack transport
jQuery.ajaxTransport( "script", function( s ) {
- // This transport only deals with cross domain requests
- if ( s.crossDomain ) {
+ // This transport only deals with cross domain or forced-by-attrs requests
+ if ( s.crossDomain || s.scriptAttrs ) {
var script, callback;
return {
send: function( _, complete ) {
- script = jQuery( "<script>" ).prop( {
- charset: s.scriptCharset,
- src: s.url
- } ).on(
- "load error",
- callback = function( evt ) {
+ script = jQuery( "<script>" )
+ .attr( s.scriptAttrs || {} )
+ .prop( { charset: s.scriptCharset, src: s.url } )
+ .on( "load error", callback = function( evt ) {
script.remove();
callback = null;
if ( evt ) {
complete( evt.type === "error" ? 404 : 200, evt.type );
}
- }
- );
+ } );
// Use native DOM manipulation to avoid our domManip AJAX trickery
document.head.appendChild( script[ 0 ] );
@@ -9486,7 +10193,7 @@ var oldCallbacks = [],
jQuery.ajaxSetup( {
jsonp: "callback",
jsonpCallback: function() {
- var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+ var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce.guid++ ) );
this[ callback ] = true;
return callback;
}
@@ -9508,7 +10215,7 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
// Get callback name, remembering preexisting value associated with it
- callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+ callbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?
s.jsonpCallback() :
s.jsonpCallback;
@@ -9559,7 +10266,7 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
}
// Call if it was a function and we have a response
- if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+ if ( responseContainer && isFunction( overwritten ) ) {
overwritten( responseContainer[ 0 ] );
}
@@ -9646,12 +10353,12 @@ jQuery.fn.load = function( url, params, callback ) {
off = url.indexOf( " " );
if ( off > -1 ) {
- selector = jQuery.trim( url.slice( off ) );
+ selector = stripAndCollapse( url.slice( off ) );
url = url.slice( 0, off );
}
// If it's a function
- if ( jQuery.isFunction( params ) ) {
+ if ( isFunction( params ) ) {
// We assume that it's the callback
callback = params;
@@ -9703,23 +10410,6 @@ jQuery.fn.load = function( url, params, callback ) {
-// Attach a bunch of functions for handling common AJAX events
-jQuery.each( [
- "ajaxStart",
- "ajaxStop",
- "ajaxComplete",
- "ajaxError",
- "ajaxSuccess",
- "ajaxSend"
-], function( i, type ) {
- jQuery.fn[ type ] = function( fn ) {
- return this.on( type, fn );
- };
-} );
-
-
-
-
jQuery.expr.pseudos.animated = function( elem ) {
return jQuery.grep( jQuery.timers, function( fn ) {
return elem === fn.elem;
@@ -9729,13 +10419,6 @@ jQuery.expr.pseudos.animated = function( elem ) {
-/**
- * Gets a window from an element
- */
-function getWindow( elem ) {
- return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;
-}
-
jQuery.offset = {
setOffset: function( elem, options, i ) {
var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
@@ -9766,7 +10449,7 @@ jQuery.offset = {
curLeft = parseFloat( curCSSLeft ) || 0;
}
- if ( jQuery.isFunction( options ) ) {
+ if ( isFunction( options ) ) {
// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
@@ -9783,12 +10466,20 @@ jQuery.offset = {
options.using.call( elem, props );
} else {
+ if ( typeof props.top === "number" ) {
+ props.top += "px";
+ }
+ if ( typeof props.left === "number" ) {
+ props.left += "px";
+ }
curElem.css( props );
}
}
};
jQuery.fn.extend( {
+
+ // offset() relates an element's border box to the document origin
offset: function( options ) {
// Preserve chaining for setter
@@ -9800,13 +10491,14 @@ jQuery.fn.extend( {
} );
}
- var docElem, win, rect, doc,
+ var rect, win,
elem = this[ 0 ];
if ( !elem ) {
return;
}
+ // Return zeros for disconnected and hidden (display: none) elements (gh-2310)
// Support: IE <=11 only
// Running getBoundingClientRect on a
// disconnected node in IE throws an error
@@ -9814,56 +10506,52 @@ jQuery.fn.extend( {
return { top: 0, left: 0 };
}
+ // Get document-relative position by adding viewport scroll to viewport-relative gBCR
rect = elem.getBoundingClientRect();
-
- // Make sure element is not hidden (display: none)
- if ( rect.width || rect.height ) {
- doc = elem.ownerDocument;
- win = getWindow( doc );
- docElem = doc.documentElement;
-
- return {
- top: rect.top + win.pageYOffset - docElem.clientTop,
- left: rect.left + win.pageXOffset - docElem.clientLeft
- };
- }
-
- // Return zeros for disconnected and hidden elements (gh-2310)
- return rect;
+ win = elem.ownerDocument.defaultView;
+ return {
+ top: rect.top + win.pageYOffset,
+ left: rect.left + win.pageXOffset
+ };
},
+ // position() relates an element's margin box to its offset parent's padding box
+ // This corresponds to the behavior of CSS absolute positioning
position: function() {
if ( !this[ 0 ] ) {
return;
}
- var offsetParent, offset,
+ var offsetParent, offset, doc,
elem = this[ 0 ],
parentOffset = { top: 0, left: 0 };
- // Fixed elements are offset from window (parentOffset = {top:0, left: 0},
- // because it is its only offset parent
+ // position:fixed elements are offset from the viewport, which itself always has zero offset
if ( jQuery.css( elem, "position" ) === "fixed" ) {
- // Assume getBoundingClientRect is there when computed position is fixed
+ // Assume position:fixed implies availability of getBoundingClientRect
offset = elem.getBoundingClientRect();
} else {
+ offset = this.offset();
- // Get *real* offsetParent
- offsetParent = this.offsetParent();
+ // Account for the *real* offset parent, which can be the document or its root element
+ // when a statically positioned element is identified
+ doc = elem.ownerDocument;
+ offsetParent = elem.offsetParent || doc.documentElement;
+ while ( offsetParent &&
+ ( offsetParent === doc.body || offsetParent === doc.documentElement ) &&
+ jQuery.css( offsetParent, "position" ) === "static" ) {
- // Get correct offsets
- offset = this.offset();
- if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
- parentOffset = offsetParent.offset();
+ offsetParent = offsetParent.parentNode;
}
+ if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {
- // Add offsetParent borders
- parentOffset = {
- top: parentOffset.top + jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ),
- left: parentOffset.left + jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true )
- };
+ // Incorporate borders into its offset, since they are outside its content origin
+ parentOffset = jQuery( offsetParent ).offset();
+ parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true );
+ parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true );
+ }
}
// Subtract parent offsets and element margins
@@ -9902,7 +10590,14 @@ jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function(
jQuery.fn[ method ] = function( val ) {
return access( this, function( elem, method, val ) {
- var win = getWindow( elem );
+
+ // Coalesce documents and windows
+ var win;
+ if ( isWindow( elem ) ) {
+ win = elem;
+ } else if ( elem.nodeType === 9 ) {
+ win = elem.defaultView;
+ }
if ( val === undefined ) {
return win ? win[ prop ] : elem[ method ];
@@ -9927,7 +10622,7 @@ jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function(
// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347
// getComputedStyle returns percent when specified for top/left/bottom/right;
// rather than make the css module depend on the offset module, just check for it here
-jQuery.each( [ "top", "left" ], function( i, prop ) {
+jQuery.each( [ "top", "left" ], function( _i, prop ) {
jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
function( elem, computed ) {
if ( computed ) {
@@ -9956,7 +10651,7 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
return access( this, function( elem, type, value ) {
var doc;
- if ( jQuery.isWindow( elem ) ) {
+ if ( isWindow( elem ) ) {
// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
return funcName.indexOf( "outer" ) === 0 ?
@@ -9990,6 +10685,22 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
} );
+jQuery.each( [
+ "ajaxStart",
+ "ajaxStop",
+ "ajaxComplete",
+ "ajaxError",
+ "ajaxSuccess",
+ "ajaxSend"
+], function( _i, type ) {
+ jQuery.fn[ type ] = function( fn ) {
+ return this.on( type, fn );
+ };
+} );
+
+
+
+
jQuery.fn.extend( {
bind: function( types, data, fn ) {
@@ -10008,11 +10719,100 @@ jQuery.fn.extend( {
return arguments.length === 1 ?
this.off( selector, "**" ) :
this.off( types, selector || "**", fn );
+ },
+
+ hover: function( fnOver, fnOut ) {
+ return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
}
} );
+jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
+ "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+ "change select submit keydown keypress keyup contextmenu" ).split( " " ),
+ function( _i, name ) {
+
+ // Handle event binding
+ jQuery.fn[ name ] = function( data, fn ) {
+ return arguments.length > 0 ?
+ this.on( name, null, data, fn ) :
+ this.trigger( name );
+ };
+ } );
+
+
+
+
+// Support: Android <=4.0 only
+// Make sure we trim BOM and NBSP
+var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
+
+// Bind a function to a context, optionally partially applying any
+// arguments.
+// jQuery.proxy is deprecated to promote standards (specifically Function#bind)
+// However, it is not slated for removal any time soon
+jQuery.proxy = function( fn, context ) {
+ var tmp, args, proxy;
+
+ if ( typeof context === "string" ) {
+ tmp = fn[ context ];
+ context = fn;
+ fn = tmp;
+ }
+
+ // Quick check to determine if target is callable, in the spec
+ // this throws a TypeError, but we will just return undefined.
+ if ( !isFunction( fn ) ) {
+ return undefined;
+ }
+
+ // Simulated bind
+ args = slice.call( arguments, 2 );
+ proxy = function() {
+ return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
+ };
+
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+ return proxy;
+};
+
+jQuery.holdReady = function( hold ) {
+ if ( hold ) {
+ jQuery.readyWait++;
+ } else {
+ jQuery.ready( true );
+ }
+};
+jQuery.isArray = Array.isArray;
jQuery.parseJSON = JSON.parse;
+jQuery.nodeName = nodeName;
+jQuery.isFunction = isFunction;
+jQuery.isWindow = isWindow;
+jQuery.camelCase = camelCase;
+jQuery.type = toType;
+
+jQuery.now = Date.now;
+
+jQuery.isNumeric = function( obj ) {
+
+ // As of jQuery 3.0, isNumeric is limited to
+ // strings and numbers (primitives or objects)
+ // that can be coerced to finite numbers (gh-2662)
+ var type = jQuery.type( obj );
+ return ( type === "number" || type === "string" ) &&
+
+ // parseFloat NaNs numeric-cast false positives ("")
+ // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
+ // subtraction forces infinities to NaN
+ !isNaN( obj - parseFloat( obj ) );
+};
+jQuery.trim = function( text ) {
+ return text == null ?
+ "" :
+ ( text + "" ).replace( rtrim, "" );
+};
@@ -10038,7 +10838,6 @@ if ( typeof define === "function" && define.amd ) {
-
var
// Map over jQuery in case of overwrite
@@ -10062,27 +10861,88 @@ jQuery.noConflict = function( deep ) {
// Expose jQuery and $ identifiers, even in AMD
// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
// and CommonJS for browser emulators (#13566)
-if ( !noGlobal ) {
+if ( typeof noGlobal === "undefined" ) {
window.jQuery = window.$ = jQuery;
}
+
+
return jQuery;
} );
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
-(function(window){
- var _jQuery = window.jQuery.noConflict(true);
+(function(window) {'use strict';
+/* exported
+ minErrConfig,
+ errorHandlingConfig,
+ isValidObjectMaxDepth
+*/
+
+var minErrConfig = {
+ objectMaxDepth: 5,
+ urlErrorParamsEnabled: true
+};
+
+/**
+ * @ngdoc function
+ * @name angular.errorHandlingConfig
+ * @module ng
+ * @kind function
+ *
+ * @description
+ * Configure several aspects of error handling in AngularJS if used as a setter or return the
+ * current configuration if used as a getter. The following options are supported:
+ *
+ * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages.
+ *
+ * Omitted or undefined options will leave the corresponding configuration values unchanged.
+ *
+ * @param {Object=} config - The configuration object. May only contain the options that need to be
+ * updated. Supported keys:
+ *
+ * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
+ * non-positive or non-numeric value, removes the max depth limit.
+ * Default: 5
+ *
+ * * `urlErrorParamsEnabled` **{Boolean}** - Specifies whether the generated error url will
+ * contain the parameters of the thrown error. Disabling the parameters can be useful if the
+ * generated error url is very long.
+ *
+ * Default: true. When used without argument, it returns the current value.
+ */
+function errorHandlingConfig(config) {
+ if (isObject(config)) {
+ if (isDefined(config.objectMaxDepth)) {
+ minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
+ }
+ if (isDefined(config.urlErrorParamsEnabled) && isBoolean(config.urlErrorParamsEnabled)) {
+ minErrConfig.urlErrorParamsEnabled = config.urlErrorParamsEnabled;
+ }
+ } else {
+ return minErrConfig;
+ }
+}
+
+/**
+ * @private
+ * @param {Number} maxDepth
+ * @return {boolean}
+ */
+function isValidObjectMaxDepth(maxDepth) {
+ return isNumber(maxDepth) && maxDepth > 0;
+}
+
/**
* @description
*
* This object provides a utility for producing rich Error messages within
- * Angular. It can be called as follows:
+ * AngularJS. It can be called as follows:
*
* var exampleMinErr = minErr('example');
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
@@ -10099,7 +10959,7 @@ return jQuery;
* Since data will be parsed statically during a build step, some restrictions
* are applied with respect to how minErr instances are created and called.
* Instances should have names of the form namespaceMinErr for a minErr created
- * using minErr('namespace') . Error codes, namespaces and template strings
+ * using minErr('namespace'). Error codes, namespaces and template strings
* should all be static strings, not variables or general expressions.
*
* @param {string} module The namespace to use for the new minErr instance.
@@ -10110,131 +10970,148 @@ return jQuery;
function minErr(module, ErrorConstructor) {
ErrorConstructor = ErrorConstructor || Error;
- return function() {
- var SKIP_INDEXES = 2;
- var templateArgs = arguments,
- code = templateArgs[0],
+ var url = 'https://errors.angularjs.org/"1.8.2"/';
+ var regex = url.replace('.', '\\.') + '[\\s\\S]*';
+ var errRegExp = new RegExp(regex, 'g');
+
+ return function() {
+ var code = arguments[0],
+ template = arguments[1],
message = '[' + (module ? module + ':' : '') + code + '] ',
- template = templateArgs[1],
+ templateArgs = sliceArgs(arguments, 2).map(function(arg) {
+ return toDebugString(arg, minErrConfig.objectMaxDepth);
+ }),
paramPrefix, i;
+ // A minErr message has two parts: the message itself and the url that contains the
+ // encoded message.
+ // The message's parameters can contain other error messages which also include error urls.
+ // To prevent the messages from getting too long, we strip the error urls from the parameters.
+
message += template.replace(/\{\d+\}/g, function(match) {
- var index = +match.slice(1, -1),
- shiftedIndex = index + SKIP_INDEXES;
+ var index = +match.slice(1, -1);
- if (shiftedIndex < templateArgs.length) {
- return toDebugString(templateArgs[shiftedIndex]);
+ if (index < templateArgs.length) {
+ return templateArgs[index].replace(errRegExp, '');
}
return match;
});
- message += '\nhttp://errors.angularjs.org/1.5.8/' +
- (module ? module + '/' : '') + code;
+ message += '\n' + url + (module ? module + '/' : '') + code;
- for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
- message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
- encodeURIComponent(toDebugString(templateArgs[i]));
+ if (minErrConfig.urlErrorParamsEnabled) {
+ for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
+ message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
+ }
}
return new ErrorConstructor(message);
};
}
-/* We need to tell jshint what variables are being exported */
-/* global angular: true,
- msie: true,
- jqLite: true,
- jQuery: true,
- slice: true,
- splice: true,
- push: true,
- toString: true,
- ngMinErr: true,
- angularModule: true,
- uid: true,
- REGEX_STRING_REGEXP: true,
- VALIDITY_STATE_PROPERTY: true,
-
- lowercase: true,
- uppercase: true,
- manualLowercase: true,
- manualUppercase: true,
- nodeName_: true,
- isArrayLike: true,
- forEach: true,
- forEachSorted: true,
- reverseParams: true,
- nextUid: true,
- setHashKey: true,
- extend: true,
- toInt: true,
- inherit: true,
- merge: true,
- noop: true,
- identity: true,
- valueFn: true,
- isUndefined: true,
- isDefined: true,
- isObject: true,
- isBlankObject: true,
- isString: true,
- isNumber: true,
- isDate: true,
- isArray: true,
- isFunction: true,
- isRegExp: true,
- isWindow: true,
- isScope: true,
- isFile: true,
- isFormData: true,
- isBlob: true,
- isBoolean: true,
- isPromiseLike: true,
- trim: true,
- escapeForRegexp: true,
- isElement: true,
- makeMap: true,
- includes: true,
- arrayRemove: true,
- copy: true,
- equals: true,
- csp: true,
- jq: true,
- concat: true,
- sliceArgs: true,
- bind: true,
- toJsonReplacer: true,
- toJson: true,
- fromJson: true,
- convertTimezoneToLocal: true,
- timezoneToOffset: true,
- startingTag: true,
- tryDecodeURIComponent: true,
- parseKeyValue: true,
- toKeyValue: true,
- encodeUriSegment: true,
- encodeUriQuery: true,
- angularInit: true,
- bootstrap: true,
- getTestability: true,
- snake_case: true,
- bindJQuery: true,
- assertArg: true,
- assertArgFn: true,
- assertNotHasOwnProperty: true,
- getter: true,
- getBlockNodes: true,
- hasOwnProperty: true,
- createMap: true,
-
- NODE_TYPE_ELEMENT: true,
- NODE_TYPE_ATTRIBUTE: true,
- NODE_TYPE_TEXT: true,
- NODE_TYPE_COMMENT: true,
- NODE_TYPE_DOCUMENT: true,
- NODE_TYPE_DOCUMENT_FRAGMENT: true,
+/* We need to tell ESLint what variables are being exported */
+/* exported
+ angular,
+ msie,
+ jqLite,
+ jQuery,
+ slice,
+ splice,
+ push,
+ toString,
+ minErrConfig,
+ errorHandlingConfig,
+ isValidObjectMaxDepth,
+ ngMinErr,
+ angularModule,
+ uid,
+ REGEX_STRING_REGEXP,
+ VALIDITY_STATE_PROPERTY,
+
+ lowercase,
+ uppercase,
+ nodeName_,
+ isArrayLike,
+ forEach,
+ forEachSorted,
+ reverseParams,
+ nextUid,
+ setHashKey,
+ extend,
+ toInt,
+ inherit,
+ merge,
+ noop,
+ identity,
+ valueFn,
+ isUndefined,
+ isDefined,
+ isObject,
+ isBlankObject,
+ isString,
+ isNumber,
+ isNumberNaN,
+ isDate,
+ isError,
+ isArray,
+ isFunction,
+ isRegExp,
+ isWindow,
+ isScope,
+ isFile,
+ isFormData,
+ isBlob,
+ isBoolean,
+ isPromiseLike,
+ trim,
+ escapeForRegexp,
+ isElement,
+ makeMap,
+ includes,
+ arrayRemove,
+ copy,
+ simpleCompare,
+ equals,
+ csp,
+ jq,
+ concat,
+ sliceArgs,
+ bind,
+ toJsonReplacer,
+ toJson,
+ fromJson,
+ convertTimezoneToLocal,
+ timezoneToOffset,
+ addDateMinutes,
+ startingTag,
+ tryDecodeURIComponent,
+ parseKeyValue,
+ toKeyValue,
+ encodeUriSegment,
+ encodeUriQuery,
+ angularInit,
+ bootstrap,
+ getTestability,
+ snake_case,
+ bindJQuery,
+ assertArg,
+ assertArgFn,
+ assertNotHasOwnProperty,
+ getter,
+ getBlockNodes,
+ hasOwnProperty,
+ createMap,
+ stringify,
+ UNSAFE_restoreLegacyJqLiteXHTMLReplacement,
+
+ NODE_TYPE_ELEMENT,
+ NODE_TYPE_ATTRIBUTE,
+ NODE_TYPE_TEXT,
+ NODE_TYPE_COMMENT,
+ NODE_TYPE_DOCUMENT,
+ NODE_TYPE_DOCUMENT_FRAGMENT
*/
////////////////////////////////////
@@ -10246,13 +11123,11 @@ function minErr(module, ErrorConstructor) {
* @installation
* @description
*
- * # ng (core module)
* The ng module is loaded by default when an AngularJS application is started. The module itself
* contains the essential components for an AngularJS application to function. The table below
* lists a high level breakdown of each of the services/factories, filters, directives and testing
* components available within this core module.
*
- * <div doc-module-components="ng"></div>
*/
var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
@@ -10261,33 +11136,26 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
// This is used so that it's possible for internal tests to create mock ValidityStates.
var VALIDITY_STATE_PROPERTY = 'validity';
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
+/**
+ * @private
+ *
+ * @description Converts the specified string to lowercase.
+ * @param {string} string String to be converted to lowercase.
+ * @returns {string} Lowercased string.
+ */
var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
-var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
-
-
-var manualLowercase = function(s) {
- /* jshint bitwise: false */
- return isString(s)
- ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);})
- : s;
-};
-var manualUppercase = function(s) {
- /* jshint bitwise: false */
- return isString(s)
- ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);})
- : s;
-};
-
-// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish
-// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods
-// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387
-if ('i' !== 'I'.toLowerCase()) {
- lowercase = manualLowercase;
- uppercase = manualUppercase;
-}
+/**
+ * @private
+ *
+ * @description Converts the specified string to uppercase.
+ * @param {string} string String to be converted to uppercase.
+ * @returns {string} Uppercased string.
+ */
+var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
var
@@ -10306,6 +11174,7 @@ var
angularModule,
uid = 0;
+// Support: IE 9-11 only
/**
* documentMode is an IE-only property
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
@@ -10332,12 +11201,11 @@ function isArrayLike(obj) {
// Support: iOS 8.2 (not reproducible in simulator)
// "length" in obj used to prevent JIT error (gh-11508)
- var length = "length" in Object(obj) && obj.length;
+ var length = 'length' in Object(obj) && obj.length;
// NodeList objects (with `item` method) and
// other objects with suitable length characteristics are array-like
- return isNumber(length) &&
- (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item == 'function');
+ return isNumber(length) && (length >= 0 && (length - 1) in obj || typeof obj.item === 'function');
}
@@ -10381,9 +11249,7 @@ function forEach(obj, iterator, context) {
if (obj) {
if (isFunction(obj)) {
for (key in obj) {
- // Need to check if hasOwnProperty exists,
- // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function
- if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) {
+ if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key, obj);
}
}
@@ -10488,8 +11354,10 @@ function baseExtend(dst, objs, deep) {
} else if (isElement(src)) {
dst[key] = src.clone();
} else {
- if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
- baseExtend(dst[key], [src], true);
+ if (key !== '__proto__') {
+ if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
+ baseExtend(dst[key], [src], true);
+ }
}
} else {
dst[key] = src;
@@ -10538,6 +11406,22 @@ function extend(dst) {
* Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source
* objects, performing a deep copy.
*
+* @deprecated
+* sinceVersion="1.6.5"
+* This function is deprecated, but will not be removed in the 1.x lifecycle.
+* There are edge cases (see {@link angular.merge#known-issues known issues}) that are not
+* supported by this function. We suggest using another, similar library for all-purpose merging,
+* such as [lodash's merge()](https://lodash.com/docs/4.17.4#merge).
+*
+* @knownIssue
+* This is a list of (known) object types that are not handled correctly by this function:
+* - [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob)
+* - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream)
+* - [`CanvasGradient`](https://developer.mozilla.org/docs/Web/API/CanvasGradient)
+* - AngularJS {@link $rootScope.Scope scopes};
+*
+* `angular.merge` also does not support merging objects with circular references.
+*
* @param {Object} dst Destination object.
* @param {...Object} src Source object(s).
* @returns {Object} Reference to `dst`.
@@ -10552,6 +11436,11 @@ function toInt(str) {
return parseInt(str, 10);
}
+var isNumberNaN = Number.isNaN || function isNumberNaN(num) {
+ // eslint-disable-next-line no-self-compare
+ return num !== num;
+};
+
function inherit(parent, extra) {
return extend(Object.create(parent), extra);
@@ -10740,7 +11629,27 @@ function isDate(value) {
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is an `Array`.
*/
-var isArray = Array.isArray;
+function isArray(arr) {
+ return Array.isArray(arr) || arr instanceof Array;
+}
+
+/**
+ * @description
+ * Determines if a reference is an `Error`.
+ * Loosely based on https://www.npmjs.com/package/iserror
+ *
+ * @param {*} value Reference to check.
+ * @returns {boolean} True if `value` is an `Error`.
+ */
+function isError(value) {
+ var tag = toString.call(value);
+ switch (tag) {
+ case '[object Error]': return true;
+ case '[object Exception]': return true;
+ case '[object DOMException]': return true;
+ default: return value instanceof Error;
+ }
+}
/**
* @ngdoc function
@@ -10811,7 +11720,7 @@ function isPromiseLike(obj) {
}
-var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/;
+var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/;
function isTypedArray(value) {
return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
}
@@ -10829,8 +11738,10 @@ var trim = function(value) {
// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021
// Prereq: s is a string.
var escapeForRegexp = function(s) {
- return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
- replace(/\x08/g, '\\x08');
+ return s
+ .replace(/([-()[\]{}+?*.$^|,:#<!\\])/g, '\\$1')
+ // eslint-disable-next-line no-control-regex
+ .replace(/\x08/g, '\\x08');
};
@@ -10870,7 +11781,7 @@ function nodeName_(element) {
}
function includes(array, obj) {
- return Array.prototype.indexOf.call(array, obj) != -1;
+ return Array.prototype.indexOf.call(array, obj) !== -1;
}
function arrayRemove(array, value) {
@@ -10888,7 +11799,9 @@ function arrayRemove(array, value) {
* @kind function
*
* @description
- * Creates a deep copy of `source`, which should be an object or an array.
+ * Creates a deep copy of `source`, which should be an object or an array. This functions is used
+ * internally, mostly in the change-detection code. It is not intended as an all-purpose copy
+ * function, and has several limitations (see below).
*
* * If no destination is supplied, a copy of the object or array is created.
* * If a destination is provided, all of its elements (for arrays) or properties (for objects)
@@ -10897,19 +11810,39 @@ function arrayRemove(array, value) {
* * If `source` is identical to `destination` an exception will be thrown.
*
* <br />
+ *
* <div class="alert alert-warning">
* Only enumerable properties are taken into account. Non-enumerable properties (both on `source`
* and on `destination`) will be ignored.
* </div>
*
- * @param {*} source The source that will be used to make a copy.
- * Can be any type, including primitives, `null`, and `undefined`.
- * @param {(Object|Array)=} destination Destination into which the source is copied. If
- * provided, must be of the same type as `source`.
+ * <div class="alert alert-warning">
+ * `angular.copy` does not check if destination and source are of the same type. It's the
+ * developer's responsibility to make sure they are compatible.
+ * </div>
+ *
+ * @knownIssue
+ * This is a non-exhaustive list of object types / features that are not handled correctly by
+ * `angular.copy`. Note that since this functions is used by the change detection code, this
+ * means binding or watching objects of these types (or that include these types) might not work
+ * correctly.
+ * - [`File`](https://developer.mozilla.org/docs/Web/API/File)
+ * - [`Map`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)
+ * - [`ImageData`](https://developer.mozilla.org/docs/Web/API/ImageData)
+ * - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream)
+ * - [`Set`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set)
+ * - [`WeakMap`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)
+ * - [`getter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)/
+ * [`setter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set)
+ *
+ * @param {*} source The source that will be used to make a copy. Can be any type, including
+ * primitives, `null`, and `undefined`.
+ * @param {(Object|Array)=} destination Destination into which the source is copied. If provided,
+ * must be of the same type as `source`.
* @returns {*} The copy or updated `destination`, if `destination` was specified.
*
* @example
- <example module="copyExample">
+ <example module="copyExample" name="angular-copy">
<file name="index.html">
<div ng-controller="ExampleController">
<form novalidate class="simple-form">
@@ -10921,7 +11854,7 @@ function arrayRemove(array, value) {
<button ng-click="update(user)">SAVE</button>
</form>
<pre>form = {{user | json}}</pre>
- <pre>master = {{master | json}}</pre>
+ <pre>leader = {{leader | json}}</pre>
</div>
</file>
<file name="script.js">
@@ -10929,16 +11862,16 @@ function arrayRemove(array, value) {
angular.
module('copyExample', []).
controller('ExampleController', ['$scope', function($scope) {
- $scope.master = {};
+ $scope.leader = {};
$scope.reset = function() {
// Example with 1 argument
- $scope.user = angular.copy($scope.master);
+ $scope.user = angular.copy($scope.leader);
};
$scope.update = function(user) {
// Example with 2 arguments
- angular.copy(user, $scope.master);
+ angular.copy(user, $scope.leader);
};
$scope.reset();
@@ -10946,16 +11879,17 @@ function arrayRemove(array, value) {
</file>
</example>
*/
-function copy(source, destination) {
+function copy(source, destination, maxDepth) {
var stackSource = [];
var stackDest = [];
+ maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN;
if (destination) {
if (isTypedArray(destination) || isArrayBuffer(destination)) {
- throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
+ throw ngMinErr('cpta', 'Can\'t copy! TypedArray destination cannot be mutated.');
}
if (source === destination) {
- throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
+ throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.');
}
// Empty the destination object
@@ -10971,35 +11905,39 @@ function copy(source, destination) {
stackSource.push(source);
stackDest.push(destination);
- return copyRecurse(source, destination);
+ return copyRecurse(source, destination, maxDepth);
}
- return copyElement(source);
+ return copyElement(source, maxDepth);
- function copyRecurse(source, destination) {
+ function copyRecurse(source, destination, maxDepth) {
+ maxDepth--;
+ if (maxDepth < 0) {
+ return '...';
+ }
var h = destination.$$hashKey;
var key;
if (isArray(source)) {
for (var i = 0, ii = source.length; i < ii; i++) {
- destination.push(copyElement(source[i]));
+ destination.push(copyElement(source[i], maxDepth));
}
} else if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
}
} else {
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
}
}
@@ -11007,7 +11945,7 @@ function copy(source, destination) {
return destination;
}
- function copyElement(source) {
+ function copyElement(source, maxDepth) {
// Simple values
if (!isObject(source)) {
return source;
@@ -11021,7 +11959,7 @@ function copy(source, destination) {
if (isWindow(source) || isScope(source)) {
throw ngMinErr('cpws',
- "Can't copy! Making copies of Window or Scope instances is not supported.");
+ 'Can\'t copy! Making copies of Window or Scope instances is not supported.');
}
var needsRecurse = false;
@@ -11036,7 +11974,7 @@ function copy(source, destination) {
stackDest.push(destination);
return needsRecurse
- ? copyRecurse(source, destination)
+ ? copyRecurse(source, destination, maxDepth)
: destination;
}
@@ -11054,10 +11992,13 @@ function copy(source, destination) {
return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length);
case '[object ArrayBuffer]':
- //Support: IE10
+ // Support: IE10
if (!source.slice) {
+ // If we're in this case we know the environment supports ArrayBuffer
+ /* eslint-disable no-undef */
var copied = new ArrayBuffer(source.byteLength);
new Uint8Array(copied).set(new Uint8Array(source));
+ /* eslint-enable */
return copied;
}
return source.slice(0);
@@ -11069,7 +12010,7 @@ function copy(source, destination) {
return new source.constructor(source.valueOf());
case '[object RegExp]':
- var re = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
+ var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]);
re.lastIndex = source.lastIndex;
return re;
@@ -11084,6 +12025,10 @@ function copy(source, destination) {
}
+// eslint-disable-next-line no-self-compare
+function simpleCompare(a, b) { return a === b || (a !== a && b !== b); }
+
+
/**
* @ngdoc function
* @name angular.equals
@@ -11140,7 +12085,6 @@ function copy(source, destination) {
angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) {
$scope.user1 = {};
$scope.user2 = {};
- $scope.result;
$scope.compare = function() {
$scope.result = angular.equals($scope.user1, $scope.user2);
};
@@ -11151,12 +12095,13 @@ function copy(source, destination) {
function equals(o1, o2) {
if (o1 === o2) return true;
if (o1 === null || o2 === null) return false;
+ // eslint-disable-next-line no-self-compare
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
- if (t1 == t2 && t1 == 'object') {
+ if (t1 === t2 && t1 === 'object') {
if (isArray(o1)) {
if (!isArray(o2)) return false;
- if ((length = o1.length) == o2.length) {
+ if ((length = o1.length) === o2.length) {
for (key = 0; key < length; key++) {
if (!equals(o1[key], o2[key])) return false;
}
@@ -11164,10 +12109,10 @@ function equals(o1, o2) {
}
} else if (isDate(o1)) {
if (!isDate(o2)) return false;
- return equals(o1.getTime(), o2.getTime());
+ return simpleCompare(o1.getTime(), o2.getTime());
} else if (isRegExp(o1)) {
if (!isRegExp(o2)) return false;
- return o1.toString() == o2.toString();
+ return o1.toString() === o2.toString();
} else {
if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
@@ -11215,9 +12160,8 @@ var csp = function() {
function noUnsafeEval() {
try {
- /* jshint -W031, -W054 */
+ // eslint-disable-next-line no-new, no-new-func
new Function('');
- /* jshint +W031, +W054 */
return false;
} catch (e) {
return true;
@@ -11238,7 +12182,7 @@ var csp = function() {
* used to force either jqLite by leaving ng-jq blank or setting the name of
* the jquery variable under window (eg. jQuery).
*
- * Since angular looks for this directive when it is loaded (doesn't wait for the
+ * Since AngularJS looks for this directive when it is loaded (doesn't wait for the
* DOMContentLoaded event), it must be placed on an element that comes before the script
* which loads angular. Also, only the first instance of `ng-jq` will be used and all
* others ignored.
@@ -11269,7 +12213,8 @@ var jq = function() {
var i, ii = ngAttrPrefixes.length, prefix, name;
for (i = 0; i < ii; ++i) {
prefix = ngAttrPrefixes[i];
- if (el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
+ el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]');
+ if (el) {
name = el.getAttribute(prefix + 'jq');
break;
}
@@ -11287,7 +12232,6 @@ function sliceArgs(args, startIndex) {
}
-/* jshint -W101 */
/**
* @ngdoc function
* @name angular.bind
@@ -11305,7 +12249,6 @@ function sliceArgs(args, startIndex) {
* @param {...*} args Optional arguments to be prebound to the `fn` function call.
* @returns {function()} Function that wraps the `fn` with all the specified bindings.
*/
-/* jshint +W101 */
function bind(self, fn) {
var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : [];
if (isFunction(fn) && !(fn instanceof RegExp)) {
@@ -11352,9 +12295,9 @@ function toJsonReplacer(key, value) {
*
* @description
* Serializes input into a JSON-formatted string. Properties with leading $$ characters will be
- * stripped since angular uses this notation internally.
+ * stripped since AngularJS uses this notation internally.
*
- * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON.
+ * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON.
* @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace.
* If set to an integer, the JSON output will contain that many spaces per indentation.
* @returns {string|undefined} JSON-ified string representing `obj`.
@@ -11410,10 +12353,11 @@ function fromJson(json) {
var ALL_COLONS = /:/g;
function timezoneToOffset(timezone, fallback) {
+ // Support: IE 9-11 only, Edge 13-15+
// IE/Edge do not "understand" colon (`:`) in timezone
timezone = timezone.replace(ALL_COLONS, '');
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
- return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
+ return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
@@ -11436,18 +12380,13 @@ function convertTimezoneToLocal(date, timezone, reverse) {
* @returns {string} Returns the string representation of the element.
*/
function startingTag(element) {
- element = jqLite(element).clone();
- 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) {}
- var elemHtml = jqLite('<div>').append(element).html();
+ element = jqLite(element).clone().empty();
+ var elemHtml = jqLite('<div></div>').append(element).html();
try {
return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) :
elemHtml.
match(/^(<[^>]+>)/)[1].
- replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);});
+ replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);});
} catch (e) {
return lowercase(elemHtml);
}
@@ -11480,7 +12419,7 @@ function tryDecodeURIComponent(value) {
*/
function parseKeyValue(/**string*/keyValue) {
var obj = {};
- forEach((keyValue || "").split('&'), function(keyValue) {
+ forEach((keyValue || '').split('&'), function(keyValue) {
var splitPoint, key, val;
if (keyValue) {
key = keyValue = keyValue.replace(/\+/g,'%20');
@@ -11545,7 +12484,7 @@ function encodeUriSegment(val) {
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
* encoded per http://tools.ietf.org/html/rfc3986:
- * query = *( pchar / "/" / "?" )
+ * query = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
@@ -11575,6 +12514,58 @@ function getNgAttribute(element, ngAttr) {
return null;
}
+function allowAutoBootstrap(document) {
+ var script = document.currentScript;
+
+ if (!script) {
+ // Support: IE 9-11 only
+ // IE does not have `document.currentScript`
+ return true;
+ }
+
+ // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack
+ if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) {
+ return false;
+ }
+
+ var attributes = script.attributes;
+ var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')];
+
+ return srcs.every(function(src) {
+ if (!src) {
+ return true;
+ }
+ if (!src.value) {
+ return false;
+ }
+
+ var link = document.createElement('a');
+ link.href = src.value;
+
+ if (document.location.origin === link.origin) {
+ // Same-origin resources are always allowed, even for banned URL schemes.
+ return true;
+ }
+ // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web.
+ // This is to prevent angular.js bundled with browser extensions from being used to bypass the
+ // content security policy in web pages and other browser extensions.
+ switch (link.protocol) {
+ case 'http:':
+ case 'https:':
+ case 'ftp:':
+ case 'blob:':
+ case 'file:':
+ case 'data:':
+ return true;
+ default:
+ return false;
+ }
+ });
+}
+
+// Cached as it has to run during loading so that document.currentScript is available.
+var isAutoBootstrapAllowed = allowAutoBootstrap(window.document);
+
/**
* @ngdoc directive
* @name ngApp
@@ -11616,9 +12607,13 @@ function getNgAttribute(element, ngAttr) {
* document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}`
* would not be resolved to `3`.
*
+ * @example
+ *
+ * ### Simple Usage
+ *
* `ngApp` is the easiest, and most common way to bootstrap an application.
*
- <example module="ngAppDemo">
+ <example module="ngAppDemo" name="ng-app">
<file name="index.html">
<div ng-controller="ngAppDemoController">
I can add: {{a}} + {{b}} = {{ a+b }}
@@ -11632,9 +12627,13 @@ function getNgAttribute(element, ngAttr) {
</file>
</example>
*
+ * @example
+ *
+ * ### With `ngStrictDi`
+ *
* Using `ngStrictDi`, you would see something like this:
*
- <example ng-app-included="true">
+ <example ng-app-included="true" name="strict-di">
<file name="index.html">
<div ng-app="ngAppStrictDemo" ng-strict-di>
<div ng-controller="GoodController1">
@@ -11683,7 +12682,7 @@ function getNgAttribute(element, ngAttr) {
}])
.controller('GoodController2', GoodController2);
function GoodController2($scope) {
- $scope.name = "World";
+ $scope.name = 'World';
}
GoodController2.$inject = ['$scope'];
</file>
@@ -11733,7 +12732,12 @@ function angularInit(element, bootstrap) {
}
});
if (appElement) {
- config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
+ if (!isAutoBootstrapAllowed) {
+ window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
+ 'an extension, document.location.href does not match.');
+ return;
+ }
+ config.strictDi = getNgAttribute(appElement, 'strict-di') !== null;
bootstrap(appElement, module ? [module] : [], config);
}
}
@@ -11743,14 +12747,14 @@ function angularInit(element, bootstrap) {
* @name angular.bootstrap
* @module ng
* @description
- * Use this function to manually start up angular application.
+ * Use this function to manually start up AngularJS application.
*
* For more information, see the {@link guide/bootstrap Bootstrap guide}.
*
- * Angular will detect if it has been loaded into the browser more than once and only allow the
+ * AngularJS will detect if it has been loaded into the browser more than once and only allow the
* first loaded script to be bootstrapped and will report a warning to the browser console for
* each of the subsequent scripts. This prevents strange results in applications, where otherwise
- * multiple instances of Angular try to work on the DOM.
+ * multiple instances of AngularJS try to work on the DOM.
*
* <div class="alert alert-warning">
* **Note:** Protractor based end-to-end tests cannot use this function to bootstrap manually.
@@ -11784,7 +12788,7 @@ function angularInit(element, bootstrap) {
* </html>
* ```
*
- * @param {DOMElement} element DOM element which is the root of angular application.
+ * @param {DOMElement} element DOM element which is the root of AngularJS application.
* @param {Array<String|Function|Array>=} modules an array of modules to load into the application.
* Each item in the array should be the name of a predefined module or a (DI annotated)
* function that will be invoked by the injector as a `config` block.
@@ -11811,7 +12815,7 @@ function bootstrap(element, modules, config) {
// Encode angle brackets to prevent input from being sanitized to empty string #8683.
throw ngMinErr(
'btstrpd',
- "App already bootstrapped with this element '{0}'",
+ 'App already bootstrapped with this element \'{0}\'',
tag.replace(/</,'&lt;').replace(/>/,'&gt;'));
}
@@ -11884,9 +12888,9 @@ function reloadWithDebugInfo() {
* @name angular.getTestability
* @module ng
* @description
- * Get the testability service for the instance of Angular on the given
+ * Get the testability service for the instance of AngularJS on the given
* element.
- * @param {DOMElement} element DOM element which is the root of angular application.
+ * @param {DOMElement} element DOM element which is the root of AngularJS application.
*/
function getTestability(rootElement) {
var injector = angular.element(rootElement).injector();
@@ -11920,37 +12924,37 @@ function bindJQuery() {
window[jqName]; // use jQuery specified by `ngJq`
// Use jQuery if it exists with proper functionality, otherwise default to us.
- // Angular 1.2+ requires jQuery 1.7+ for on()/off() support.
- // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older
+ // AngularJS 1.2+ requires jQuery 1.7+ for on()/off() support.
+ // AngularJS 1.3+ technically requires at least jQuery 2.1+ but it may work with older
// versions. It will not work for sure with jQuery <1.7, though.
if (jQuery && jQuery.fn.on) {
jqLite = jQuery;
extend(jQuery.fn, {
scope: JQLitePrototype.scope,
isolateScope: JQLitePrototype.isolateScope,
- controller: JQLitePrototype.controller,
+ controller: /** @type {?} */ (JQLitePrototype).controller,
injector: JQLitePrototype.injector,
inheritedData: JQLitePrototype.inheritedData
});
-
- // All nodes removed from the DOM via various jQuery APIs like .remove()
- // are passed through jQuery.cleanData. Monkey-patch this method to fire
- // the $destroy event on all removed nodes.
- originalCleanData = jQuery.cleanData;
- jQuery.cleanData = function(elems) {
- var events;
- for (var i = 0, elem; (elem = elems[i]) != null; i++) {
- events = jQuery._data(elem, "events");
- if (events && events.$destroy) {
- jQuery(elem).triggerHandler('$destroy');
- }
- }
- originalCleanData(elems);
- };
} else {
jqLite = JQLite;
}
+ // All nodes removed from the DOM via various jqLite/jQuery APIs like .remove()
+ // are passed through jqLite/jQuery.cleanData. Monkey-patch this method to fire
+ // the $destroy event on all removed nodes.
+ originalCleanData = jqLite.cleanData;
+ jqLite.cleanData = function(elems) {
+ var events;
+ for (var i = 0, elem; (elem = elems[i]) != null; i++) {
+ events = (jqLite._data(elem) || {}).events;
+ if (events && events.$destroy) {
+ jqLite(elem).triggerHandler('$destroy');
+ }
+ }
+ originalCleanData(elems);
+ };
+
angular.element = jqLite;
// Prevent double-proxying.
@@ -11958,11 +12962,31 @@ function bindJQuery() {
}
/**
+ * @ngdoc function
+ * @name angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement
+ * @module ng
+ * @kind function
+ *
+ * @description
+ * Restores the pre-1.8 behavior of jqLite that turns XHTML-like strings like
+ * `<div /><span />` to `<div></div><span></span>` instead of `<div><span></span></div>`.
+ * The new behavior is a security fix. Thus, if you need to call this function, please try to adjust
+ * your code for this change and remove your use of this function as soon as possible.
+
+ * Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read the
+ * [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details
+ * about the workarounds.
+ */
+function UNSAFE_restoreLegacyJqLiteXHTMLReplacement() {
+ JQLite.legacyXHTMLReplacement = true;
+}
+
+/**
* throw error if the argument is falsy.
*/
function assertArg(arg, name, reason) {
if (!arg) {
- throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
+ throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required'));
}
return arg;
}
@@ -11984,7 +13008,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) {
*/
function assertNotHasOwnProperty(name, context) {
if (name === 'hasOwnProperty') {
- throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context);
+ throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
}
}
@@ -12054,6 +13078,27 @@ function createMap() {
return Object.create(null);
}
+function stringify(value) {
+ if (value == null) { // null || undefined
+ return '';
+ }
+ switch (typeof value) {
+ case 'string':
+ break;
+ case 'number':
+ value = '' + value;
+ break;
+ default:
+ if (hasCustomToString(value) && !isArray(value) && !isDate(value)) {
+ value = value.toString();
+ } else {
+ value = toJson(value);
+ }
+ }
+
+ return value;
+}
+
var NODE_TYPE_ELEMENT = 1;
var NODE_TYPE_ATTRIBUTE = 2;
var NODE_TYPE_TEXT = 3;
@@ -12067,7 +13112,7 @@ var NODE_TYPE_DOCUMENT_FRAGMENT = 11;
* @module ng
* @description
*
- * Interface for configuring angular {@link angular.module modules}.
+ * Interface for configuring AngularJS {@link angular.module modules}.
*/
function setupModuleLoader(window) {
@@ -12094,9 +13139,9 @@ function setupModuleLoader(window) {
* @module ng
* @description
*
- * The `angular.module` is a global place for creating, registering and retrieving Angular
+ * The `angular.module` is a global place for creating, registering and retrieving AngularJS
* modules.
- * All modules (angular core or 3rd party) that should be available to an application must be
+ * All modules (AngularJS core or 3rd party) that should be available to an application must be
* registered using this mechanism.
*
* Passing one argument retrieves an existing {@link angular.Module},
@@ -12140,6 +13185,9 @@ function setupModuleLoader(window) {
* @returns {angular.Module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
+
+ var info = {};
+
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
@@ -12152,9 +13200,9 @@ function setupModuleLoader(window) {
}
return ensure(modules, name, function() {
if (!requires) {
- throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
- "the module name or forgot to load it. If registering a module ensure that you " +
- "specify the dependencies as the second argument.", name);
+ throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' +
+ 'the module name or forgot to load it. If registering a module ensure that you ' +
+ 'specify the dependencies as the second argument.', name);
}
/** @type {!Array.<Array.<*>>} */
@@ -12176,6 +13224,45 @@ function setupModuleLoader(window) {
_runBlocks: runBlocks,
/**
+ * @ngdoc method
+ * @name angular.Module#info
+ * @module ng
+ *
+ * @param {Object=} info Information about the module
+ * @returns {Object|Module} The current info object for this module if called as a getter,
+ * or `this` if called as a setter.
+ *
+ * @description
+ * Read and write custom information about this module.
+ * For example you could put the version of the module in here.
+ *
+ * ```js
+ * angular.module('myModule', []).info({ version: '1.0.0' });
+ * ```
+ *
+ * The version could then be read back out by accessing the module elsewhere:
+ *
+ * ```
+ * var version = angular.module('myModule').info().version;
+ * ```
+ *
+ * You can also retrieve this information during runtime via the
+ * {@link $injector#modules `$injector.modules`} property:
+ *
+ * ```js
+ * var version = $injector.modules['myModule'].info().version;
+ * ```
+ */
+ info: function(value) {
+ if (isDefined(value)) {
+ if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value');
+ info = value;
+ return this;
+ }
+ return info;
+ },
+
+ /**
* @ngdoc property
* @name angular.Module#requires
* @module ng
@@ -12264,7 +13351,7 @@ function setupModuleLoader(window) {
* @description
* See {@link auto.$provide#decorator $provide.decorator()}.
*/
- decorator: invokeLaterAndSetModuleName('$provide', 'decorator'),
+ decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks),
/**
* @ngdoc method
@@ -12304,13 +13391,13 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#filter
* @module ng
- * @param {string} name Filter name - this must be a valid angular expression identifier
+ * @param {string} name Filter name - this must be a valid AngularJS expression identifier
* @param {Function} filterFactory Factory function for creating new instance of filter.
* @description
* See {@link ng.$filterProvider#register $filterProvider.register()}.
*
* <div class="alert alert-warning">
- * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
* Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
@@ -12347,7 +13434,8 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#component
* @module ng
- * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
+ * @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`),
+ * or an object map of components where the keys are the names and the values are the component definition objects.
* @param {Object} options Component definition object (a simplified
* {@link ng.$compile#directive-definition-object directive definition object})
*
@@ -12363,7 +13451,13 @@ function setupModuleLoader(window) {
* @param {Function} configFn Execute this function on module load. Useful for service
* configuration.
* @description
- * Use this method to register work which needs to be performed on module loading.
+ * Use this method to configure services by injecting their
+ * {@link angular.Module#provider `providers`}, e.g. for adding routes to the
+ * {@link ngRoute.$routeProvider $routeProvider}.
+ *
+ * Note that you can only inject {@link angular.Module#provider `providers`} and
+ * {@link angular.Module#constant `constants`} into this function.
+ *
* For more about how to configure services, see
* {@link providers#provider-recipe Provider Recipe}.
*/
@@ -12410,10 +13504,11 @@ function setupModuleLoader(window) {
* @param {string} method
* @returns {angular.Module}
*/
- function invokeLaterAndSetModuleName(provider, method) {
+ function invokeLaterAndSetModuleName(provider, method, queue) {
+ if (!queue) queue = invokeQueue;
return function(recipeName, factoryFunction) {
if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
- invokeQueue.push([provider, method, arguments]);
+ queue.push([provider, method, arguments]);
return moduleInstance;
};
}
@@ -12450,11 +13545,19 @@ function shallowCopy(src, dst) {
return dst || src;
}
-/* global toDebugString: true */
+/* exported toDebugString */
-function serializeObject(obj) {
+function serializeObject(obj, maxDepth) {
var seen = [];
+ // There is no direct way to stringify object until reaching a specific depth
+ // and a very deep object can cause a performance issue, so we copy the object
+ // based on this specific depth and then stringify it.
+ if (isValidObjectMaxDepth(maxDepth)) {
+ // This file is also included in `angular-loader`, so `copy()` might not always be available in
+ // the closure. Therefore, it is lazily retrieved as `angular.copy()` when needed.
+ obj = angular.copy(obj, null, maxDepth);
+ }
return JSON.stringify(obj, function(key, val) {
val = toJsonReplacer(key, val);
if (isObject(val)) {
@@ -12467,13 +13570,13 @@ function serializeObject(obj) {
});
}
-function toDebugString(obj) {
+function toDebugString(obj, maxDepth) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (isUndefined(obj)) {
return 'undefined';
} else if (typeof obj !== 'string') {
- return serializeObject(obj);
+ return serializeObject(obj, maxDepth);
}
return obj;
}
@@ -12485,11 +13588,10 @@ function toDebugString(obj) {
htmlAnchorDirective,
inputDirective,
- inputDirective,
+ hiddenInputBrowserCacheDirective,
formDirective,
scriptDirective,
selectDirective,
- styleDirective,
optionDirective,
ngBindDirective,
ngBindHtmlDirective,
@@ -12507,6 +13609,7 @@ function toDebugString(obj) {
ngInitDirective,
ngNonBindableDirective,
ngPluralizeDirective,
+ ngRefDirective,
ngRepeatDirective,
ngShowDirective,
ngStyleDirective,
@@ -12543,12 +13646,13 @@ function toDebugString(obj) {
$ControllerProvider,
$DateProvider,
$DocumentProvider,
+ $$IsDocumentHiddenProvider,
$ExceptionHandlerProvider,
$FilterProvider,
$$ForceReflowProvider,
$InterpolateProvider,
+ $$IntervalFactoryProvider,
$IntervalProvider,
- $$HashMapProvider,
$HttpProvider,
$HttpParamSerializerProvider,
$HttpParamSerializerJQLikeProvider,
@@ -12557,6 +13661,7 @@ function toDebugString(obj) {
$jsonpCallbacksProvider,
$LocationProvider,
$LogProvider,
+ $$MapProvider,
$ParseProvider,
$RootScopeProvider,
$QProvider,
@@ -12565,6 +13670,7 @@ function toDebugString(obj) {
$SceProvider,
$SceDelegateProvider,
$SnifferProvider,
+ $$TaskTrackerFactoryProvider,
$TemplateCacheProvider,
$TemplateRequestProvider,
$$TestabilityProvider,
@@ -12592,16 +13698,19 @@ function toDebugString(obj) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.5.8', // all of these placeholder strings will be replaced by grunt's
- major: 1, // package task
- minor: 5,
- dot: 8,
- codeName: 'arbitrary-fallbacks'
+ // These placeholder strings will be replaced by grunt's `build` task.
+ // They need to be double- or single-quoted.
+ full: '1.8.2',
+ major: '1',
+ minor: '8',
+ dot: '2',
+ codeName: 'meteoric-mining'
};
function publishExternalAPI(angular) {
extend(angular, {
+ 'errorHandlingConfig': errorHandlingConfig,
'bootstrap': bootstrap,
'copy': copy,
'extend': extend,
@@ -12625,13 +13734,17 @@ function publishExternalAPI(angular) {
'isArray': isArray,
'version': version,
'isDate': isDate,
- 'lowercase': lowercase,
- 'uppercase': uppercase,
'callbacks': {$$counter: 0},
'getTestability': getTestability,
+ 'reloadWithDebugInfo': reloadWithDebugInfo,
+ 'UNSAFE_restoreLegacyJqLiteXHTMLReplacement': UNSAFE_restoreLegacyJqLiteXHTMLReplacement,
'$$minErr': minErr,
'$$csp': csp,
- 'reloadWithDebugInfo': reloadWithDebugInfo
+ '$$encodeUriSegment': encodeUriSegment,
+ '$$encodeUriQuery': encodeUriQuery,
+ '$$lowercase': lowercase,
+ '$$stringify': stringify,
+ '$$uppercase': uppercase
});
angularModule = setupModuleLoader(window);
@@ -12650,7 +13763,6 @@ function publishExternalAPI(angular) {
form: formDirective,
script: scriptDirective,
select: selectDirective,
- style: styleDirective,
option: optionDirective,
ngBind: ngBindDirective,
ngBindHtml: ngBindHtmlDirective,
@@ -12667,6 +13779,7 @@ function publishExternalAPI(angular) {
ngInit: ngInitDirective,
ngNonBindable: ngNonBindableDirective,
ngPluralize: ngPluralizeDirective,
+ ngRef: ngRefDirective,
ngRepeat: ngRepeatDirective,
ngShow: ngShowDirective,
ngStyle: ngStyleDirective,
@@ -12690,7 +13803,8 @@ function publishExternalAPI(angular) {
ngModelOptions: ngModelOptionsDirective
}).
directive({
- ngInclude: ngIncludeFillContentDirective
+ ngInclude: ngIncludeFillContentDirective,
+ input: hiddenInputBrowserCacheDirective
}).
directive(ngAttributeAliasDirectives).
directive(ngEventDirectives);
@@ -12706,11 +13820,13 @@ function publishExternalAPI(angular) {
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
$document: $DocumentProvider,
+ $$isDocumentHidden: $$IsDocumentHiddenProvider,
$exceptionHandler: $ExceptionHandlerProvider,
$filter: $FilterProvider,
$$forceReflow: $$ForceReflowProvider,
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
+ $$intervalFactory: $$IntervalFactoryProvider,
$http: $HttpProvider,
$httpParamSerializer: $HttpParamSerializerProvider,
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
@@ -12726,6 +13842,7 @@ function publishExternalAPI(angular) {
$sce: $SceProvider,
$sceDelegate: $SceDelegateProvider,
$sniffer: $SnifferProvider,
+ $$taskTrackerFactory: $$TaskTrackerFactoryProvider,
$templateCache: $TemplateCacheProvider,
$templateRequest: $TemplateRequestProvider,
$$testability: $$TestabilityProvider,
@@ -12733,7 +13850,7 @@ function publishExternalAPI(angular) {
$window: $WindowProvider,
$$rAF: $$RAFProvider,
$$jqLite: $$jqLiteProvider,
- $$HashMap: $$HashMapProvider,
+ $$Map: $$MapProvider,
$$cookieReader: $$CookieReaderProvider
});
}
@@ -12751,11 +13868,10 @@ function publishExternalAPI(angular) {
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-/* global JQLitePrototype: true,
- addEventListenerFn: true,
- removeEventListenerFn: true,
+/* global
+ JQLitePrototype: true,
BOOLEAN_ATTR: true,
- ALIASED_ATTR: true,
+ ALIASED_ATTR: true
*/
//////////////////////////////////
@@ -12773,31 +13889,32 @@ function publishExternalAPI(angular) {
*
* If jQuery is available, `angular.element` is an alias for the
* [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element`
- * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or **jqLite**.
+ * delegates to AngularJS's built-in subset of jQuery, called "jQuery lite" or **jqLite**.
*
* jqLite is a tiny, API-compatible subset of jQuery that allows
- * Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most
+ * AngularJS to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most
* commonly needed functionality with the goal of having a very small footprint.
*
* To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the
* {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a
* specific version of jQuery if multiple versions exist on the page.
*
- * <div class="alert alert-info">**Note:** All element references in Angular are always wrapped with jQuery or
+ * <div class="alert alert-info">**Note:** All element references in AngularJS are always wrapped with jQuery or
* jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.</div>
*
* <div class="alert alert-warning">**Note:** Keep in mind that this function will not find elements
* by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)`
* or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.</div>
*
- * ## Angular's jqLite
+ * ## AngularJS's jqLite
* jqLite provides only the following jQuery methods:
*
* - [`addClass()`](http://api.jquery.com/addClass/) - Does not support a function as first argument
* - [`after()`](http://api.jquery.com/after/)
- * - [`append()`](http://api.jquery.com/append/)
+ * - [`append()`](http://api.jquery.com/append/) - Contrary to jQuery, this doesn't clone elements
+ * so will not work correctly when invoked on a jqLite object containing more than one DOM node
* - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters
- * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
+ * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData
* - [`children()`](http://api.jquery.com/children/) - Does not support selectors
* - [`clone()`](http://api.jquery.com/clone/)
* - [`contents()`](http://api.jquery.com/contents/)
@@ -12817,21 +13934,31 @@ function publishExternalAPI(angular) {
* - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors
* - [`prepend()`](http://api.jquery.com/prepend/)
* - [`prop()`](http://api.jquery.com/prop/)
- * - [`ready()`](http://api.jquery.com/ready/)
+ * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`)
* - [`remove()`](http://api.jquery.com/remove/)
- * - [`removeAttr()`](http://api.jquery.com/removeAttr/)
+ * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes
* - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument
* - [`removeData()`](http://api.jquery.com/removeData/)
* - [`replaceWith()`](http://api.jquery.com/replaceWith/)
* - [`text()`](http://api.jquery.com/text/)
* - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument
* - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers
- * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter
+ * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter
* - [`val()`](http://api.jquery.com/val/)
* - [`wrap()`](http://api.jquery.com/wrap/)
*
+ * jqLite also provides a method restoring pre-1.8 insecure treatment of XHTML-like tags.
+ * This legacy behavior turns input like `<div /><span />` to `<div></div><span></span>`
+ * instead of `<div><span></span></div>` like version 1.8 & newer do. To restore it, invoke:
+ * ```js
+ * angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement();
+ * ```
+ * Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read the
+ * [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details
+ * about the workarounds.
+ *
* ## jQuery/jqLite Extras
- * Angular also provides the following additional methods and events to both jQuery and jqLite:
+ * AngularJS also provides the following additional methods and events to both jQuery and jqLite:
*
* ### Events
* - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event
@@ -12864,13 +13991,7 @@ function publishExternalAPI(angular) {
JQLite.expando = 'ng339';
var jqCache = JQLite.cache = {},
- jqId = 1,
- addEventListenerFn = function(element, type, fn) {
- element.addEventListener(type, fn, false);
- },
- removeEventListenerFn = function(element, type, fn) {
- element.removeEventListener(type, fn, false);
- };
+ jqId = 1;
/*
* !!! This is an undocumented "private" function !!!
@@ -12883,22 +14004,31 @@ JQLite._data = function(node) {
function jqNextId() { return ++jqId; }
-var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
-var MOZ_HACK_REGEXP = /^moz([A-Z])/;
-var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"};
+var DASH_LOWERCASE_REGEXP = /-([a-z])/g;
+var MS_HACK_REGEXP = /^-ms-/;
+var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' };
var jqLiteMinErr = minErr('jqLite');
/**
- * Converts snake_case to camelCase.
- * Also there is special case for Moz prefix starting with upper case letter.
+ * Converts kebab-case to camelCase.
+ * There is also a special case for the ms prefix starting with a lowercase letter.
+ * @param name Name to normalize
+ */
+function cssKebabToCamel(name) {
+ return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-'));
+}
+
+function fnCamelCaseReplace(all, letter) {
+ return letter.toUpperCase();
+}
+
+/**
+ * Converts kebab-case to camelCase.
* @param name Name to normalize
*/
-function camelCase(name) {
- return name.
- replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
- return offset ? letter.toUpperCase() : letter;
- }).
- replace(MOZ_HACK_REGEXP, 'Moz$1');
+function kebabToCamel(name) {
+ return name
+ .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace);
}
var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;
@@ -12906,20 +14036,36 @@ var HTML_REGEXP = /<|&#?\w+;/;
var TAG_NAME_REGEXP = /<([\w:-]+)/;
var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi;
+// Table parts need to be wrapped with `<table>` or they're
+// stripped to their contents when put in a div.
+// XHTML parsers do not magically insert elements in the
+// same way that tag soup parsers do, so we cannot shorten
+// this by omitting <tbody> or other required elements.
var wrapMap = {
- 'option': [1, '<select multiple="multiple">', '</select>'],
-
- 'thead': [1, '<table>', '</table>'],
- 'col': [2, '<table><colgroup>', '</colgroup></table>'],
- 'tr': [2, '<table><tbody>', '</tbody></table>'],
- 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
- '_default': [0, "", ""]
+ thead: ['table'],
+ col: ['colgroup', 'table'],
+ tr: ['tbody', 'table'],
+ td: ['tr', 'tbody', 'table']
};
-wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
+// Support: IE <10 only
+// IE 9 requires an option wrapper & it needs to have the whole table structure
+// set up in advance; assigning `"<td></td>"` to `tr.innerHTML` doesn't work, etc.
+var wrapMapIE9 = {
+ option: [1, '<select multiple="multiple">', '</select>'],
+ _default: [0, '', '']
+};
+
+for (var key in wrapMap) {
+ var wrapMapValueClosing = wrapMap[key];
+ var wrapMapValue = wrapMapValueClosing.slice().reverse();
+ wrapMapIE9[key] = [wrapMapValue.length, '<' + wrapMapValue.join('><') + '>', '</' + wrapMapValueClosing.join('></') + '>'];
+}
+
+wrapMapIE9.optgroup = wrapMapIE9.option;
function jqLiteIsTextNode(html) {
return !HTML_REGEXP.test(html);
@@ -12939,14 +14085,8 @@ function jqLiteHasData(node) {
return false;
}
-function jqLiteCleanData(nodes) {
- for (var i = 0, ii = nodes.length; i < ii; i++) {
- jqLiteRemoveData(nodes[i]);
- }
-}
-
function jqLiteBuildFragment(html, context) {
- var tmp, tag, wrap,
+ var tmp, tag, wrap, finalHtml,
fragment = context.createDocumentFragment(),
nodes = [], i;
@@ -12955,26 +14095,43 @@ function jqLiteBuildFragment(html, context) {
nodes.push(context.createTextNode(html));
} else {
// Convert html into DOM nodes
- tmp = fragment.appendChild(context.createElement("div"));
- tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase();
- wrap = wrapMap[tag] || wrapMap._default;
- tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2];
+ tmp = fragment.appendChild(context.createElement('div'));
+ tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase();
+ finalHtml = JQLite.legacyXHTMLReplacement ?
+ html.replace(XHTML_TAG_REGEXP, '<$1></$2>') :
+ html;
- // Descend through wrappers to the right content
- i = wrap[0];
- while (i--) {
- tmp = tmp.lastChild;
+ if (msie < 10) {
+ wrap = wrapMapIE9[tag] || wrapMapIE9._default;
+ tmp.innerHTML = wrap[1] + finalHtml + wrap[2];
+
+ // Descend through wrappers to the right content
+ i = wrap[0];
+ while (i--) {
+ tmp = tmp.firstChild;
+ }
+ } else {
+ wrap = wrapMap[tag] || [];
+
+ // Create wrappers & descend into them
+ i = wrap.length;
+ while (--i > -1) {
+ tmp.appendChild(window.document.createElement(wrap[i]));
+ tmp = tmp.firstChild;
+ }
+
+ tmp.innerHTML = finalHtml;
}
nodes = concat(nodes, tmp.childNodes);
tmp = fragment.firstChild;
- tmp.textContent = "";
+ tmp.textContent = '';
}
// Remove wrapper from fragment
- fragment.textContent = "";
- fragment.innerHTML = ""; // Clear inner HTML
+ fragment.textContent = '';
+ fragment.innerHTML = ''; // Clear inner HTML
forEach(nodes, function(node) {
fragment.appendChild(node);
});
@@ -13009,10 +14166,9 @@ function jqLiteWrapNode(node, wrapper) {
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
-var jqLiteContains = window.Node.prototype.contains || function(arg) {
- // jshint bitwise: false
+var jqLiteContains = window.Node.prototype.contains || /** @this */ function(arg) {
+ // eslint-disable-next-line no-bitwise
return !!(this.compareDocumentPosition(arg) & 16);
- // jshint bitwise: true
};
/////////////////////////////////////////////
@@ -13028,7 +14184,7 @@ function JQLite(element) {
argIsString = true;
}
if (!(this instanceof JQLite)) {
- if (argIsString && element.charAt(0) != '<') {
+ if (argIsString && element.charAt(0) !== '<') {
throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element');
}
return new JQLite(element);
@@ -13036,6 +14192,8 @@ function JQLite(element) {
if (argIsString) {
jqLiteAddNodes(this, jqLiteParseHTML(element));
+ } else if (isFunction(element)) {
+ jqLiteReady(element);
} else {
jqLiteAddNodes(this, element);
}
@@ -13046,13 +14204,32 @@ function jqLiteClone(element) {
}
function jqLiteDealoc(element, onlyDescendants) {
- if (!onlyDescendants) jqLiteRemoveData(element);
+ if (!onlyDescendants && jqLiteAcceptsData(element)) jqLite.cleanData([element]);
if (element.querySelectorAll) {
- var descendants = element.querySelectorAll('*');
- for (var i = 0, l = descendants.length; i < l; i++) {
- jqLiteRemoveData(descendants[i]);
- }
+ jqLite.cleanData(element.querySelectorAll('*'));
+ }
+}
+
+function isEmptyObject(obj) {
+ var name;
+
+ for (name in obj) {
+ return false;
+ }
+ return true;
+}
+
+function removeIfEmptyData(element) {
+ var expandoId = element.ng339;
+ var expandoStore = expandoId && jqCache[expandoId];
+
+ var events = expandoStore && expandoStore.events;
+ var data = expandoStore && expandoStore.data;
+
+ if ((!data || isEmptyObject(data)) && (!events || isEmptyObject(events))) {
+ delete jqCache[expandoId];
+ element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it
}
}
@@ -13068,7 +14245,7 @@ function jqLiteOff(element, type, fn, unsupported) {
if (!type) {
for (type in events) {
if (type !== '$destroy') {
- removeEventListenerFn(element, type, handle);
+ element.removeEventListener(type, handle);
}
delete events[type];
}
@@ -13080,7 +14257,7 @@ function jqLiteOff(element, type, fn, unsupported) {
arrayRemove(listenerFns || [], fn);
}
if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
- removeEventListenerFn(element, type, handle);
+ element.removeEventListener(type, handle);
delete events[type];
}
};
@@ -13092,6 +14269,8 @@ function jqLiteOff(element, type, fn, unsupported) {
}
});
}
+
+ removeIfEmptyData(element);
}
function jqLiteRemoveData(element, name) {
@@ -13101,17 +14280,11 @@ function jqLiteRemoveData(element, name) {
if (expandoStore) {
if (name) {
delete expandoStore.data[name];
- return;
+ } else {
+ expandoStore.data = {};
}
- if (expandoStore.handle) {
- if (expandoStore.events.$destroy) {
- expandoStore.handle({}, '$destroy');
- }
- jqLiteOff(element);
- }
- delete jqCache[expandoId];
- element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it
+ removeIfEmptyData(element);
}
}
@@ -13131,6 +14304,7 @@ function jqLiteExpandoStore(element, createIfNecessary) {
function jqLiteData(element, key, value) {
if (jqLiteAcceptsData(element)) {
+ var prop;
var isSimpleSetter = isDefined(value);
var isSimpleGetter = !isSimpleSetter && key && !isObject(key);
@@ -13139,16 +14313,18 @@ function jqLiteData(element, key, value) {
var data = expandoStore && expandoStore.data;
if (isSimpleSetter) { // data('key', value)
- data[key] = value;
+ data[kebabToCamel(key)] = value;
} else {
if (massGetter) { // data()
return data;
} else {
if (isSimpleGetter) { // data('key')
// don't force creation of expandoStore if it doesn't exist yet
- return data && data[key];
+ return data && data[kebabToCamel(key)];
} else { // mass-setter: data({key1: val1, key2: val2})
- extend(data, key);
+ for (prop in key) {
+ data[kebabToCamel(prop)] = key[prop];
+ }
}
}
}
@@ -13157,35 +14333,43 @@ function jqLiteData(element, key, value) {
function jqLiteHasClass(element, selector) {
if (!element.getAttribute) return false;
- return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " ").
- indexOf(" " + selector + " ") > -1);
+ return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' ').
+ indexOf(' ' + selector + ' ') > -1);
}
function jqLiteRemoveClass(element, cssClasses) {
if (cssClasses && element.setAttribute) {
+ var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ')
+ .replace(/[\n\t]/g, ' ');
+ var newClasses = existingClasses;
+
forEach(cssClasses.split(' '), function(cssClass) {
- element.setAttribute('class', trim(
- (" " + (element.getAttribute('class') || '') + " ")
- .replace(/[\n\t]/g, " ")
- .replace(" " + trim(cssClass) + " ", " "))
- );
+ cssClass = trim(cssClass);
+ newClasses = newClasses.replace(' ' + cssClass + ' ', ' ');
});
+
+ if (newClasses !== existingClasses) {
+ element.setAttribute('class', trim(newClasses));
+ }
}
}
function jqLiteAddClass(element, cssClasses) {
if (cssClasses && element.setAttribute) {
var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ')
- .replace(/[\n\t]/g, " ");
+ .replace(/[\n\t]/g, ' ');
+ var newClasses = existingClasses;
forEach(cssClasses.split(' '), function(cssClass) {
cssClass = trim(cssClass);
- if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) {
- existingClasses += cssClass + ' ';
+ if (newClasses.indexOf(' ' + cssClass + ' ') === -1) {
+ newClasses += cssClass + ' ';
}
});
- element.setAttribute('class', trim(existingClasses));
+ if (newClasses !== existingClasses) {
+ element.setAttribute('class', trim(newClasses));
+ }
}
}
@@ -13223,7 +14407,7 @@ function jqLiteController(element, name) {
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 == NODE_TYPE_DOCUMENT) {
+ if (element.nodeType === NODE_TYPE_DOCUMENT) {
element = element.documentElement;
}
var names = isArray(name) ? name : [name];
@@ -13267,30 +14451,32 @@ function jqLiteDocumentLoaded(action, win) {
}
}
+function jqLiteReady(fn) {
+ function trigger() {
+ window.document.removeEventListener('DOMContentLoaded', trigger);
+ window.removeEventListener('load', trigger);
+ fn();
+ }
+
+ // check if document is already loaded
+ if (window.document.readyState === 'complete') {
+ window.setTimeout(fn);
+ } else {
+ // We can not use jqLite since we are not done loading and jQuery could be loaded later.
+
+ // Works for modern browsers and IE9
+ window.document.addEventListener('DOMContentLoaded', trigger);
+
+ // Fallback to window.onload for others
+ window.addEventListener('load', trigger);
+ }
+}
+
//////////////////////////////////////////
// Functions which are declared directly.
//////////////////////////////////////////
var JQLitePrototype = JQLite.prototype = {
- ready: function(fn) {
- var fired = false;
-
- function trigger() {
- if (fired) return;
- fired = true;
- fn();
- }
-
- // check if document is already loaded
- if (window.document.readyState === 'complete') {
- window.setTimeout(trigger);
- } else {
- this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9
- // we can not use jqLite since we are not done loading and jQuery could be loaded later.
- // jshint -W064
- JQLite(window).on('load', trigger); // fallback to window.onload for others
- // jshint +W064
- }
- },
+ ready: jqLiteReady,
toString: function() {
var value = [];
forEach(this, function(e) { value.push('' + e);});
@@ -13325,7 +14511,8 @@ var ALIASED_ATTR = {
'ngMaxlength': 'maxlength',
'ngMin': 'min',
'ngMax': 'max',
- 'ngPattern': 'pattern'
+ 'ngPattern': 'pattern',
+ 'ngStep': 'step'
};
function getBooleanAttrName(element, name) {
@@ -13344,7 +14531,12 @@ forEach({
data: jqLiteData,
removeData: jqLiteRemoveData,
hasData: jqLiteHasData,
- cleanData: jqLiteCleanData
+ cleanData: function jqLiteCleanData(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ jqLiteRemoveData(nodes[i]);
+ jqLiteOff(nodes[i]);
+ }
+ }
}, function(fn, name) {
JQLite[name] = fn;
});
@@ -13376,7 +14568,7 @@ forEach({
hasClass: jqLiteHasClass,
css: function(element, name, value) {
- name = camelCase(name);
+ name = cssKebabToCamel(name);
if (isDefined(value)) {
element.style[name] = value;
@@ -13386,33 +14578,33 @@ forEach({
},
attr: function(element, name, value) {
+ var ret;
var nodeType = element.nodeType;
- if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) {
+ if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT ||
+ !element.getAttribute) {
return;
}
+
var lowercasedName = lowercase(name);
- if (BOOLEAN_ATTR[lowercasedName]) {
- if (isDefined(value)) {
- if (!!value) {
- element[name] = true;
- element.setAttribute(name, lowercasedName);
- } else {
- element[name] = false;
- element.removeAttribute(lowercasedName);
- }
+ var isBooleanAttr = BOOLEAN_ATTR[lowercasedName];
+
+ if (isDefined(value)) {
+ // setter
+
+ if (value === null || (value === false && isBooleanAttr)) {
+ element.removeAttribute(name);
} else {
- return (element[name] ||
- (element.attributes.getNamedItem(name) || noop).specified)
- ? lowercasedName
- : undefined;
- }
- } else if (isDefined(value)) {
- element.setAttribute(name, value);
- } else if (element.getAttribute) {
- // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code
- // some elements (e.g. Document) don't have get attribute, so return undefined
- var ret = element.getAttribute(name, 2);
- // normalize non-existing attributes to undefined (as jQuery)
+ element.setAttribute(name, isBooleanAttr ? lowercasedName : value);
+ }
+ } else {
+ // getter
+
+ ret = element.getAttribute(name);
+
+ if (isBooleanAttr && ret !== null) {
+ ret = lowercasedName;
+ }
+ // Normalize non-existing attributes to undefined (as jQuery).
return ret === null ? undefined : ret;
}
},
@@ -13447,7 +14639,7 @@ forEach({
result.push(option.value || option.text);
}
});
- return result.length === 0 ? null : result;
+ return result;
}
return element.value;
}
@@ -13475,7 +14667,7 @@ forEach({
// in a way that survives minification.
// jqLiteEmpty takes no arguments but is a setter.
if (fn !== jqLiteEmpty &&
- (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
+ (isUndefined((fn.length === 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
if (isObject(arg1)) {
// we are a write, but the object properties are the key/values
@@ -13617,7 +14809,7 @@ forEach({
eventFns = events[type] = [];
eventFns.specialHandlerWrapper = specialHandlerWrapper;
if (type !== '$destroy' && !noEventListener) {
- addEventListenerFn(element, type, handle);
+ element.addEventListener(type, handle);
}
}
@@ -13710,12 +14902,15 @@ forEach({
after: function(element, newElement) {
var index = element, parent = element.parentNode;
- newElement = new JQLite(newElement);
- for (var i = 0, ii = newElement.length; i < ii; i++) {
- var node = newElement[i];
- parent.insertBefore(node, index.nextSibling);
- index = node;
+ if (parent) {
+ newElement = new JQLite(newElement);
+
+ for (var i = 0, ii = newElement.length; i < ii; i++) {
+ var node = newElement[i];
+ parent.insertBefore(node, index.nextSibling);
+ index = node;
+ }
}
},
@@ -13809,14 +15004,15 @@ forEach({
}
return isDefined(value) ? value : this;
};
-
- // bind legacy bind/unbind to on/off
- JQLite.prototype.bind = JQLite.prototype.on;
- JQLite.prototype.unbind = JQLite.prototype.off;
});
+// bind legacy bind/unbind to on/off
+JQLite.prototype.bind = JQLite.prototype.on;
+JQLite.prototype.unbind = JQLite.prototype.off;
+
// Provider for private $$jqLite service
+/** @this */
function $$jqLiteProvider() {
this.$get = function $$jqLite() {
return extend(JQLite, {
@@ -13859,7 +15055,7 @@ function hashKey(obj, nextUidFn) {
}
var objType = typeof obj;
- if (objType == 'function' || (objType == 'object' && obj !== null)) {
+ if (objType === 'function' || (objType === 'object' && obj !== null)) {
key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)();
} else {
key = objType + ':' + obj;
@@ -13868,50 +15064,74 @@ function hashKey(obj, nextUidFn) {
return key;
}
-/**
- * HashMap which can use objects as keys
- */
-function HashMap(array, isolatedUid) {
- if (isolatedUid) {
- var uid = 0;
- this.nextUid = function() {
- return ++uid;
- };
- }
- forEach(array, this.put, this);
-}
-HashMap.prototype = {
- /**
- * Store key value pair
- * @param key key to store can be any type
- * @param value value to store can be any type
- */
- put: function(key, value) {
- this[hashKey(key, this.nextUid)] = value;
+// A minimal ES2015 Map implementation.
+// Should be bug/feature equivalent to the native implementations of supported browsers
+// (for the features required in Angular).
+// See https://kangax.github.io/compat-table/es6/#test-Map
+var nanKey = Object.create(null);
+function NgMapShim() {
+ this._keys = [];
+ this._values = [];
+ this._lastKey = NaN;
+ this._lastIndex = -1;
+}
+NgMapShim.prototype = {
+ _idx: function(key) {
+ if (key !== this._lastKey) {
+ this._lastKey = key;
+ this._lastIndex = this._keys.indexOf(key);
+ }
+ return this._lastIndex;
+ },
+ _transformKey: function(key) {
+ return isNumberNaN(key) ? nanKey : key;
},
-
- /**
- * @param key
- * @returns {Object} the value for the key
- */
get: function(key) {
- return this[hashKey(key, this.nextUid)];
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx !== -1) {
+ return this._values[idx];
+ }
},
+ has: function(key) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ return idx !== -1;
+ },
+ set: function(key, value) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx === -1) {
+ idx = this._lastIndex = this._keys.length;
+ }
+ this._keys[idx] = key;
+ this._values[idx] = value;
- /**
- * Remove the key/value pair
- * @param key
- */
- remove: function(key) {
- var value = this[key = hashKey(key, this.nextUid)];
- delete this[key];
- return value;
+ // Support: IE11
+ // Do not `return this` to simulate the partial IE11 implementation
+ },
+ delete: function(key) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx === -1) {
+ return false;
+ }
+ this._keys.splice(idx, 1);
+ this._values.splice(idx, 1);
+ this._lastKey = NaN;
+ this._lastIndex = -1;
+ return true;
}
};
-var $$HashMapProvider = [function() {
+// For now, always use `NgMapShim`, even if `window.Map` is available. Some native implementations
+// are still buggy (often in subtle ways) and can cause hard-to-debug failures. When native `Map`
+// implementations get more stable, we can reconsider switching to `window.Map` (when available).
+var NgMap = NgMapShim;
+
+var $$MapProvider = [/** @this */function() {
this.$get = [function() {
- return HashMap;
+ return NgMap;
}];
}];
@@ -13945,8 +15165,8 @@ var $$HashMapProvider = [function() {
* });
* ```
*
- * Sometimes you want to get access to the injector of a currently running Angular app
- * from outside Angular. Perhaps, you want to inject and compile some markup after the
+ * Sometimes you want to get access to the injector of a currently running AngularJS app
+ * from outside AngularJS. Perhaps, you want to inject and compile some markup after the
* application has been bootstrapped. You can do this using the extra `injector()` added
* to JQuery/jqLite elements. See {@link angular.element}.
*
@@ -13978,19 +15198,15 @@ var $$HashMapProvider = [function() {
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/
-var ARROW_ARG = /^([^\(]+?)=>/;
-var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
+var ARROW_ARG = /^([^(]+?)=>/;
+var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
function stringifyFn(fn) {
- // Support: Chrome 50-51 only
- // Creating a new string by adding `' '` at the end, to hack around some bug in Chrome v50/51
- // (See https://github.com/angular/angular.js/issues/14487.)
- // TODO (gkalpak): Remove workaround when Chrome v52 is released
- return Function.prototype.toString.call(fn) + ' ';
+ return Function.prototype.toString.call(fn);
}
function extractArgs(fn) {
@@ -14066,7 +15282,7 @@ function annotate(fn, strictDi, name) {
* })).toBe($injector);
* ```
*
- * # Injection Function Annotation
+ * ## Injection Function Annotation
*
* JavaScript does not have annotations, and annotations are needed for dependency injection. The
* following are all valid ways of annotating function with injection arguments and are equivalent.
@@ -14084,7 +15300,7 @@ function annotate(fn, strictDi, name) {
* $injector.invoke(['serviceA', function(serviceA){}]);
* ```
*
- * ## Inference
+ * ### Inference
*
* In JavaScript calling `toString()` on a function returns the function definition. The definition
* can then be parsed and the function arguments can be extracted. This method of discovering
@@ -14092,14 +15308,36 @@ function annotate(fn, strictDi, name) {
* *NOTE:* This does not work with minification, and obfuscation tools since these tools change the
* argument names.
*
- * ## `$inject` Annotation
+ * ### `$inject` Annotation
* By adding an `$inject` property onto a function the injection parameters can be specified.
*
- * ## Inline
+ * ### Inline
* As an array of injection names, where the last item in the array is the function to call.
*/
/**
+ * @ngdoc property
+ * @name $injector#modules
+ * @type {Object}
+ * @description
+ * A hash containing all the modules that have been loaded into the
+ * $injector.
+ *
+ * You can use this property to find out information about a module via the
+ * {@link angular.Module#info `myModule.info(...)`} method.
+ *
+ * For example:
+ *
+ * ```
+ * var info = $injector.modules['ngAnimate'].info();
+ * ```
+ *
+ * **Do not use this property to attempt to modify the modules after the application
+ * has been bootstrapped.**
+ */
+
+
+/**
* @ngdoc method
* @name $injector#get
*
@@ -14161,7 +15399,7 @@ function annotate(fn, strictDi, name) {
* function is invoked. There are three ways in which the function can be annotated with the needed
* dependencies.
*
- * # Argument names
+ * #### Argument names
*
* The simplest form is to extract the dependencies from the arguments of the function. This is done
* by converting the function into a string using `toString()` method and extracting the argument
@@ -14181,7 +15419,7 @@ function annotate(fn, strictDi, name) {
* This method does not work with code minification / obfuscation. For this reason the following
* annotation strategies are supported.
*
- * # The `$inject` property
+ * #### The `$inject` property
*
* If a function has an `$inject` property and its value is an array of strings, then the strings
* represent names of services to be injected into the function.
@@ -14197,7 +15435,7 @@ function annotate(fn, strictDi, name) {
* expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
* ```
*
- * # The array notation
+ * #### The array notation
*
* It is often desirable to inline Injected functions and that's when setting the `$inject` property
* is very inconvenient. In these situations using the array notation to specify the dependencies in
@@ -14234,8 +15472,45 @@ function annotate(fn, strictDi, name) {
*
* @returns {Array.<string>} The names of the services which the function requires.
*/
-
-
+/**
+ * @ngdoc method
+ * @name $injector#loadNewModules
+ *
+ * @description
+ *
+ * **This is a dangerous API, which you use at your own risk!**
+ *
+ * Add the specified modules to the current injector.
+ *
+ * This method will add each of the injectables to the injector and execute all of the config and run
+ * blocks for each module passed to the method.
+ *
+ * If a module has already been loaded into the injector then it will not be loaded again.
+ *
+ * * The application developer is responsible for loading the code containing the modules; and for
+ * ensuring that lazy scripts are not downloaded and executed more often that desired.
+ * * Previously compiled HTML will not be affected by newly loaded directives, filters and components.
+ * * Modules cannot be unloaded.
+ *
+ * You can use {@link $injector#modules `$injector.modules`} to check whether a module has been loaded
+ * into the injector, which may indicate whether the script has been executed already.
+ *
+ * @example
+ * Here is an example of loading a bundle of modules, with a utility method called `getScript`:
+ *
+ * ```javascript
+ * app.factory('loadModule', function($injector) {
+ * return function loadModule(moduleName, bundleUrl) {
+ * return getScript(bundleUrl).then(function() { $injector.loadNewModules([moduleName]); });
+ * };
+ * })
+ * ```
+ *
+ * @param {Array<String|Function|Array>=} mods an array of modules to load into the application.
+ * Each item in the array should be the name of a predefined module or a (DI annotated)
+ * function that will be invoked by the injector as a `config` block.
+ * See: {@link angular.module modules}
+ */
/**
@@ -14248,7 +15523,7 @@ function annotate(fn, strictDi, name) {
* with the {@link auto.$injector $injector}. Many of these functions are also exposed on
* {@link angular.Module}.
*
- * An Angular **service** is a singleton object created by a **service factory**. These **service
+ * An AngularJS **service** is a singleton object created by a **service factory**. These **service
* factories** are functions which, in turn, are created by a **service provider**.
* The **service providers** are constructor functions. When instantiated they must contain a
* property called `$get`, which holds the **service factory** function.
@@ -14300,6 +15575,9 @@ function annotate(fn, strictDi, name) {
* which lets you specify whether the {@link ng.$log $log} service will log debug messages to the
* console or not.
*
+ * It is possible to inject other providers into the provider function,
+ * but the injected provider must have been defined before the one that requires it.
+ *
* @param {string} name The name of the instance. NOTE: the provider will be available under `name +
'Provider'` key.
* @param {(Object|function())} provider If the provider is:
@@ -14476,7 +15754,7 @@ function annotate(fn, strictDi, name) {
*
* Value services are similar to constant services, except that they cannot be injected into a
* module configuration function (see {@link angular.Module#config}) but they can be overridden by
- * an Angular {@link auto.$provide#decorator decorator}.
+ * an AngularJS {@link auto.$provide#decorator decorator}.
*
* @param {string} name The name of the instance.
* @param {*} value The value.
@@ -14507,7 +15785,7 @@ function annotate(fn, strictDi, name) {
*
* But unlike {@link auto.$provide#value value}, a constant can be
* injected into a module configuration function (see {@link angular.Module#config}) and it cannot
- * be overridden by an Angular {@link auto.$provide#decorator decorator}.
+ * be overridden by an AngularJS {@link auto.$provide#decorator decorator}.
*
* @param {string} name The name of the constant.
* @param {*} value The constant value.
@@ -14565,7 +15843,7 @@ function createInjector(modulesToLoad, strictDi) {
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
- loadedModules = new HashMap([], true),
+ loadedModules = new NgMap(),
providerCache = {
$provide: {
provider: supportObject(provider),
@@ -14581,7 +15859,7 @@ function createInjector(modulesToLoad, strictDi) {
if (angular.isString(caller)) {
path.push(caller);
}
- throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
+ throw $injectorMinErr('unpr', 'Unknown provider: {0}', path.join(' <- '));
})),
instanceCache = {},
protoInstanceInjector =
@@ -14593,11 +15871,17 @@ function createInjector(modulesToLoad, strictDi) {
instanceInjector = protoInstanceInjector;
providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
+ instanceInjector.modules = providerInjector.modules = createMap();
var runBlocks = loadModules(modulesToLoad);
instanceInjector = protoInstanceInjector.get('$injector');
instanceInjector.strictDi = strictDi;
forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); });
+ instanceInjector.loadNewModules = function(mods) {
+ forEach(loadModules(mods), function(fn) { if (fn) instanceInjector.invoke(fn); });
+ };
+
+
return instanceInjector;
////////////////////////////////////
@@ -14620,16 +15904,16 @@ function createInjector(modulesToLoad, strictDi) {
provider_ = providerInjector.instantiate(provider_);
}
if (!provider_.$get) {
- throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
+ throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name);
}
- return providerCache[name + providerSuffix] = provider_;
+ return (providerCache[name + providerSuffix] = provider_);
}
function enforceReturnValue(name, factory) {
- return function enforcedReturnValue() {
+ return /** @this */ function enforcedReturnValue() {
var result = instanceInjector.invoke(factory, this);
if (isUndefined(result)) {
- throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name);
+ throw $injectorMinErr('undef', 'Provider \'{0}\' must return a value from $get factory method.', name);
}
return result;
};
@@ -14673,7 +15957,7 @@ function createInjector(modulesToLoad, strictDi) {
var runBlocks = [], moduleFn;
forEach(modulesToLoad, function(module) {
if (loadedModules.get(module)) return;
- loadedModules.put(module, true);
+ loadedModules.set(module, true);
function runInvokeQueue(queue) {
var i, ii;
@@ -14688,6 +15972,7 @@ function createInjector(modulesToLoad, strictDi) {
try {
if (isString(module)) {
moduleFn = angularModule(module);
+ instanceInjector.modules[module] = moduleFn;
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
runInvokeQueue(moduleFn._invokeQueue);
runInvokeQueue(moduleFn._configBlocks);
@@ -14702,15 +15987,15 @@ function createInjector(modulesToLoad, strictDi) {
if (isArray(module)) {
module = module[module.length - 1];
}
- if (e.message && e.stack && e.stack.indexOf(e.message) == -1) {
+ if (e.message && e.stack && e.stack.indexOf(e.message) === -1) {
// Safari & FF's stack traces don't contain error.message content
// unlike those of Chrome and IE
// So if stack doesn't contain message, we create a new string that contains both.
// Since error.stack is read-only in Safari, I'm overriding e and not e.stack here.
- /* jshint -W022 */
+ // eslint-disable-next-line no-ex-assign
e = e.message + '\n' + e.stack;
}
- throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}",
+ throw $injectorMinErr('modulerr', 'Failed to instantiate module {0} due to:\n{1}',
module, e.stack || e.message || e);
}
});
@@ -14734,7 +16019,8 @@ function createInjector(modulesToLoad, strictDi) {
try {
path.unshift(serviceName);
cache[serviceName] = INSTANTIATING;
- return cache[serviceName] = factory(serviceName, caller);
+ cache[serviceName] = factory(serviceName, caller);
+ return cache[serviceName];
} catch (err) {
if (cache[serviceName] === INSTANTIATING) {
delete cache[serviceName];
@@ -14764,14 +16050,16 @@ function createInjector(modulesToLoad, strictDi) {
}
function isClass(func) {
+ // Support: IE 9-11 only
// IE 9-11 do not support classes and IE9 leaks with the code below.
- if (msie <= 11) {
+ if (msie || typeof func !== 'function') {
return false;
}
- // Support: Edge 12-13 only
- // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/
- return typeof func === 'function'
- && /^(?:class\b|constructor\()/.test(stringifyFn(func));
+ var result = func.$$ngIsClass;
+ if (!isBoolean(result)) {
+ result = func.$$ngIsClass = /^class\b/.test(stringifyFn(func));
+ }
+ return result;
}
function invoke(fn, self, locals, serviceName) {
@@ -14824,6 +16112,7 @@ createInjector.$$annotate = annotate;
/**
* @ngdoc provider
* @name $anchorScrollProvider
+ * @this
*
* @description
* Use `$anchorScrollProvider` to disable automatic scrolling whenever
@@ -14895,7 +16184,7 @@ function $AnchorScrollProvider() {
* </div>
*
* @example
- <example module="anchorScrollExample">
+ <example module="anchorScrollExample" name="anchor-scroll">
<file name="index.html">
<div id="scrollArea" ng-controller="ScrollController">
<a ng-click="gotoBottom()">Go to bottom</a>
@@ -14905,7 +16194,7 @@ function $AnchorScrollProvider() {
<file name="script.js">
angular.module('anchorScrollExample', [])
.controller('ScrollController', ['$scope', '$location', '$anchorScroll',
- function ($scope, $location, $anchorScroll) {
+ function($scope, $location, $anchorScroll) {
$scope.gotoBottom = function() {
// set the location.hash to the id of
// the element you wish to scroll to.
@@ -14934,7 +16223,7 @@ function $AnchorScrollProvider() {
* See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details.
*
* @example
- <example module="anchorScrollOffsetExample">
+ <example module="anchorScrollOffsetExample" name="anchor-scroll-offset">
<file name="index.html">
<div class="fixed-header" ng-controller="headerCtrl">
<a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
@@ -14951,7 +16240,7 @@ function $AnchorScrollProvider() {
$anchorScroll.yOffset = 50; // always scroll by 50 extra pixels
}])
.controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
- function ($anchorScroll, $location, $scope) {
+ function($anchorScroll, $location, $scope) {
$scope.gotoAnchor = function(x) {
var newHash = 'anchor' + x;
if ($location.hash() !== newHash) {
@@ -15058,7 +16347,8 @@ function $AnchorScrollProvider() {
}
function scroll(hash) {
- hash = isString(hash) ? hash : $location.hash();
+ // Allow numeric hashes
+ hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash();
var elm;
// empty hash, scroll to the top of the page
@@ -15070,7 +16360,7 @@ function $AnchorScrollProvider() {
// first anchor with given name :-D
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm);
- // no element and hash == 'top', scroll to the top of the page
+ // no element and hash === 'top', scroll to the top of the page
else if (hash === 'top') scrollTo(null);
}
@@ -15145,14 +16435,14 @@ function prepareAnimateOptions(options) {
: {};
}
-var $$CoreAnimateJsProvider = function() {
+var $$CoreAnimateJsProvider = /** @this */ function() {
this.$get = noop;
};
// this is prefixed with Core since it conflicts with
// the animateQueueProvider defined in ngAnimate/animateQueue.js
-var $$CoreAnimateQueueProvider = function() {
- var postDigestQueue = new HashMap();
+var $$CoreAnimateQueueProvider = /** @this */ function() {
+ var postDigestQueue = new NgMap();
var postDigestElements = [];
this.$get = ['$$AnimateRunner', '$rootScope',
@@ -15164,17 +16454,23 @@ var $$CoreAnimateQueueProvider = function() {
pin: noop,
push: function(element, event, options, domOperation) {
- domOperation && domOperation();
+ if (domOperation) {
+ domOperation();
+ }
options = options || {};
- options.from && element.css(options.from);
- options.to && element.css(options.to);
+ if (options.from) {
+ element.css(options.from);
+ }
+ if (options.to) {
+ element.css(options.to);
+ }
if (options.addClass || options.removeClass) {
addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
}
- var runner = new $$AnimateRunner(); // jshint ignore:line
+ var runner = new $$AnimateRunner();
// since there are no animations to run the runner needs to be
// notified that the animation call is complete.
@@ -15218,10 +16514,14 @@ var $$CoreAnimateQueueProvider = function() {
});
forEach(element, function(elm) {
- toAdd && jqLiteAddClass(elm, toAdd);
- toRemove && jqLiteRemoveClass(elm, toRemove);
+ if (toAdd) {
+ jqLiteAddClass(elm, toAdd);
+ }
+ if (toRemove) {
+ jqLiteRemoveClass(elm, toRemove);
+ }
});
- postDigestQueue.remove(element);
+ postDigestQueue.delete(element);
}
});
postDigestElements.length = 0;
@@ -15236,7 +16536,7 @@ var $$CoreAnimateQueueProvider = function() {
if (classesAdded || classesRemoved) {
- postDigestQueue.put(element, data);
+ postDigestQueue.set(element, data);
postDigestElements.push(element);
if (postDigestElements.length === 1) {
@@ -15259,8 +16559,10 @@ var $$CoreAnimateQueueProvider = function() {
*
* To see the functional implementation check out `src/ngAnimate/animate.js`.
*/
-var $AnimateProvider = ['$provide', function($provide) {
+var $AnimateProvider = ['$provide', /** @this */ function($provide) {
var provider = this;
+ var classNameFilter = null;
+ var customFilter = null;
this.$$registeredAnimations = Object.create(null);
@@ -15305,7 +16607,7 @@ var $AnimateProvider = ['$provide', function($provide) {
*/
this.register = function(name, factory) {
if (name && name.charAt(0) !== '.') {
- throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name);
+ throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name);
}
var key = name + '-animation';
@@ -15315,6 +16617,51 @@ var $AnimateProvider = ['$provide', function($provide) {
/**
* @ngdoc method
+ * @name $animateProvider#customFilter
+ *
+ * @description
+ * Sets and/or returns the custom filter function that is used to "filter" animations, i.e.
+ * determine if an animation is allowed or not. When no filter is specified (the default), no
+ * animation will be blocked. Setting the `customFilter` value will only allow animations for
+ * which the filter function's return value is truthy.
+ *
+ * This allows to easily create arbitrarily complex rules for filtering animations, such as
+ * allowing specific events only, or enabling animations on specific subtrees of the DOM, etc.
+ * Filtering animations can also boost performance for low-powered devices, as well as
+ * applications containing a lot of structural operations.
+ *
+ * <div class="alert alert-success">
+ * **Best Practice:**
+ * Keep the filtering function as lean as possible, because it will be called for each DOM
+ * action (e.g. insertion, removal, class change) performed by "animation-aware" directives.
+ * See {@link guide/animations#which-directives-support-animations- here} for a list of built-in
+ * directives that support animations.
+ * Performing computationally expensive or time-consuming operations on each call of the
+ * filtering function can make your animations sluggish.
+ * </div>
+ *
+ * **Note:** If present, `customFilter` will be checked before
+ * {@link $animateProvider#classNameFilter classNameFilter}.
+ *
+ * @param {Function=} filterFn - The filter function which will be used to filter all animations.
+ * If a falsy value is returned, no animation will be performed. The function will be called
+ * with the following arguments:
+ * - **node** `{DOMElement}` - The DOM element to be animated.
+ * - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass`
+ * etc).
+ * - **options** `{Object}` - A collection of options/styles used for the animation.
+ * @return {Function} The current filter function or `null` if there is none set.
+ */
+ this.customFilter = function(filterFn) {
+ if (arguments.length === 1) {
+ customFilter = isFunction(filterFn) ? filterFn : null;
+ }
+
+ return customFilter;
+ };
+
+ /**
+ * @ngdoc method
* @name $animateProvider#classNameFilter
*
* @description
@@ -15324,21 +16671,26 @@ var $AnimateProvider = ['$provide', function($provide) {
* When setting the `classNameFilter` value, animations will only be performed on elements
* that successfully match the filter expression. This in turn can boost performance
* for low-powered devices as well as applications containing a lot of structural operations.
+ *
+ * **Note:** If present, `classNameFilter` will be checked after
+ * {@link $animateProvider#customFilter customFilter}. If `customFilter` is present and returns
+ * false, `classNameFilter` will not be checked.
+ *
* @param {RegExp=} expression The className expression which will be checked against all animations
* @return {RegExp} The current CSS className expression value. If null then there is no expression value
*/
this.classNameFilter = function(expression) {
if (arguments.length === 1) {
- this.$$classNameFilter = (expression instanceof RegExp) ? expression : null;
- if (this.$$classNameFilter) {
- var reservedRegex = new RegExp("(\\s+|\\/)" + NG_ANIMATE_CLASSNAME + "(\\s+|\\/)");
- if (reservedRegex.test(this.$$classNameFilter.toString())) {
- throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
-
+ classNameFilter = (expression instanceof RegExp) ? expression : null;
+ if (classNameFilter) {
+ var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]');
+ if (reservedRegex.test(classNameFilter.toString())) {
+ classNameFilter = null;
+ throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
}
}
}
- return this.$$classNameFilter;
+ return classNameFilter;
};
this.$get = ['$$animateQueue', function($$animateQueue) {
@@ -15352,7 +16704,11 @@ var $AnimateProvider = ['$provide', function($provide) {
afterElement = null;
}
}
- afterElement ? afterElement.after(element) : parentElement.prepend(element);
+ if (afterElement) {
+ afterElement.after(element);
+ } else {
+ parentElement.prepend(element);
+ }
}
/**
@@ -15395,14 +16751,39 @@ var $AnimateProvider = ['$provide', function($provide) {
* );
* ```
*
+ * <div class="alert alert-warning">
+ * **Note**: Generally, the events that are fired correspond 1:1 to `$animate` method names,
+ * e.g. {@link ng.$animate#addClass addClass()} will fire `addClass`, and {@link ng.ngClass}
+ * will fire `addClass` if classes are added, and `removeClass` if classes are removed.
+ * However, there are two exceptions:
+ *
+ * <ul>
+ * <li>if both an {@link ng.$animate#addClass addClass()} and a
+ * {@link ng.$animate#removeClass removeClass()} action are performed during the same
+ * animation, the event fired will be `setClass`. This is true even for `ngClass`.</li>
+ * <li>an {@link ng.$animate#animate animate()} call that adds and removes classes will fire
+ * the `setClass` event, but if it either removes or adds classes,
+ * it will fire `animate` instead.</li>
+ * </ul>
+ *
+ * </div>
+ *
* @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...)
* @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself
* as well as among its children
- * @param {Function} callback the callback function that will be fired when the listener is triggered
+ * @param {Function} callback the callback function that will be fired when the listener is triggered.
*
* The arguments present in the callback function are:
* * `element` - The captured DOM element that the animation was fired on.
* * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends).
+ * * `data` - an object with these properties:
+ * * addClass - `{string|null}` - space-separated CSS classes to add to the element
+ * * removeClass - `{string|null}` - space-separated CSS classes to remove from the element
+ * * from - `{Object|null}` - CSS properties & values at the beginning of the animation
+ * * to - `{Object|null}` - CSS properties & values at the end of the animation
+ *
+ * Note that the callback does not trigger a scope digest. Wrap your call into a
+ * {@link $rootScope.Scope#$apply scope.$apply} to propagate changes to the scope.
*/
on: $$animateQueue.on,
@@ -15442,7 +16823,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* @name $animate#pin
* @kind function
* @description Associates the provided element with a host parent element to allow the element to be animated even if it exists
- * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the
+ * outside of the DOM structure of the AngularJS application. By doing so, any animation triggered via `$animate` can be issued on the
* element despite being outside the realm of the application or within another application. Say for example if the application
* was bootstrapped on an element that is somewhere inside of the `<body>` tag, but we wanted to allow for an element to be situated
* as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind
@@ -15490,12 +16871,78 @@ var $AnimateProvider = ['$provide', function($provide) {
* @ngdoc method
* @name $animate#cancel
* @kind function
- * @description Cancels the provided animation.
+ * @description Cancels the provided animation and applies the end state of the animation.
+ * Note that this does not cancel the underlying operation, e.g. the setting of classes or
+ * adding the element to the DOM.
+ *
+ * @param {animationRunner} animationRunner An animation runner returned by an $animate function.
*
- * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
+ * @example
+ <example module="animationExample" deps="angular-animate.js" animations="true" name="animate-cancel">
+ <file name="app.js">
+ angular.module('animationExample', ['ngAnimate']).component('cancelExample', {
+ templateUrl: 'template.html',
+ controller: function($element, $animate) {
+ this.runner = null;
+
+ this.addClass = function() {
+ this.runner = $animate.addClass($element.find('div'), 'red');
+ var ctrl = this;
+ this.runner.finally(function() {
+ ctrl.runner = null;
+ });
+ };
+
+ this.removeClass = function() {
+ this.runner = $animate.removeClass($element.find('div'), 'red');
+ var ctrl = this;
+ this.runner.finally(function() {
+ ctrl.runner = null;
+ });
+ };
+
+ this.cancel = function() {
+ $animate.cancel(this.runner);
+ };
+ }
+ });
+ </file>
+ <file name="template.html">
+ <p>
+ <button id="add" ng-click="$ctrl.addClass()">Add</button>
+ <button ng-click="$ctrl.removeClass()">Remove</button>
+ <br>
+ <button id="cancel" ng-click="$ctrl.cancel()" ng-disabled="!$ctrl.runner">Cancel</button>
+ <br>
+ <div id="target">CSS-Animated Text</div>
+ </p>
+ </file>
+ <file name="index.html">
+ <cancel-example></cancel-example>
+ </file>
+ <file name="style.css">
+ .red-add, .red-remove {
+ transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
+ }
+
+ .red,
+ .red-add.red-add-active {
+ color: #FF0000;
+ font-size: 40px;
+ }
+
+ .red-remove.red-remove-active {
+ font-size: 10px;
+ color: black;
+ }
+
+ </file>
+ </example>
*/
cancel: function(runner) {
- runner.end && runner.end();
+ if (runner.cancel) {
+ runner.cancel();
+ }
},
/**
@@ -15520,7 +16967,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
enter: function(element, parent, after, options) {
parent = parent && jqLite(parent);
@@ -15552,7 +16999,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
move: function(element, parent, after, options) {
parent = parent && jqLite(parent);
@@ -15579,7 +17026,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
leave: function(element, options) {
return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
@@ -15604,12 +17051,11 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {object=} options an optional collection of options/styles that will be applied to the element.
* The object can have the following properties:
*
- * - **addClass** - `{string}` - space-separated CSS classes to add to element
- * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} animationRunner the animation runner
*/
addClass: function(element, className, options) {
options = prepareAnimateOptions(options);
@@ -15636,10 +17082,9 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* - **addClass** - `{string}` - space-separated CSS classes to add to element
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
- * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
removeClass: function(element, className, options) {
options = prepareAnimateOptions(options);
@@ -15666,11 +17111,11 @@ var $AnimateProvider = ['$provide', function($provide) {
* The object can have the following properties:
*
* - **addClass** - `{string}` - space-separated CSS classes to add to element
- * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
setClass: function(element, add, remove, options) {
options = prepareAnimateOptions(options);
@@ -15686,7 +17131,7 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element.
* If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take
- * on the provided styles. For example, if a transition animation is set for the given classNamem, then the provided `from` and
+ * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and
* `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding
* style in `to`, the style in `from` is applied immediately, and no animation is run.
* If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate`
@@ -15717,7 +17162,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
animate: function(element, from, to, className, options) {
options = prepareAnimateOptions(options);
@@ -15732,7 +17177,7 @@ var $AnimateProvider = ['$provide', function($provide) {
}];
}];
-var $$AnimateAsyncRunFactoryProvider = function() {
+var $$AnimateAsyncRunFactoryProvider = /** @this */ function() {
this.$get = ['$$rAF', function($$rAF) {
var waitQueue = [];
@@ -15753,15 +17198,19 @@ var $$AnimateAsyncRunFactoryProvider = function() {
passed = true;
});
return function(callback) {
- passed ? callback() : waitForTick(callback);
+ if (passed) {
+ callback();
+ } else {
+ waitForTick(callback);
+ }
};
};
}];
};
-var $$AnimateRunnerFactoryProvider = function() {
- this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout',
- function($q, $sniffer, $$animateAsyncRun, $document, $timeout) {
+var $$AnimateRunnerFactoryProvider = /** @this */ function() {
+ this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout',
+ function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) {
var INITIAL_STATE = 0;
var DONE_PENDING_STATE = 1;
@@ -15813,11 +17262,7 @@ var $$AnimateRunnerFactoryProvider = function() {
this._doneCallbacks = [];
this._tick = function(fn) {
- var doc = $document[0];
-
- // the document may not be ready or attached
- // to the module for some internal tests
- if (doc && doc.hidden) {
+ if ($$isDocumentHidden()) {
timeoutTick(fn);
} else {
rafTick(fn);
@@ -15846,7 +17291,11 @@ var $$AnimateRunnerFactoryProvider = function() {
var self = this;
this.promise = $q(function(resolve, reject) {
self.done(function(status) {
- status === false ? reject() : resolve();
+ if (status === false) {
+ reject();
+ } else {
+ resolve();
+ }
});
});
}
@@ -15920,6 +17369,7 @@ var $$AnimateRunnerFactoryProvider = function() {
* @ngdoc service
* @name $animateCss
* @kind object
+ * @this
*
* @description
* This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included,
@@ -15952,7 +17402,6 @@ var $CoreAnimateCssProvider = function() {
options.from = null;
}
- /* jshint newcap: false */
var closed, runner = new $$AnimateRunner();
return {
start: run,
@@ -15990,6858 +17439,7 @@ var $CoreAnimateCssProvider = function() {
/* global stripHash: true */
-/**
- * ! This is a private undocumented service !
- *
- * @name $browser
- * @requires $log
- * @description
- * This object has two goals:
- *
- * - hide all the global state in the browser caused by the window object
- * - abstract away all the browser specific features and inconsistencies
- *
- * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser`
- * service, which can be used for convenient testing of the application without the interaction with
- * the real browser apis.
- */
-/**
- * @param {object} window The global window object.
- * @param {object} document jQuery wrapped document.
- * @param {object} $log window.console or an object with the same interface.
- * @param {object} $sniffer $sniffer service
- */
-function Browser(window, document, $log, $sniffer) {
- var self = this,
- location = window.location,
- history = window.history,
- setTimeout = window.setTimeout,
- clearTimeout = window.clearTimeout,
- pendingDeferIds = {};
-
- self.isMock = false;
-
- var outstandingRequestCount = 0;
- var outstandingRequestCallbacks = [];
-
- // TODO(vojta): remove this temporary api
- self.$$completeOutstandingRequest = completeOutstandingRequest;
- self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
-
- /**
- * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks`
- * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
- */
- function completeOutstandingRequest(fn) {
- try {
- fn.apply(null, sliceArgs(arguments, 1));
- } finally {
- outstandingRequestCount--;
- if (outstandingRequestCount === 0) {
- while (outstandingRequestCallbacks.length) {
- try {
- outstandingRequestCallbacks.pop()();
- } catch (e) {
- $log.error(e);
- }
- }
- }
- }
- }
-
- function getHash(url) {
- var index = url.indexOf('#');
- return index === -1 ? '' : url.substr(index);
- }
-
- /**
- * @private
- * Note: this method is used only by scenario runner
- * TODO(vojta): prefix this method with $$ ?
- * @param {function()} callback Function that will be called when no outstanding request
- */
- self.notifyWhenNoOutstandingRequests = function(callback) {
- if (outstandingRequestCount === 0) {
- callback();
- } else {
- outstandingRequestCallbacks.push(callback);
- }
- };
-
- //////////////////////////////////////////////////////////////
- // URL API
- //////////////////////////////////////////////////////////////
-
- var cachedState, lastHistoryState,
- lastBrowserUrl = location.href,
- baseElement = document.find('base'),
- pendingLocation = null,
- getCurrentState = !$sniffer.history ? noop : function getCurrentState() {
- try {
- return history.state;
- } catch (e) {
- // MSIE can reportedly throw when there is no state (UNCONFIRMED).
- }
- };
-
- cacheState();
- lastHistoryState = cachedState;
-
- /**
- * @name $browser#url
- *
- * @description
- * GETTER:
- * Without any argument, this method just returns current value of location.href.
- *
- * SETTER:
- * With at least one argument, this method sets url to new value.
- * If html5 history api supported, pushState/replaceState is used, otherwise
- * location.href/location.replace is used.
- * Returns its own instance to allow chaining
- *
- * 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 {object=} state object to use with pushState/replaceState
- */
- 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) {
- var sameState = lastHistoryState === state;
-
- // 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 || sameState)) {
- return self;
- }
- var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
- lastBrowserUrl = url;
- lastHistoryState = state;
- // 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 || !sameState)) {
- history[replace ? 'replaceState' : 'pushState'](state, '', url);
- cacheState();
- // Do the assignment again so that those two variables are referentially identical.
- lastHistoryState = cachedState;
- } else {
- if (!sameBase) {
- pendingLocation = url;
- }
- if (replace) {
- location.replace(url);
- } else if (!sameBase) {
- location.href = url;
- } else {
- location.hash = getHash(url);
- }
- if (location.href !== url) {
- pendingLocation = url;
- }
- }
- if (pendingLocation) {
- pendingLocation = url;
- }
- return self;
- // getter
- } else {
- // - pendingLocation is needed as browsers don't allow to read out
- // the new location.href if a reload happened or if there is a bug like in iOS 9 (see
- // https://openradar.appspot.com/22186109).
- // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
- return pendingLocation || 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 cachedState;
- };
-
- var urlChangeListeners = [],
- urlChangeInit = false;
-
- function cacheStateAndFireUrlChange() {
- pendingLocation = null;
- cacheState();
- fireUrlChange();
- }
-
- // This variable should be used *only* inside the cacheState function.
- var lastCachedState = null;
- function cacheState() {
- // This should be the only place in $browser where `history.state` is read.
- cachedState = getCurrentState();
- cachedState = isUndefined(cachedState) ? null : cachedState;
-
- // Prevent callbacks fo fire twice if both hashchange & popstate were fired.
- if (equals(cachedState, lastCachedState)) {
- cachedState = lastCachedState;
- }
- lastCachedState = cachedState;
- }
-
- function fireUrlChange() {
- if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
- return;
- }
-
- lastBrowserUrl = self.url();
- lastHistoryState = cachedState;
- forEach(urlChangeListeners, function(listener) {
- listener(self.url(), cachedState);
- });
- }
-
- /**
- * @name $browser#onUrlChange
- *
- * @description
- * Register callback function that will be called, when url changes.
- *
- * It's only called when the url is changed from outside of angular:
- * - user types different url into address bar
- * - user clicks on history (forward/back) button
- * - user clicks on a link
- *
- * It's not called when url is changed by $browser.url() method
- *
- * The listener gets called with new url as parameter.
- *
- * NOTE: this api is intended for use only by the $location service. Please use the
- * {@link ng.$location $location service} to monitor url changes in angular apps.
- *
- * @param {function(string)} listener Listener function to be called when url changes.
- * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous.
- */
- self.onUrlChange = function(callback) {
- // TODO(vojta): refactor to use node's syntax for events
- if (!urlChangeInit) {
- // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera)
- // don't fire popstate when user change the address bar and don't fire hashchange when url
- // changed by push/replaceState
-
- // html5 history api - popstate event
- if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange);
- // hashchange event
- jqLite(window).on('hashchange', cacheStateAndFireUrlChange);
-
- urlChangeInit = true;
- }
-
- urlChangeListeners.push(callback);
- return callback;
- };
-
- /**
- * @private
- * Remove popstate and hashchange handler from window.
- *
- * NOTE: this api is intended for use only by $rootScope.
- */
- self.$$applicationDestroyed = function() {
- jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange);
- };
-
- /**
- * Checks whether the url has changed outside of Angular.
- * Needs to be exported to be able to check for changes that have been done in sync,
- * as hashchange/popstate events fire in async.
- */
- self.$$checkUrlChange = fireUrlChange;
-
- //////////////////////////////////////////////////////////////
- // Misc API
- //////////////////////////////////////////////////////////////
-
- /**
- * @name $browser#baseHref
- *
- * @description
- * Returns current <base href>
- * (always relative - without domain)
- *
- * @returns {string} The current base href
- */
- self.baseHref = function() {
- var href = baseElement.attr('href');
- return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : '';
- };
-
- /**
- * @name $browser#defer
- * @param {function()} fn A function, who's execution should be deferred.
- * @param {number=} [delay=0] of milliseconds to defer the function execution.
- * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`.
- *
- * @description
- * Executes a fn asynchronously via `setTimeout(fn, delay)`.
- *
- * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using
- * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed
- * via `$browser.defer.flush()`.
- *
- */
- self.defer = function(fn, delay) {
- var timeoutId;
- outstandingRequestCount++;
- timeoutId = setTimeout(function() {
- delete pendingDeferIds[timeoutId];
- completeOutstandingRequest(fn);
- }, delay || 0);
- pendingDeferIds[timeoutId] = true;
- return timeoutId;
- };
-
-
- /**
- * @name $browser#defer.cancel
- *
- * @description
- * Cancels a deferred task identified with `deferId`.
- *
- * @param {*} deferId Token returned by the `$browser.defer` function.
- * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
- * canceled.
- */
- self.defer.cancel = function(deferId) {
- if (pendingDeferIds[deferId]) {
- delete pendingDeferIds[deferId];
- clearTimeout(deferId);
- completeOutstandingRequest(noop);
- return true;
- }
- return false;
- };
-
-}
-
-function $BrowserProvider() {
- this.$get = ['$window', '$log', '$sniffer', '$document',
- function($window, $log, $sniffer, $document) {
- return new Browser($window, $document, $log, $sniffer);
- }];
-}
-
-/**
- * @ngdoc service
- * @name $cacheFactory
- *
- * @description
- * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to
- * them.
- *
- * ```js
- *
- * var cache = $cacheFactory('cacheId');
- * expect($cacheFactory.get('cacheId')).toBe(cache);
- * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined();
- *
- * cache.put("key", "value");
- * cache.put("another key", "another value");
- *
- * // We've specified no options on creation
- * expect(cache.info()).toEqual({id: 'cacheId', size: 2});
- *
- * ```
- *
- *
- * @param {string} cacheId Name or id of the newly created cache.
- * @param {object=} options Options object that specifies the cache behavior. Properties:
- *
- * - `{number=}` `capacity` — turns the cache into LRU cache.
- *
- * @returns {object} Newly created cache object with the following set of methods:
- *
- * - `{object}` `info()` — Returns id, size, and options of cache.
- * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns
- * it.
- * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss.
- * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache.
- * - `{void}` `removeAll()` — Removes all cached values.
- * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory.
- *
- * @example
- <example module="cacheExampleApp">
- <file name="index.html">
- <div ng-controller="CacheController">
- <input ng-model="newCacheKey" placeholder="Key">
- <input ng-model="newCacheValue" placeholder="Value">
- <button ng-click="put(newCacheKey, newCacheValue)">Cache</button>
-
- <p ng-if="keys.length">Cached Values</p>
- <div ng-repeat="key in keys">
- <span ng-bind="key"></span>
- <span>: </span>
- <b ng-bind="cache.get(key)"></b>
- </div>
-
- <p>Cache Info</p>
- <div ng-repeat="(key, value) in cache.info()">
- <span ng-bind="key"></span>
- <span>: </span>
- <b ng-bind="value"></b>
- </div>
- </div>
- </file>
- <file name="script.js">
- angular.module('cacheExampleApp', []).
- controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) {
- $scope.keys = [];
- $scope.cache = $cacheFactory('cacheId');
- $scope.put = function(key, value) {
- if (angular.isUndefined($scope.cache.get(key))) {
- $scope.keys.push(key);
- }
- $scope.cache.put(key, angular.isUndefined(value) ? null : value);
- };
- }]);
- </file>
- <file name="style.css">
- p {
- margin: 10px 0 3px;
- }
- </file>
- </example>
- */
-function $CacheFactoryProvider() {
-
- this.$get = function() {
- var caches = {};
-
- function cacheFactory(cacheId, options) {
- if (cacheId in caches) {
- throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId);
- }
-
- var size = 0,
- stats = extend({}, options, {id: cacheId}),
- data = createMap(),
- capacity = (options && options.capacity) || Number.MAX_VALUE,
- lruHash = createMap(),
- freshEnd = null,
- staleEnd = null;
-
- /**
- * @ngdoc type
- * @name $cacheFactory.Cache
- *
- * @description
- * A cache object used to store and retrieve data, primarily used by
- * {@link $http $http} and the {@link ng.directive:script script} directive to cache
- * templates and other data.
- *
- * ```js
- * angular.module('superCache')
- * .factory('superCache', ['$cacheFactory', function($cacheFactory) {
- * return $cacheFactory('super-cache');
- * }]);
- * ```
- *
- * Example test:
- *
- * ```js
- * it('should behave like a cache', inject(function(superCache) {
- * superCache.put('key', 'value');
- * superCache.put('another key', 'another value');
- *
- * expect(superCache.info()).toEqual({
- * id: 'super-cache',
- * size: 2
- * });
- *
- * superCache.remove('another key');
- * expect(superCache.get('another key')).toBeUndefined();
- *
- * superCache.removeAll();
- * expect(superCache.info()).toEqual({
- * id: 'super-cache',
- * size: 0
- * });
- * }));
- * ```
- */
- return caches[cacheId] = {
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#put
- * @kind function
- *
- * @description
- * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be
- * retrieved later, and incrementing the size of the cache if the key was not already
- * present in the cache. If behaving like an LRU cache, it will also remove stale
- * entries from the set.
- *
- * It will not insert undefined values into the cache.
- *
- * @param {string} key the key under which the cached data is stored.
- * @param {*} value the value to store alongside the key. If it is undefined, the key
- * will not be stored.
- * @returns {*} the value stored.
- */
- put: function(key, value) {
- if (isUndefined(value)) return;
- if (capacity < Number.MAX_VALUE) {
- var lruEntry = lruHash[key] || (lruHash[key] = {key: key});
-
- refresh(lruEntry);
- }
-
- if (!(key in data)) size++;
- data[key] = value;
-
- if (size > capacity) {
- this.remove(staleEnd.key);
- }
-
- return value;
- },
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#get
- * @kind function
- *
- * @description
- * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object.
- *
- * @param {string} key the key of the data to be retrieved
- * @returns {*} the value stored.
- */
- get: function(key) {
- if (capacity < Number.MAX_VALUE) {
- var lruEntry = lruHash[key];
-
- if (!lruEntry) return;
-
- refresh(lruEntry);
- }
-
- return data[key];
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#remove
- * @kind function
- *
- * @description
- * Removes an entry from the {@link $cacheFactory.Cache Cache} object.
- *
- * @param {string} key the key of the entry to be removed
- */
- remove: function(key) {
- if (capacity < Number.MAX_VALUE) {
- var lruEntry = lruHash[key];
-
- if (!lruEntry) return;
-
- if (lruEntry == freshEnd) freshEnd = lruEntry.p;
- if (lruEntry == staleEnd) staleEnd = lruEntry.n;
- link(lruEntry.n,lruEntry.p);
-
- delete lruHash[key];
- }
-
- if (!(key in data)) return;
-
- delete data[key];
- size--;
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#removeAll
- * @kind function
- *
- * @description
- * Clears the cache object of any entries.
- */
- removeAll: function() {
- data = createMap();
- size = 0;
- lruHash = createMap();
- freshEnd = staleEnd = null;
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#destroy
- * @kind function
- *
- * @description
- * Destroys the {@link $cacheFactory.Cache Cache} object entirely,
- * removing it from the {@link $cacheFactory $cacheFactory} set.
- */
- destroy: function() {
- data = null;
- stats = null;
- lruHash = null;
- delete caches[cacheId];
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#info
- * @kind function
- *
- * @description
- * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}.
- *
- * @returns {object} an object with the following properties:
- * <ul>
- * <li>**id**: the id of the cache instance</li>
- * <li>**size**: the number of entries kept in the cache instance</li>
- * <li>**...**: any additional properties from the options object when creating the
- * cache.</li>
- * </ul>
- */
- info: function() {
- return extend({}, stats, {size: size});
- }
- };
-
-
- /**
- * makes the `entry` the freshEnd of the LRU linked list
- */
- function refresh(entry) {
- if (entry != freshEnd) {
- if (!staleEnd) {
- staleEnd = entry;
- } else if (staleEnd == entry) {
- staleEnd = entry.n;
- }
-
- link(entry.n, entry.p);
- link(entry, freshEnd);
- freshEnd = entry;
- freshEnd.n = null;
- }
- }
-
-
- /**
- * bidirectionally links two entries of the LRU linked list
- */
- function link(nextEntry, prevEntry) {
- if (nextEntry != prevEntry) {
- if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify
- if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify
- }
- }
- }
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory#info
- *
- * @description
- * Get information about all the caches that have been created
- *
- * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info`
- */
- cacheFactory.info = function() {
- var info = {};
- forEach(caches, function(cache, cacheId) {
- info[cacheId] = cache.info();
- });
- return info;
- };
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory#get
- *
- * @description
- * Get access to a cache object by the `cacheId` used when it was created.
- *
- * @param {string} cacheId Name or id of a cache to access.
- * @returns {object} Cache object identified by the cacheId or undefined if no such cache.
- */
- cacheFactory.get = function(cacheId) {
- return caches[cacheId];
- };
-
-
- return cacheFactory;
- };
-}
-
-/**
- * @ngdoc service
- * @name $templateCache
- *
- * @description
- * The first time a template is used, it is loaded in the template cache for quick retrieval. You
- * can load templates directly into the cache in a `script` tag, or by consuming the
- * `$templateCache` service directly.
- *
- * Adding via the `script` tag:
- *
- * ```html
- * <script type="text/ng-template" id="templateId.html">
- * <p>This is the content of the template</p>
- * </script>
- * ```
- *
- * **Note:** the `script` tag containing the template does not need to be included in the `head` of
- * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE,
- * element with ng-app attribute), otherwise the template will be ignored.
- *
- * Adding via the `$templateCache` service:
- *
- * ```js
- * var myApp = angular.module('myApp', []);
- * myApp.run(function($templateCache) {
- * $templateCache.put('templateId.html', 'This is the content of the template');
- * });
- * ```
- *
- * To retrieve the template later, simply use it in your HTML:
- * ```html
- * <div ng-include=" 'templateId.html' "></div>
- * ```
- *
- * or get it via Javascript:
- * ```js
- * $templateCache.get('templateId.html')
- * ```
- *
- * See {@link ng.$cacheFactory $cacheFactory}.
- *
- */
-function $TemplateCacheProvider() {
- this.$get = ['$cacheFactory', function($cacheFactory) {
- return $cacheFactory('templates');
- }];
-}
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Any commits to this file should be reviewed with security in mind. *
- * Changes to this file can potentially create security vulnerabilities. *
- * An approval from 2 Core members with history of modifying *
- * this file is required. *
- * *
- * Does the change somehow allow for arbitrary javascript to be executed? *
- * Or allows for someone to change the prototype of built-in objects? *
- * Or gives undesired access to variables likes document or window? *
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
- *
- * DOM-related variables:
- *
- * - "node" - DOM Node
- * - "element" - DOM Element or Node
- * - "$node" or "$element" - jqLite-wrapped node or element
- *
- *
- * Compiler related stuff:
- *
- * - "linkFn" - linking fn of a single directive
- * - "nodeLinkFn" - function that aggregates all linking fns for a particular node
- * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node
- * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList)
- */
-
-
-/**
- * @ngdoc service
- * @name $compile
- * @kind function
- *
- * @description
- * Compiles an HTML string or DOM into a template and produces a template function, which
- * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together.
- *
- * The compilation is a process of walking the DOM tree and matching DOM elements to
- * {@link ng.$compileProvider#directive directives}.
- *
- * <div class="alert alert-warning">
- * **Note:** This document is an in-depth reference of all directive options.
- * For a gentle introduction to directives with examples of common use cases,
- * see the {@link guide/directive directive guide}.
- * </div>
- *
- * ## Comprehensive Directive API
- *
- * There are many different options for a directive.
- *
- * The difference resides in the return value of the factory function.
- * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)}
- * that defines the directive properties, or just the `postLink` function (all other properties will have
- * the default values).
- *
- * <div class="alert alert-success">
- * **Best Practice:** It's recommended to use the "directive definition object" form.
- * </div>
- *
- * Here's an example directive declared with a Directive Definition Object:
- *
- * ```js
- * var myModule = angular.module(...);
- *
- * myModule.directive('directiveName', function factory(injectables) {
- * var directiveDefinitionObject = {
- * priority: 0,
- * template: '<div></div>', // or // function(tElement, tAttrs) { ... },
- * // or
- * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
- * transclude: false,
- * restrict: 'A',
- * templateNamespace: 'html',
- * scope: false,
- * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
- * controllerAs: 'stringIdentifier',
- * bindToController: false,
- * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
- * compile: function compile(tElement, tAttrs, transclude) {
- * return {
- * pre: function preLink(scope, iElement, iAttrs, controller) { ... },
- * post: function postLink(scope, iElement, iAttrs, controller) { ... }
- * }
- * // or
- * // return function postLink( ... ) { ... }
- * },
- * // or
- * // link: {
- * // pre: function preLink(scope, iElement, iAttrs, controller) { ... },
- * // post: function postLink(scope, iElement, iAttrs, controller) { ... }
- * // }
- * // or
- * // link: function postLink( ... ) { ... }
- * };
- * return directiveDefinitionObject;
- * });
- * ```
- *
- * <div class="alert alert-warning">
- * **Note:** Any unspecified options will use the default value. You can see the default values below.
- * </div>
- *
- * Therefore the above can be simplified as:
- *
- * ```js
- * var myModule = angular.module(...);
- *
- * myModule.directive('directiveName', function factory(injectables) {
- * var directiveDefinitionObject = {
- * link: function postLink(scope, iElement, iAttrs) { ... }
- * };
- * return directiveDefinitionObject;
- * // or
- * // return function postLink(scope, iElement, iAttrs) { ... }
- * });
- * ```
- *
- * ### Life-cycle hooks
- * Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the
- * directive:
- * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
- * had their bindings initialized (and before the pre &amp; post linking functions for the directives on
- * this element). This is a good place to put initialization code for your controller.
- * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
- * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
- * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a
- * component such as cloning the bound value to prevent accidental mutation of the outer value.
- * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on
- * changes. Any actions that you wish to take in response to the changes that you detect must be
- * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook
- * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not
- * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments;
- * if detecting changes, you must store the previous value(s) for comparison to the current values.
- * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
- * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
- * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
- * components will have their `$onDestroy()` hook called before child components.
- * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
- * function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
- * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
- * they are waiting for their template to load asynchronously and their own compilation and linking has been
- * suspended until that occurs.
- *
- * #### Comparison with Angular 2 life-cycle hooks
- * Angular 2 also uses life-cycle hooks for its components. While the Angular 1 life-cycle hooks are similar there are
- * some differences that you should be aware of, especially when it comes to moving your code from Angular 1 to Angular 2:
- *
- * * Angular 1 hooks are prefixed with `$`, such as `$onInit`. Angular 2 hooks are prefixed with `ng`, such as `ngOnInit`.
- * * Angular 1 hooks can be defined on the controller prototype or added to the controller inside its constructor.
- * In Angular 2 you can only define hooks on the prototype of the Component class.
- * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in Angular 1 than you would to
- * `ngDoCheck` in Angular 2
- * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be
- * propagated throughout the application.
- * Angular 2 does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an
- * error or do nothing depending upon the state of `enableProdMode()`.
- *
- * #### Life-cycle hook examples
- *
- * This example shows how you can check for mutations to a Date object even though the identity of the object
- * has not changed.
- *
- * <example name="doCheckDateExample" module="do-check-module">
- * <file name="app.js">
- * angular.module('do-check-module', [])
- * .component('app', {
- * template:
- * 'Month: <input ng-model="$ctrl.month" ng-change="$ctrl.updateDate()">' +
- * 'Date: {{ $ctrl.date }}' +
- * '<test date="$ctrl.date"></test>',
- * controller: function() {
- * this.date = new Date();
- * this.month = this.date.getMonth();
- * this.updateDate = function() {
- * this.date.setMonth(this.month);
- * };
- * }
- * })
- * .component('test', {
- * bindings: { date: '<' },
- * template:
- * '<pre>{{ $ctrl.log | json }}</pre>',
- * controller: function() {
- * var previousValue;
- * this.log = [];
- * this.$doCheck = function() {
- * var currentValue = this.date && this.date.valueOf();
- * if (previousValue !== currentValue) {
- * this.log.push('doCheck: date mutated: ' + this.date);
- * previousValue = currentValue;
- * }
- * };
- * }
- * });
- * </file>
- * <file name="index.html">
- * <app></app>
- * </file>
- * </example>
- *
- * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the
- * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large
- * arrays or objects can have a negative impact on your application performance)
- *
- * <example name="doCheckArrayExample" module="do-check-module">
- * <file name="index.html">
- * <div ng-init="items = []">
- * <button ng-click="items.push(items.length)">Add Item</button>
- * <button ng-click="items = []">Reset Items</button>
- * <pre>{{ items }}</pre>
- * <test items="items"></test>
- * </div>
- * </file>
- * <file name="app.js">
- * angular.module('do-check-module', [])
- * .component('test', {
- * bindings: { items: '<' },
- * template:
- * '<pre>{{ $ctrl.log | json }}</pre>',
- * controller: function() {
- * this.log = [];
- *
- * this.$doCheck = function() {
- * if (this.items_ref !== this.items) {
- * this.log.push('doCheck: items changed');
- * this.items_ref = this.items;
- * }
- * if (!angular.equals(this.items_clone, this.items)) {
- * this.log.push('doCheck: items mutated');
- * this.items_clone = angular.copy(this.items);
- * }
- * };
- * }
- * });
- * </file>
- * </example>
- *
- *
- * ### Directive Definition Object
- *
- * The directive definition object provides instructions to the {@link ng.$compile
- * compiler}. The attributes are:
- *
- * #### `multiElement`
- * When this property is set to true, the HTML compiler will collect DOM nodes between
- * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
- * together as the directive elements. It is recommended that this feature be used on directives
- * which are not strictly behavioral (such as {@link ngClick}), and which
- * do not manipulate or replace child nodes (such as {@link ngInclude}).
- *
- * #### `priority`
- * When there are multiple directives defined on a single DOM element, sometimes it
- * is necessary to specify the order in which the directives are applied. The `priority` is used
- * to sort the directives before their `compile` functions get called. Priority is defined as a
- * number. Directives with greater numerical `priority` are compiled first. Pre-link functions
- * are also run in priority order, but post-link functions are run in reverse order. The order
- * of directives with the same priority is undefined. The default priority is `0`.
- *
- * #### `terminal`
- * If set to true then the current `priority` will be the last set of directives
- * which will execute (any directives at the current priority will still execute
- * as the order of execution on same `priority` is undefined). Note that expressions
- * and other directives used in the directive's template will also be excluded from execution.
- *
- * #### `scope`
- * The scope property can be `true`, an object or a falsy value:
- *
- * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope.
- *
- * * **`true`:** A new child scope that prototypically inherits from its parent will be created for
- * the directive's element. If multiple directives on the same element request a new scope,
- * only one new scope is created. The new scope rule does not apply for the root of the template
- * since the root of the template always gets a new scope.
- *
- * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The
- * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent
- * scope. This is useful when creating reusable components, which should not accidentally read or modify
- * data in the parent scope.
- *
- * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the
- * directive's element. These local properties are useful for aliasing values for templates. The keys in
- * the object hash map to the name of the property on the isolate scope; the values define how the property
- * is bound to the parent scope, via matching attributes on the directive's element:
- *
- * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is
- * always a string since DOM attributes are strings. If no `attr` name is specified then the
- * attribute name is assumed to be the same as the local name. Given `<my-component
- * my-attr="hello {{name}}">` and the isolate scope definition `scope: { localName:'@myAttr' }`,
- * the directive's scope property `localName` will reflect the interpolated value of `hello
- * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's
- * scope. The `name` is read from the parent scope (not the directive's scope).
- *
- * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression
- * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope.
- * If no `attr` name is specified then the attribute name is assumed to be the same as the local
- * name. Given `<my-component my-attr="parentModel">` and the isolate scope definition `scope: {
- * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the
- * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in
- * `localModel` and vice versa. Optional attributes should be marked as such with a question mark:
- * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't
- * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`})
- * will be thrown upon discovering changes to the local value, since it will be impossible to sync
- * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`}
- * method is used for tracking changes, and the equality check is based on object identity.
- * However, if an object literal or an array literal is passed as the binding expression, the
- * equality check is done by value (using the {@link angular.equals} function). It's also possible
- * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection
- * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional).
- *
- * * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an
- * expression passed via the attribute `attr`. The expression is evaluated in the context of the
- * parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the
- * local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`.
- *
- * For example, given `<my-component my-attr="parentModel">` and directive definition of
- * `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the
- * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
- * in `localModel`, but changes in `localModel` will not reflect in `parentModel`. There are however
- * two caveats:
- * 1. one-way binding does not copy the value from the parent to the isolate scope, it simply
- * sets the same value. That means if your bound value is an object, changes to its properties
- * in the isolated scope will be reflected in the parent scope (because both reference the same object).
- * 2. one-way binding watches changes to the **identity** of the parent value. That means the
- * {@link ng.$rootScope.Scope#$watch `$watch`} on the parent value only fires if the reference
- * to the value has changed. In most cases, this should not be of concern, but can be important
- * to know if you one-way bind to an object, and then replace that object in the isolated scope.
- * If you now change a property of the object in your parent scope, the change will not be
- * propagated to the isolated scope, because the identity of the object on the parent scope
- * has not changed. Instead you must assign a new object.
- *
- * One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings
- * back to the parent. However, it does not make this completely impossible.
- *
- * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If
- * no `attr` name is specified then the attribute name is assumed to be the same as the local name.
- * Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: {
- * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for
- * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope
- * via an expression to the parent scope. This can be done by passing a map of local variable names
- * and values into the expression wrapper fn. For example, if the expression is `increment(amount)`
- * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`.
- *
- * In general it's possible to apply more than one directive to one element, but there might be limitations
- * depending on the type of scope required by the directives. The following points will help explain these limitations.
- * For simplicity only two directives are taken into account, but it is also applicable for several directives:
- *
- * * **no scope** + **no scope** => Two directives which don't require their own scope will use their parent's scope
- * * **child scope** + **no scope** => Both directives will share one single child scope
- * * **child scope** + **child scope** => Both directives will share one single child scope
- * * **isolated scope** + **no scope** => The isolated directive will use it's own created isolated scope. The other directive will use
- * its parent's scope
- * * **isolated scope** + **child scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives cannot
- * be applied to the same element.
- * * **isolated scope** + **isolated scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives
- * cannot be applied to the same element.
- *
- *
- * #### `bindToController`
- * This property is used to bind scope properties directly to the controller. It can be either
- * `true` or an object hash with the same format as the `scope` property. Additionally, a controller
- * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller
- * definition: `controller: 'myCtrl as myAlias'`.
- *
- * When an isolate scope is used for a directive (see above), `bindToController: true` will
- * allow a component to have its properties bound to the controller, rather than to scope.
- *
- * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller
- * properties. You can access these bindings once they have been initialized by providing a controller method called
- * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings
- * initialized.
- *
- * <div class="alert alert-warning">
- * **Deprecation warning:** although bindings for non-ES6 class controllers are currently
- * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization
- * code that relies upon bindings inside a `$onInit` method on the controller, instead.
- * </div>
- *
- * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property.
- * This will set up the scope bindings to the controller directly. Note that `scope` can still be used
- * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate
- * scope (useful for component directives).
- *
- * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`.
- *
- *
- * #### `controller`
- * Controller constructor function. The controller is instantiated before the
- * pre-linking phase and can be accessed by other directives (see
- * `require` attribute). This allows the directives to communicate with each other and augment
- * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals:
- *
- * * `$scope` - Current scope associated with the element
- * * `$element` - Current element
- * * `$attrs` - Current attributes object for the element
- * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
- * `function([scope], cloneLinkingFn, futureParentElement, slotName)`:
- * * `scope`: (optional) override the scope.
- * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content.
- * * `futureParentElement` (optional):
- * * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
- * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
- * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
- * and when the `cloneLinkinFn` is passed,
- * as those elements need to created and cloned in a special way when they are defined outside their
- * usual containers (e.g. like `<svg>`).
- * * See also the `directive.templateNamespace` property.
- * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`)
- * then the default translusion is provided.
- * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns
- * `true` if the specified slot contains content (i.e. one or more DOM nodes).
- *
- * #### `require`
- * Require another directive and inject its controller as the fourth argument to the linking function. The
- * `require` property can be a string, an array or an object:
- * * a **string** containing the name of the directive to pass to the linking function
- * * an **array** containing the names of directives to pass to the linking function. The argument passed to the
- * linking function will be an array of controllers in the same order as the names in the `require` property
- * * an **object** whose property values are the names of the directives to pass to the linking function. The argument
- * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding
- * controllers.
- *
- * If the `require` property is an object and `bindToController` is truthy, then the required controllers are
- * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers
- * have been constructed but before `$onInit` is called.
- * If the name of the required controller is the same as the local name (the key), the name can be
- * omitted. For example, `{parentDir: '^^'}` is equivalent to `{parentDir: '^^parentDir'}`.
- * See the {@link $compileProvider#component} helper for an example of how this can be used.
- * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is
- * raised (unless no link function is specified and the required controllers are not being bound to the directive
- * controller, in which case error checking is skipped). 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`
- * Identifier name for a reference to the controller in the directive's scope.
- * This allows the controller to be referenced from the directive template. This is especially
- * useful when a directive is used as component, i.e. with an `isolate` scope. It's also possible
- * to use it in a directive without an `isolate` / `new` scope, but you need to be aware that the
- * `controllerAs` reference might overwrite a property that already exists on the parent scope.
- *
- *
- * #### `restrict`
- * String of subset of `EACM` which restricts the directive to a specific directive
- * declaration style. If omitted, the defaults (elements and attributes) are used.
- *
- * * `E` - Element name (default): `<my-directive></my-directive>`
- * * `A` - Attribute (default): `<div my-directive="exp"></div>`
- * * `C` - Class: `<div class="my-directive: exp;"></div>`
- * * `M` - Comment: `<!-- directive: my-directive exp -->`
- *
- *
- * #### `templateNamespace`
- * String representing the document type used by the markup in the template.
- * AngularJS needs this information as those elements need to be created and cloned
- * in a special way when they are defined outside their usual containers like `<svg>` and `<math>`.
- *
- * * `html` - All root nodes in the template are HTML. Root nodes may also be
- * top-level elements such as `<svg>` or `<math>`.
- * * `svg` - The root nodes in the template are SVG elements (excluding `<math>`).
- * * `math` - The root nodes in the template are MathML elements (excluding `<svg>`).
- *
- * If no `templateNamespace` is specified, then the namespace is considered to be `html`.
- *
- * #### `template`
- * HTML markup that may:
- * * Replace the contents of the directive's element (default).
- * * Replace the directive's element itself (if `replace` is true - DEPRECATED).
- * * Wrap the contents of the directive's element (if `transclude` is true).
- *
- * Value may be:
- *
- * * A string. For example `<div red-on-hover>{{delete_str}}</div>`.
- * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile`
- * function api below) and returns a string value.
- *
- *
- * #### `templateUrl`
- * This is similar to `template` but the template is loaded from the specified URL, asynchronously.
- *
- * Because template loading is asynchronous the compiler will suspend compilation of directives on that element
- * for later when the template has been resolved. In the meantime it will continue to compile and link
- * sibling and parent elements as though this element had not contained any directives.
- *
- * The compiler does not suspend the entire compilation to wait for templates to be loaded because this
- * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the
- * case when only one deeply nested directive has `templateUrl`.
- *
- * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache}
- *
- * You can specify `templateUrl` as a string representing the URL or as a function which takes two
- * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns
- * a string value representing the url. In either case, the template URL is passed through {@link
- * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}.
- *
- *
- * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0)
- * specify what the template should replace. Defaults to `false`.
- *
- * * `true` - the template will replace the directive's element.
- * * `false` - the template will replace the contents of the directive's element.
- *
- * The replacement process migrates all of the attributes / classes from the old element to the new
- * one. See the {@link guide/directive#template-expanding-directive
- * Directives Guide} for an example.
- *
- * 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`
- * 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.
- *
- *
- * #### `compile`
- *
- * ```js
- * function compile(tElement, tAttrs, transclude) { ... }
- * ```
- *
- * The compile function deals with transforming the template DOM. Since most directives do not do
- * template transformation, it is not used often. The compile function takes the following arguments:
- *
- * * `tElement` - template element - The element where the directive has been declared. It is
- * safe to do template transformation on the element and child elements only.
- *
- * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared
- * between all directive compile functions.
- *
- * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)`
- *
- * <div class="alert alert-warning">
- * **Note:** The template instance and the link instance may be different objects if the template has
- * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that
- * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration
- * should be done in a linking function rather than in a compile function.
- * </div>
-
- * <div class="alert alert-warning">
- * **Note:** The compile function cannot handle directives that recursively use themselves in their
- * own templates or compile functions. Compiling these directives results in an infinite loop and
- * stack overflow errors.
- *
- * This can be avoided by manually using $compile in the postLink function to imperatively compile
- * a directive's template instead of relying on automatic template compilation via `template` or
- * `templateUrl` declaration or manual compilation inside the compile function.
- * </div>
- *
- * <div class="alert alert-danger">
- * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it
- * e.g. does not know about the right outer scope. Please use the transclude function that is passed
- * to the link function instead.
- * </div>
-
- * A compile function can have a return value which can be either a function or an object.
- *
- * * returning a (post-link) function - is equivalent to registering the linking function via the
- * `link` property of the config object when the compile function is empty.
- *
- * * returning an object with function(s) registered via `pre` and `post` properties - allows you to
- * control when a linking function should be called during the linking phase. See info about
- * pre-linking and post-linking functions below.
- *
- *
- * #### `link`
- * This property is used only if the `compile` property is not defined.
- *
- * ```js
- * function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
- * ```
- *
- * The link function is responsible for registering DOM listeners as well as updating the DOM. It is
- * executed after the template has been cloned. This is where most of the directive logic will be
- * put.
- *
- * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the
- * directive for registering {@link ng.$rootScope.Scope#$watch watches}.
- *
- * * `iElement` - instance element - The element where the directive is to be used. It is safe to
- * manipulate the children of the element only in `postLink` function since the children have
- * already been linked.
- *
- * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared
- * between all directive linking functions.
- *
- * * `controller` - the directive's required controller instance(s) - Instances are shared
- * among all directives, which allows the directives to use the controllers as a communication
- * channel. The exact value depends on the directive's `require` property:
- * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one
- * * `string`: the controller instance
- * * `array`: array of controller instances
- *
- * If a required controller cannot be found, and it is optional, the instance is `null`,
- * otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown.
- *
- * Note that you can also require the directive's own controller - it will be made available like
- * any other controller.
- *
- * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
- * This is the same as the `$transclude` parameter of directive controllers,
- * see {@link ng.$compile#-controller- the controller section for details}.
- * `function([scope], cloneLinkingFn, futureParentElement)`.
- *
- * #### Pre-linking function
- *
- * Executed before the child elements are linked. Not safe to do DOM transformation since the
- * compiler linking function will fail to locate the correct elements for linking.
- *
- * #### Post-linking function
- *
- * Executed after the child elements are linked.
- *
- * Note that child elements that contain `templateUrl` directives will not have been compiled
- * and linked since they are waiting for their template to load asynchronously and their own
- * 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.
- *
- *
- * ### Transclusion
- *
- * Transclusion is the process of extracting a collection of DOM elements 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>
- *
- * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
- * directive's element, the entire element or multiple parts of the element contents:
- *
- * * `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.
- * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
- *
- * **Mult-slot transclusion** is declared by providing an object for the `transclude` property.
- *
- * This object is a map where the keys are the name of the slot to fill and the value is an element selector
- * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`)
- * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc).
- *
- * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
- *
- * If the element selector is prefixed with a `?` then that slot is optional.
- *
- * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `<my-custom-element>` elements to
- * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive.
- *
- * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements
- * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call
- * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and
- * injectable into the directive's controller.
- *
- *
- * #### 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 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 transclude 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 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 transcluded 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.
- *
- * * *Accessing normalized attribute names:* Directives like 'ngBind' can be expressed in many ways:
- * 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. The attributes object allows for normalized access
- * to the attributes.
- *
- * * *Directive inter-communication:* All directives share the same instance of the attributes
- * object which allows the directives to use the attributes object as inter directive
- * communication.
- *
- * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object
- * allowing other directives to read the interpolated value.
- *
- * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes
- * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also
- * the only way to easily get the actual value because during the linking phase the interpolation
- * hasn't been evaluated yet and so the value is at this time set to `undefined`.
- *
- * ```js
- * function linkingFn(scope, elm, attrs, ctrl) {
- * // get the attribute value
- * console.log(attrs.ngModel);
- *
- * // change the attribute
- * attrs.$set('ngModel', 'new value');
- *
- * // observe changes to interpolated attribute
- * attrs.$observe('ngModel', function(value) {
- * console.log('ngModel has changed value to ' + value);
- * });
- * }
- * ```
- *
- * ## Example
- *
- * <div class="alert alert-warning">
- * **Note**: Typically directives are registered with `module.directive`. The example below is
- * to illustrate how `$compile` works.
- * </div>
- *
- <example module="compileExample">
- <file name="index.html">
- <script>
- angular.module('compileExample', [], function($compileProvider) {
- // configure new 'compile' directive by passing a directive
- // factory function. The factory function injects the '$compile'
- $compileProvider.directive('compile', function($compile) {
- // directive factory creates a link function
- return function(scope, element, attrs) {
- scope.$watch(
- function(scope) {
- // watch the 'compile' expression for changes
- return scope.$eval(attrs.compile);
- },
- function(value) {
- // when the 'compile' expression changes
- // assign it into the current DOM
- element.html(value);
-
- // compile the new DOM and link it to the current
- // scope.
- // NOTE: we only compile .childNodes so that
- // we don't get into infinite loop compiling ourselves
- $compile(element.contents())(scope);
- }
- );
- };
- });
- })
- .controller('GreeterController', ['$scope', function($scope) {
- $scope.name = 'Angular';
- $scope.html = 'Hello {{name}}';
- }]);
- </script>
- <div ng-controller="GreeterController">
- <input ng-model="name"> <br/>
- <textarea ng-model="html"></textarea> <br/>
- <div compile="html"></div>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should auto compile', function() {
- var textarea = $('textarea');
- var output = $('div[compile]');
- // The initial state reads 'Hello Angular'.
- expect(output.getText()).toBe('Hello Angular');
- textarea.clear();
- textarea.sendKeys('{{name}}!');
- expect(output.getText()).toBe('Angular!');
- });
- </file>
- </example>
-
- *
- *
- * @param {string|DOMElement} element Element or HTML string to compile into a template function.
- * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED.
- *
- * <div class="alert alert-danger">
- * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it
- * e.g. will not use the right outer scope. Please pass the transclude function as a
- * `parentBoundTranscludeFn` to the link function instead.
- * </div>
- *
- * @param {number} maxPriority only apply directives lower than given priority (Only effects the
- * root element(s), not their children)
- * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template
- * (a DOM element/tree) to a scope. Where:
- *
- * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
- * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
- * `template` and call the `cloneAttachFn` function allowing the caller to attach the
- * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
- * called as: <br/> `cloneAttachFn(clonedElement, scope)` where:
- *
- * * `clonedElement` - is a clone of the original `element` passed into the compiler.
- * * `scope` - is the current scope with which the linking function is working with.
- *
- * * `options` - An optional object hash with linking options. If `options` is provided, then the following
- * keys may be used to control linking behavior:
- *
- * * `parentBoundTranscludeFn` - the transclude function made available to
- * directives; if given, it will be passed through to the link functions of
- * directives found in `element` during compilation.
- * * `transcludeControllers` - an object hash with keys that map controller names
- * to a hash with the key `instance`, which maps to the controller instance;
- * if given, it will make the controllers available to directives on the compileNode:
- * ```
- * {
- * parent: {
- * instance: parentControllerInstance
- * }
- * }
- * ```
- * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add
- * the cloned elements; only needed for transcludes that are allowed to contain non html
- * elements (e.g. SVG elements). See also the directive.controller property.
- *
- * Calling the linking function returns the element of the template. It is either the original
- * element passed in, or the clone of the element if the `cloneAttachFn` is provided.
- *
- * After linking the view is not updated until after a call to $digest which typically is done by
- * Angular automatically.
- *
- * If you need access to the bound view, there are two ways to do it:
- *
- * - If you are not asking the linking function to clone the template, create the DOM element(s)
- * before you send them to the compiler and keep this reference around.
- * ```js
- * var element = $compile('<p>{{total}}</p>')(scope);
- * ```
- *
- * - if on the other hand, you need the element to be cloned, the view reference from the original
- * example would not point to the clone, but rather to the original template that was cloned. In
- * this case, you can access the clone via the cloneAttachFn:
- * ```js
- * var templateElement = angular.element('<p>{{total}}</p>'),
- * scope = ....;
- *
- * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) {
- * //attach the clone to DOM document at the right place
- * });
- *
- * //now we have reference to the cloned DOM via `clonedElement`
- * ```
- *
- *
- * For information on how the compiler works, see the
- * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
- */
-
-var $compileMinErr = minErr('$compile');
-
-function UNINITIALIZED_VALUE() {}
-var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
-
-/**
- * @ngdoc provider
- * @name $compileProvider
- *
- * @description
- */
-$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider'];
-function $CompileProvider($provide, $$sanitizeUriProvider) {
- var hasDirectives = {},
- Suffix = 'Directive',
- COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/,
- CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/,
- 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)$/;
- var bindingCache = createMap();
-
- function parseIsolateBindings(scope, directiveName, isController) {
- var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/;
-
- var bindings = createMap();
-
- forEach(scope, function(definition, scopeName) {
- if (definition in bindingCache) {
- bindings[scopeName] = bindingCache[definition];
- return;
- }
- var match = definition.match(LOCAL_REGEXP);
-
- if (!match) {
- throw $compileMinErr('iscp',
- "Invalid {3} for directive '{0}'." +
- " Definition: {... {1}: '{2}' ...}",
- directiveName, scopeName, definition,
- (isController ? "controller bindings definition" :
- "isolate scope definition"));
- }
-
- bindings[scopeName] = {
- mode: match[1][0],
- collection: match[2] === '*',
- optional: match[3] === '?',
- attrName: match[4] || scopeName
- };
- if (match[4]) {
- bindingCache[definition] = bindings[scopeName];
- }
- });
-
- return bindings;
- }
-
- function parseDirectiveBindings(directive, directiveName) {
- var bindings = {
- isolateScope: null,
- bindToController: null
- };
- if (isObject(directive.scope)) {
- if (directive.bindToController === true) {
- bindings.bindToController = parseIsolateBindings(directive.scope,
- directiveName, true);
- bindings.isolateScope = {};
- } else {
- bindings.isolateScope = parseIsolateBindings(directive.scope,
- directiveName, false);
- }
- }
- if (isObject(directive.bindToController)) {
- bindings.bindToController =
- parseIsolateBindings(directive.bindToController, directiveName, true);
- }
- if (isObject(bindings.bindToController)) {
- var controller = directive.controller;
- var controllerAs = directive.controllerAs;
- if (!controller) {
- // There is no controller, there may or may not be a controllerAs property
- throw $compileMinErr('noctrl',
- "Cannot bind to controller without directive '{0}'s controller.",
- directiveName);
- } else if (!identifierForController(controller, controllerAs)) {
- // There is a controller, but no identifier or controllerAs property
- throw $compileMinErr('noident',
- "Cannot bind to controller without identifier for directive '{0}'.",
- directiveName);
- }
- }
- return bindings;
- }
-
- function assertValidDirectiveName(name) {
- var letter = name.charAt(0);
- if (!letter || letter !== lowercase(letter)) {
- throw $compileMinErr('baddir', "Directive/Component name '{0}' is invalid. The first character must be a lowercase letter", name);
- }
- if (name !== name.trim()) {
- throw $compileMinErr('baddir',
- "Directive/Component name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
- name);
- }
- }
-
- function getDirectiveRequire(directive) {
- var require = directive.require || (directive.controller && directive.name);
-
- if (!isArray(require) && isObject(require)) {
- forEach(require, function(value, key) {
- var match = value.match(REQUIRE_PREFIX_REGEXP);
- var name = value.substring(match[0].length);
- if (!name) require[key] = match[0] + key;
- });
- }
-
- return require;
- }
-
- /**
- * @ngdoc method
- * @name $compileProvider#directive
- * @kind function
- *
- * @description
- * Register a new directive with the compiler.
- *
- * @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which
- * will match as <code>ng-bind</code>), or an object map of directives where the keys are the
- * names and the values are the factories.
- * @param {Function|Array} directiveFactory An injectable directive factory function. See the
- * {@link guide/directive directive guide} and the {@link $compile compile API} for more info.
- * @returns {ng.$compileProvider} Self for chaining.
- */
- this.directive = function registerDirective(name, directiveFactory) {
- assertNotHasOwnProperty(name, 'directive');
- if (isString(name)) {
- assertValidDirectiveName(name);
- assertArg(directiveFactory, 'directiveFactory');
- if (!hasDirectives.hasOwnProperty(name)) {
- hasDirectives[name] = [];
- $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
- function($injector, $exceptionHandler) {
- var directives = [];
- forEach(hasDirectives[name], function(directiveFactory, index) {
- try {
- var directive = $injector.invoke(directiveFactory);
- if (isFunction(directive)) {
- directive = { compile: valueFn(directive) };
- } else if (!directive.compile && directive.link) {
- directive.compile = valueFn(directive.link);
- }
- directive.priority = directive.priority || 0;
- directive.index = index;
- directive.name = directive.name || name;
- directive.require = getDirectiveRequire(directive);
- directive.restrict = directive.restrict || 'EA';
- directive.$$moduleName = directiveFactory.$$moduleName;
- directives.push(directive);
- } catch (e) {
- $exceptionHandler(e);
- }
- });
- return directives;
- }]);
- }
- hasDirectives[name].push(directiveFactory);
- } else {
- forEach(name, reverseParams(registerDirective));
- }
- return this;
- };
-
- /**
- * @ngdoc method
- * @name $compileProvider#component
- * @module ng
- * @param {string} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`)
- * @param {Object} options Component definition object (a simplified
- * {@link ng.$compile#directive-definition-object directive definition object}),
- * with the following properties (all optional):
- *
- * - `controller` – `{(string|function()=}` – controller constructor function that should be
- * associated with newly created scope or the name of a {@link ng.$compile#-controller-
- * registered controller} if passed as a string. An empty `noop` function by default.
- * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope.
- * If present, the controller will be published to scope under the `controllerAs` name.
- * If not present, this will default to be `$ctrl`.
- * - `template` – `{string=|function()=}` – html template as a string or a function that
- * returns an html template as a string which should be used as the contents of this component.
- * Empty string by default.
- *
- * If `template` is a function, then it is {@link auto.$injector#invoke injected} with
- * the following locals:
- *
- * - `$element` - Current element
- * - `$attrs` - Current attributes object for the element
- *
- * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
- * template that should be used as the contents of this component.
- *
- * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with
- * the following locals:
- *
- * - `$element` - Current element
- * - `$attrs` - Current attributes object for the element
- *
- * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties.
- * Component properties are always bound to the component controller and not to the scope.
- * See {@link ng.$compile#-bindtocontroller- `bindToController`}.
- * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled.
- * Disabled by default.
- * - `require` - `{Object<string, string>=}` - requires the controllers of other directives and binds them to
- * this component's controller. The object keys specify the property names under which the required
- * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}.
- * - `$...` – additional properties to attach to the directive factory function and the controller
- * constructor function. (This is used by the component router to annotate)
- *
- * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls.
- * @description
- * Register a **component definition** with the compiler. This is a shorthand for registering a special
- * type of directive, which represents a self-contained UI component in your application. Such components
- * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`).
- *
- * Component definitions are very simple and do not require as much configuration as defining general
- * directives. Component definitions usually consist only of a template and a controller backing it.
- *
- * In order to make the definition easier, components enforce best practices like use of `controllerAs`,
- * `bindToController`. They always have **isolate scope** and are restricted to elements.
- *
- * Here are a few examples of how you would usually define components:
- *
- * ```js
- * var myMod = angular.module(...);
- * myMod.component('myComp', {
- * template: '<div>My name is {{$ctrl.name}}</div>',
- * controller: function() {
- * this.name = 'shahar';
- * }
- * });
- *
- * myMod.component('myComp', {
- * template: '<div>My name is {{$ctrl.name}}</div>',
- * bindings: {name: '@'}
- * });
- *
- * myMod.component('myComp', {
- * templateUrl: 'views/my-comp.html',
- * controller: 'MyCtrl',
- * controllerAs: 'ctrl',
- * bindings: {name: '@'}
- * });
- *
- * ```
- * For more examples, and an in-depth guide, see the {@link guide/component component guide}.
- *
- * <br />
- * See also {@link ng.$compileProvider#directive $compileProvider.directive()}.
- */
- this.component = function registerComponent(name, options) {
- var controller = options.controller || function() {};
-
- function factory($injector) {
- function makeInjectable(fn) {
- if (isFunction(fn) || isArray(fn)) {
- return function(tElement, tAttrs) {
- return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
- };
- } else {
- return fn;
- }
- }
-
- var template = (!options.template && !options.templateUrl ? '' : options.template);
- var ddo = {
- controller: controller,
- controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
- template: makeInjectable(template),
- templateUrl: makeInjectable(options.templateUrl),
- transclude: options.transclude,
- scope: {},
- bindToController: options.bindings || {},
- restrict: 'E',
- require: options.require
- };
-
- // Copy annotations (starting with $) over to the DDO
- forEach(options, function(val, key) {
- if (key.charAt(0) === '$') ddo[key] = val;
- });
-
- return ddo;
- }
-
- // TODO(pete) remove the following `forEach` before we release 1.6.0
- // The component-router@0.2.0 looks for the annotations on the controller constructor
- // Nothing in Angular looks for annotations on the factory function but we can't remove
- // it from 1.5.x yet.
-
- // Copy any annotation properties (starting with $) over to the factory and controller constructor functions
- // These could be used by libraries such as the new component router
- forEach(options, function(val, key) {
- if (key.charAt(0) === '$') {
- factory[key] = val;
- // Don't try to copy over annotations to named controller
- if (isFunction(controller)) controller[key] = val;
- }
- });
-
- factory.$inject = ['$injector'];
-
- return this.directive(name, factory);
- };
-
-
- /**
- * @ngdoc method
- * @name $compileProvider#aHrefSanitizationWhitelist
- * @kind function
- *
- * @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
- * urls during a[href] sanitization.
- *
- * The sanitization is a security measure aimed at preventing XSS attacks via html links.
- *
- * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
- *
- * @param {RegExp=} regexp New regexp to whitelist urls with.
- * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
- * chaining otherwise.
- */
- this.aHrefSanitizationWhitelist = function(regexp) {
- if (isDefined(regexp)) {
- $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp);
- return this;
- } else {
- return $$sanitizeUriProvider.aHrefSanitizationWhitelist();
- }
- };
-
-
- /**
- * @ngdoc method
- * @name $compileProvider#imgSrcSanitizationWhitelist
- * @kind function
- *
- * @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
- * urls during img[src] sanitization.
- *
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
- *
- * Any url about to be assigned to img[src] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
- *
- * @param {RegExp=} regexp New regexp to whitelist urls with.
- * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
- * chaining otherwise.
- */
- this.imgSrcSanitizationWhitelist = function(regexp) {
- if (isDefined(regexp)) {
- $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp);
- return this;
- } else {
- return $$sanitizeUriProvider.imgSrcSanitizationWhitelist();
- }
- };
-
- /**
- * @ngdoc method
- * @name $compileProvider#debugInfoEnabled
- *
- * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the
- * current debugInfoEnabled state
- * @returns {*} current value if used as getter or itself (chaining) if used as setter
- *
- * @kind function
- *
- * @description
- * Call this method to enable/disable various debug runtime information in the compiler such as adding
- * binding information and a reference to the current scope on to DOM elements.
- * If enabled, the compiler will add the following to DOM elements that have been bound to the scope
- * * `ng-binding` CSS class
- * * `$binding` data property containing an array of the binding expressions
- *
- * You may want to disable this in production for a significant performance boost. See
- * {@link guide/production#disabling-debug-data Disabling Debug Data} for more.
- *
- * The default value is true.
- */
- var debugInfoEnabled = true;
- this.debugInfoEnabled = function(enabled) {
- if (isDefined(enabled)) {
- debugInfoEnabled = enabled;
- return this;
- }
- return debugInfoEnabled;
- };
-
-
- var TTL = 10;
- /**
- * @ngdoc method
- * @name $compileProvider#onChangesTtl
- * @description
- *
- * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
- * assuming that the model is unstable.
- *
- * The current default is 10 iterations.
- *
- * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
- * in several iterations of calls to these hooks. However if an application needs more than the default 10
- * iterations to stabilize then you should investigate what is causing the model to continuously change during
- * the `$onChanges` hook execution.
- *
- * Increasing the TTL could have performance implications, so you should not change it without proper justification.
- *
- * @param {number} limit The number of `$onChanges` hook iterations.
- * @returns {number|object} the current limit (or `this` if called as a setter for chaining)
- */
- this.onChangesTtl = function(value) {
- if (arguments.length) {
- TTL = value;
- return this;
- }
- return TTL;
- };
-
- this.$get = [
- '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
- '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
- function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
- $controller, $rootScope, $sce, $animate, $$sanitizeUri) {
-
- var SIMPLE_ATTR_NAME = /^\w/;
- var specialAttrHolder = window.document.createElement('div');
-
-
-
- var onChangesTtl = TTL;
- // The onChanges hooks should all be run together in a single digest
- // When changes occur, the call to trigger their hooks will be added to this queue
- var onChangesQueue;
-
- // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
- function flushOnChangesQueue() {
- try {
- if (!(--onChangesTtl)) {
- // We have hit the TTL limit so reset everything
- onChangesQueue = undefined;
- throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL);
- }
- // We must run this hook in an apply since the $$postDigest runs outside apply
- $rootScope.$apply(function() {
- var errors = [];
- for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
- try {
- onChangesQueue[i]();
- } catch (e) {
- errors.push(e);
- }
- }
- // Reset the queue to trigger a new schedule next time there is a change
- onChangesQueue = undefined;
- if (errors.length) {
- throw errors;
- }
- });
- } finally {
- onChangesTtl++;
- }
- }
-
-
- function Attributes(element, attributesToCopy) {
- if (attributesToCopy) {
- var keys = Object.keys(attributesToCopy);
- var i, l, key;
-
- for (i = 0, l = keys.length; i < l; i++) {
- key = keys[i];
- this[key] = attributesToCopy[key];
- }
- } else {
- this.$attr = {};
- }
-
- this.$$element = element;
- }
-
- Attributes.prototype = {
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$normalize
- * @kind function
- *
- * @description
- * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or
- * `data-`) to its normalized, camelCase form.
- *
- * Also there is special case for Moz prefix starting with upper case letter.
- *
- * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
- *
- * @param {string} name Name to normalize
- */
- $normalize: directiveNormalize,
-
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$addClass
- * @kind function
- *
- * @description
- * Adds the CSS class value specified by the classVal parameter to the element. If animations
- * are enabled then an animation will be triggered for the class addition.
- *
- * @param {string} classVal The className value that will be added to the element
- */
- $addClass: function(classVal) {
- if (classVal && classVal.length > 0) {
- $animate.addClass(this.$$element, classVal);
- }
- },
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$removeClass
- * @kind function
- *
- * @description
- * Removes the CSS class value specified by the classVal parameter from the element. If
- * animations are enabled then an animation will be triggered for the class removal.
- *
- * @param {string} classVal The className value that will be removed from the element
- */
- $removeClass: function(classVal) {
- if (classVal && classVal.length > 0) {
- $animate.removeClass(this.$$element, classVal);
- }
- },
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$updateClass
- * @kind function
- *
- * @description
- * Adds and removes the appropriate CSS class values to the element based on the difference
- * between the new and old CSS class values (specified as newClasses and oldClasses).
- *
- * @param {string} newClasses The current CSS className value
- * @param {string} oldClasses The former CSS className value
- */
- $updateClass: function(newClasses, oldClasses) {
- var toAdd = tokenDifference(newClasses, oldClasses);
- if (toAdd && toAdd.length) {
- $animate.addClass(this.$$element, toAdd);
- }
-
- var toRemove = tokenDifference(oldClasses, newClasses);
- if (toRemove && toRemove.length) {
- $animate.removeClass(this.$$element, toRemove);
- }
- },
-
- /**
- * Set a normalized attribute on the element in a way such that all directives
- * can share the attribute. This function properly handles boolean attributes.
- * @param {string} key Normalized key. (ie ngAttribute)
- * @param {string|boolean} value The value to set. If `null` attribute will be deleted.
- * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
- * Defaults to true.
- * @param {string=} attrName Optional none normalized name. Defaults to key.
- */
- $set: function(key, value, writeAttr, attrName) {
- // TODO: decide whether or not to throw an error if "class"
- //is set through this function since it may cause $updateClass to
- //become unstable.
-
- var node = this.$$element[0],
- booleanKey = getBooleanAttrName(node, key),
- aliasedKey = getAliasedAttrName(key),
- observer = key,
- nodeName;
-
- if (booleanKey) {
- this.$$element.prop(key, value);
- attrName = booleanKey;
- } else if (aliasedKey) {
- this[aliasedKey] = value;
- observer = aliasedKey;
- }
-
- this[key] = value;
-
- // translate normalized key to actual key
- if (attrName) {
- this.$attr[key] = attrName;
- } else {
- attrName = this.$attr[key];
- if (!attrName) {
- this.$attr[key] = attrName = snake_case(key, '-');
- }
- }
-
- nodeName = nodeName_(this.$$element);
-
- if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
- (nodeName === 'img' && key === 'src')) {
- // sanitize a[href] and img[src] values
- this[key] = value = $$sanitizeUri(value, key === 'src');
- } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
- // 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 || isUndefined(value)) {
- this.$$element.removeAttr(attrName);
- } else {
- if (SIMPLE_ATTR_NAME.test(attrName)) {
- this.$$element.attr(attrName, value);
- } else {
- setSpecialAttr(this.$$element[0], attrName, value);
- }
- }
- }
-
- // fire observers
- var $$observers = this.$$observers;
- $$observers && forEach($$observers[observer], function(fn) {
- try {
- fn(value);
- } catch (e) {
- $exceptionHandler(e);
- }
- });
- },
-
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$observe
- * @kind function
- *
- * @description
- * Observes an interpolated attribute.
- *
- * The observer function will be invoked once during the next `$digest` following
- * compilation. The observer is then invoked whenever the interpolated value
- * 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/interpolation#how-text-and-attribute-bindings-work Interpolation
- * guide} for more info.
- * @returns {function()} Returns a deregistration function for this observer.
- */
- $observe: function(key, fn) {
- var attrs = this,
- $$observers = (attrs.$$observers || (attrs.$$observers = createMap())),
- listeners = ($$observers[key] || ($$observers[key] = []));
-
- listeners.push(fn);
- $rootScope.$evalAsync(function() {
- if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) {
- // no one registered attribute interpolation function, so lets call it manually
- fn(attrs[key]);
- }
- });
-
- return function() {
- arrayRemove(listeners, fn);
- };
- }
- };
-
- function setSpecialAttr(element, attrName, value) {
- // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute`
- // so we have to jump through some hoops to get such an attribute
- // https://github.com/angular/angular.js/pull/13318
- specialAttrHolder.innerHTML = "<span " + attrName + ">";
- var attributes = specialAttrHolder.firstChild.attributes;
- var attribute = attributes[0];
- // We have to remove the attribute from its container element before we can add it to the destination element
- attributes.removeNamedItem(attribute.name);
- attribute.value = value;
- element.attributes.setNamedItem(attribute);
- }
-
- function safeAddClass($element, className) {
- try {
- $element.addClass(className);
- } catch (e) {
- // ignore, since it means that we are trying to set class on
- // SVG element, where class name is read-only.
- }
- }
-
-
- var startSymbol = $interpolate.startSymbol(),
- endSymbol = $interpolate.endSymbol(),
- denormalizeTemplate = (startSymbol == '{{' && endSymbol == '}}')
- ? identity
- : function denormalizeTemplate(template) {
- return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
- },
- NG_ATTR_BINDING = /^ngAttr[A-Z]/;
- var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
-
- compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
- var bindings = $element.data('$binding') || [];
-
- if (isArray(binding)) {
- bindings = bindings.concat(binding);
- } else {
- bindings.push(binding);
- }
-
- $element.data('$binding', bindings);
- } : noop;
-
- compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) {
- safeAddClass($element, 'ng-binding');
- } : noop;
-
- compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) {
- var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
- $element.data(dataName, scope);
- } : noop;
-
- compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) {
- safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope');
- } : noop;
-
- compile.$$createComment = function(directiveName, comment) {
- var content = '';
- if (debugInfoEnabled) {
- content = ' ' + (directiveName || '') + ': ';
- if (comment) content += comment + ' ';
- }
- return window.document.createComment(content);
- };
-
- return compile;
-
- //================================
-
- function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,
- previousCompileContext) {
- if (!($compileNodes instanceof jqLite)) {
- // jquery always rewraps, whereas we need to preserve the original selector so that we can
- // modify it.
- $compileNodes = jqLite($compileNodes);
- }
-
- var NOT_EMPTY = /\S+/;
-
- // We can not compile top level text elements since text nodes can be merged and we will
- // not be able to attach scope data to them, so we will wrap them in <span>
- for (var i = 0, len = $compileNodes.length; i < len; i++) {
- var domNode = $compileNodes[i];
-
- if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) {
- jqLiteWrapNode(domNode, $compileNodes[i] = window.document.createElement('span'));
- }
- }
-
- var compositeLinkFn =
- compileNodes($compileNodes, transcludeFn, $compileNodes,
- maxPriority, ignoreDirective, previousCompileContext);
- compile.$$addScopeClass($compileNodes);
- var namespace = null;
- return function publicLinkFn(scope, cloneConnectFn, options) {
- assertArg(scope, 'scope');
-
- if (previousCompileContext && previousCompileContext.needsNewScope) {
- // A parent directive did a replace and a directive on this element asked
- // for transclusion, which caused us to lose a layer of element on which
- // we could hold the new transclusion scope, so we will create it manually
- // here.
- scope = scope.$parent.$new();
- }
-
- options = options || {};
- var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
- transcludeControllers = options.transcludeControllers,
- futureParentElement = options.futureParentElement;
-
- // When `parentBoundTranscludeFn` is passed, it is a
- // `controllersBoundTransclude` function (it was previously passed
- // as `transclude` to directive.link) so we must unwrap it to get
- // its `boundTranscludeFn`
- if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) {
- parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude;
- }
-
- if (!namespace) {
- namespace = detectNamespaceForChildElements(futureParentElement);
- }
- 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;
- }
-
- if (transcludeControllers) {
- for (var controllerName in transcludeControllers) {
- $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance);
- }
- }
-
- compile.$$addScopeInfo($linkNode, scope);
-
- if (cloneConnectFn) cloneConnectFn($linkNode, scope);
- if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
- return $linkNode;
- };
- }
-
- function detectNamespaceForChildElements(parentElement) {
- // TODO: Make this detect MathML as well...
- var node = parentElement && parentElement[0];
- if (!node) {
- return 'html';
- } else {
- return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html';
- }
- }
-
- /**
- * Compile function matches each node in nodeList against the directives. Once all directives
- * for a particular node are collected their compile functions are executed. The compile
- * functions return values - the linking functions - are combined into a composite linking
- * function, which is the a linking function for the node.
- *
- * @param {NodeList} nodeList an array of nodes or NodeList to compile
- * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the
- * scope argument is auto-generated to the new child of the transcluded parent scope.
- * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then
- * the rootElement must be set the jqLite collection of the compile root. This is
- * needed so that the jqLite collection items can be replaced with widgets.
- * @param {number=} maxPriority Max directive priority.
- * @returns {Function} A composite linking function of all of the matched directives or null.
- */
- function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,
- previousCompileContext) {
- var linkFns = [],
- attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;
-
- for (var i = 0; i < nodeList.length; i++) {
- attrs = new Attributes();
-
- // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
- directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,
- ignoreDirective);
-
- nodeLinkFn = (directives.length)
- ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement,
- null, [], [], previousCompileContext)
- : null;
-
- if (nodeLinkFn && nodeLinkFn.scope) {
- compile.$$addScopeClass(attrs.$$element);
- }
-
- childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
- !(childNodes = nodeList[i].childNodes) ||
- !childNodes.length)
- ? null
- : compileNodes(childNodes,
- nodeLinkFn ? (
- (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
- && nodeLinkFn.transclude) : transcludeFn);
-
- if (nodeLinkFn || childLinkFn) {
- linkFns.push(i, nodeLinkFn, childLinkFn);
- linkFnFound = true;
- nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn;
- }
-
- //use the previous context only for the first element in the virtual group
- previousCompileContext = null;
- }
-
- // return a linking function if we have found anything, null otherwise
- return linkFnFound ? compositeLinkFn : null;
-
- function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
- var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn;
- var stableNodeList;
-
-
- if (nodeLinkFnFound) {
- // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our
- // offsets don't get screwed up
- var nodeListLength = nodeList.length;
- stableNodeList = new Array(nodeListLength);
-
- // create a sparse array by only copying the elements which have a linkFn
- for (i = 0; i < linkFns.length; i+=3) {
- idx = linkFns[i];
- stableNodeList[idx] = nodeList[idx];
- }
- } else {
- stableNodeList = nodeList;
- }
-
- for (i = 0, ii = linkFns.length; i < ii;) {
- node = stableNodeList[linkFns[i++]];
- nodeLinkFn = linkFns[i++];
- childLinkFn = linkFns[i++];
-
- if (nodeLinkFn) {
- if (nodeLinkFn.scope) {
- childScope = scope.$new();
- compile.$$addScopeInfo(jqLite(node), childScope);
- } else {
- childScope = scope;
- }
-
- if (nodeLinkFn.transcludeOnThisElement) {
- childBoundTranscludeFn = createBoundTranscludeFn(
- scope, nodeLinkFn.transclude, parentBoundTranscludeFn);
-
- } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
- childBoundTranscludeFn = parentBoundTranscludeFn;
-
- } else if (!parentBoundTranscludeFn && transcludeFn) {
- childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn);
-
- } else {
- childBoundTranscludeFn = null;
- }
-
- nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
-
- } else if (childLinkFn) {
- childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
- }
- }
- }
- }
-
- function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
- function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
-
- if (!transcludedScope) {
- transcludedScope = scope.$new(false, containingScope);
- transcludedScope.$$transcluded = true;
- }
-
- return transcludeFn(transcludedScope, cloneFn, {
- parentBoundTranscludeFn: previousBoundTranscludeFn,
- transcludeControllers: controllers,
- futureParentElement: futureParentElement
- });
- }
-
- // We need to attach the transclusion slots onto the `boundTranscludeFn`
- // so that they are available inside the `controllersBoundTransclude` function
- var boundSlots = boundTranscludeFn.$$slots = createMap();
- for (var slotName in transcludeFn.$$slots) {
- if (transcludeFn.$$slots[slotName]) {
- boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
- } else {
- boundSlots[slotName] = null;
- }
- }
-
- return boundTranscludeFn;
- }
-
- /**
- * Looks for directives on the given node and adds them to the directive collection which is
- * sorted.
- *
- * @param node Node to search.
- * @param directives An array to which the directives are added to. This array is sorted before
- * the function returns.
- * @param attrs The shared attrs object which is used to populate the normalized attributes.
- * @param {number=} maxPriority Max directive priority.
- */
- function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
- var nodeType = node.nodeType,
- attrsMap = attrs.$attr,
- match,
- className;
-
- switch (nodeType) {
- case NODE_TYPE_ELEMENT: /* Element */
- // use the node name: <directive>
- addDirective(directives,
- directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective);
-
- // iterate over the attributes
- for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
- j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
- var attrStartName = false;
- var attrEndName = false;
-
- attr = nAttrs[j];
- name = attr.name;
- value = trim(attr.value);
-
- // support ngAttr attribute binding
- ngAttrName = directiveNormalize(name);
- if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
- name = name.replace(PREFIX_REGEXP, '')
- .substr(8).replace(/_(.)/g, function(match, letter) {
- return letter.toUpperCase();
- });
- }
-
- var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
- if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
- 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);
- }
-
- // use class as directive
- className = node.className;
- if (isObject(className)) {
- // Maybe SVGAnimatedString
- className = className.animVal;
- }
- if (isString(className) && className !== '') {
- while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
- nName = directiveNormalize(match[2]);
- if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) {
- attrs[nName] = trim(match[3]);
- }
- className = className.substr(match.index + match[0].length);
- }
- }
- break;
- case NODE_TYPE_TEXT: /* Text Node */
- if (msie === 11) {
- // Workaround for #11781
- while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) {
- node.nodeValue = node.nodeValue + node.nextSibling.nodeValue;
- node.parentNode.removeChild(node.nextSibling);
- }
- }
- addTextInterpolateDirective(directives, node.nodeValue);
- break;
- case NODE_TYPE_COMMENT: /* Comment */
- collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective);
- break;
- }
-
- directives.sort(byPriority);
- return directives;
- }
-
- function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
- // function created because of performance, try/catch disables
- // the optimization of the whole function #14848
- try {
- var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
- if (match) {
- var nName = directiveNormalize(match[1]);
- if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
- attrs[nName] = trim(match[2]);
- }
- }
- } catch (e) {
- // turns out that under some circumstances IE9 throws errors when one attempts to read
- // comment's node value.
- // Just ignore it and continue. (Can't seem to reproduce in test case.)
- }
- }
-
- /**
- * Given a node with an directive-start it collects all of the siblings until it finds
- * directive-end.
- * @param node
- * @param attrStart
- * @param attrEnd
- * @returns {*}
- */
- function groupScan(node, attrStart, attrEnd) {
- var nodes = [];
- var depth = 0;
- if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
- do {
- if (!node) {
- throw $compileMinErr('uterdir',
- "Unterminated attribute, found '{0}' but no matching '{1}' found.",
- attrStart, attrEnd);
- }
- if (node.nodeType == NODE_TYPE_ELEMENT) {
- if (node.hasAttribute(attrStart)) depth++;
- if (node.hasAttribute(attrEnd)) depth--;
- }
- nodes.push(node);
- node = node.nextSibling;
- } while (depth > 0);
- } else {
- nodes.push(node);
- }
-
- return jqLite(nodes);
- }
-
- /**
- * Wrapper for linking function which converts normal linking function into a grouped
- * linking function.
- * @param linkFn
- * @param attrStart
- * @param attrEnd
- * @returns {Function}
- */
- function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
- return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) {
- element = groupScan(element[0], attrStart, attrEnd);
- return linkFn(scope, element, attrs, controllers, transcludeFn);
- };
- }
-
- /**
- * A function generator that is used to support both eager and lazy compilation
- * linking function.
- * @param eager
- * @param $compileNodes
- * @param transcludeFn
- * @param maxPriority
- * @param ignoreDirective
- * @param previousCompileContext
- * @returns {Function}
- */
- function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
- var compiled;
-
- if (eager) {
- return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
- }
- return function lazyCompilation() {
- if (!compiled) {
- compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
-
- // Null out all of these references in order to make them eligible for garbage collection
- // since this is a potentially long lived closure
- $compileNodes = transcludeFn = previousCompileContext = null;
- }
- return compiled.apply(this, arguments);
- };
- }
-
- /**
- * Once the directives have been collected, their compile functions are executed. This method
- * is responsible for inlining directive templates as well as terminating the application
- * of the directives if the terminal directive has been reached.
- *
- * @param {Array} directives Array of collected directives to execute their compile function.
- * this needs to be pre-sorted by priority order.
- * @param {Node} compileNode The raw DOM node to apply the compile functions to
- * @param {Object} templateAttrs The shared attribute function
- * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the
- * scope argument is auto-generated to the new
- * child of the transcluded parent scope.
- * @param {JQLite} jqCollection If we are working on the root of the compile tree then this
- * argument has the root jqLite array so that we can replace nodes
- * on it.
- * @param {Object=} originalReplaceDirective An optional directive that will be ignored when
- * compiling the transclusion.
- * @param {Array.<Function>} preLinkFns
- * @param {Array.<Function>} postLinkFns
- * @param {Object} previousCompileContext Context used for previous compilation of the current
- * node
- * @returns {Function} linkFn
- */
- function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn,
- jqCollection, originalReplaceDirective, preLinkFns, postLinkFns,
- previousCompileContext) {
- previousCompileContext = previousCompileContext || {};
-
- var terminalPriority = -Number.MAX_VALUE,
- newScopeDirective = previousCompileContext.newScopeDirective,
- controllerDirectives = previousCompileContext.controllerDirectives,
- newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
- templateDirective = previousCompileContext.templateDirective,
- nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective,
- hasTranscludeDirective = false,
- hasTemplate = false,
- hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective,
- $compileNode = templateAttrs.$$element = jqLite(compileNode),
- directive,
- directiveName,
- $template,
- replaceDirective = originalReplaceDirective,
- childTranscludeFn = transcludeFn,
- linkFn,
- didScanForMultipleTransclusion = false,
- mightHaveMultipleTransclusionError = false,
- directiveValue;
-
- // executes all directives on the current element
- for (var i = 0, ii = directives.length; i < ii; i++) {
- directive = directives[i];
- var attrStart = directive.$$start;
- var attrEnd = directive.$$end;
-
- // collect multiblock sections
- if (attrStart) {
- $compileNode = groupScan(compileNode, attrStart, attrEnd);
- }
- $template = undefined;
-
- if (terminalPriority > directive.priority) {
- break; // prevent further processing of directives
- }
-
- if (directiveValue = directive.scope) {
-
- // skip the check for directives with async templates, we'll check the derived sync
- // directive when the template arrives
- if (!directive.templateUrl) {
- if (isObject(directiveValue)) {
- // This directive is trying to add an isolated scope.
- // Check that there is no scope of any kind already
- assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective,
- directive, $compileNode);
- newIsolateScopeDirective = directive;
- } else {
- // This directive is trying to add a child scope.
- // Check that there is no isolated scope already
- assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive,
- $compileNode);
- }
- }
-
- newScopeDirective = newScopeDirective || directive;
- }
-
- directiveName = directive.name;
-
- // If we encounter a condition that can result in transclusion on the directive,
- // then scan ahead in the remaining directives for others that may cause a multiple
- // transclusion error to be thrown during the compilation process. If a matching directive
- // is found, then we know that when we encounter a transcluded directive, we need to eagerly
- // compile the `transclude` function rather than doing it lazily in order to throw
- // exceptions at the correct time
- if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template))
- || (directive.transclude && !directive.$$tlb))) {
- var candidateDirective;
-
- for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) {
- if ((candidateDirective.transclude && !candidateDirective.$$tlb)
- || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) {
- mightHaveMultipleTransclusionError = true;
- break;
- }
- }
-
- didScanForMultipleTransclusion = true;
- }
-
- if (!directive.templateUrl && directive.controller) {
- directiveValue = directive.controller;
- controllerDirectives = controllerDirectives || createMap();
- assertNoDuplicate("'" + directiveName + "' controller",
- controllerDirectives[directiveName], directive, $compileNode);
- controllerDirectives[directiveName] = directive;
- }
-
- if (directiveValue = directive.transclude) {
- hasTranscludeDirective = true;
-
- // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion.
- // This option should only be used by directives that know how to safely handle element transclusion,
- // where the transcluded nodes are added or replaced after linking.
- if (!directive.$$tlb) {
- assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode);
- nonTlbTranscludeDirective = directive;
- }
-
- if (directiveValue == 'element') {
- hasElementTranscludeDirective = true;
- terminalPriority = directive.priority;
- $template = $compileNode;
- $compileNode = templateAttrs.$$element =
- jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName]));
- compileNode = $compileNode[0];
- replaceWith(jqCollection, sliceArgs($template), compileNode);
-
- // Support: Chrome < 50
- // https://github.com/angular/angular.js/issues/14041
-
- // In the versions of V8 prior to Chrome 50, the document fragment that is created
- // in the `replaceWith` function is improperly garbage collected despite still
- // being referenced by the `parentNode` property of all of the child nodes. By adding
- // a reference to the fragment via a different property, we can avoid that incorrect
- // behavior.
- // TODO: remove this line after Chrome 50 has been released
- $template[0].$$parentNode = $template[0].parentNode;
-
- childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
- replaceDirective && replaceDirective.name, {
- // Don't pass in:
- // - controllerDirectives - otherwise we'll create duplicates controllers
- // - newIsolateScopeDirective or templateDirective - combining templates with
- // element transclusion doesn't make sense.
- //
- // We need only nonTlbTranscludeDirective so that we prevent putting transclusion
- // on the same element more than once.
- nonTlbTranscludeDirective: nonTlbTranscludeDirective
- });
- } else {
-
- var slots = createMap();
-
- $template = jqLite(jqLiteClone(compileNode)).contents();
-
- if (isObject(directiveValue)) {
-
- // We have transclusion slots,
- // collect them up, compile them and store their transclusion functions
- $template = [];
-
- var slotMap = createMap();
- var filledSlots = createMap();
-
- // Parse the element selectors
- forEach(directiveValue, function(elementSelector, slotName) {
- // If an element selector starts with a ? then it is optional
- var optional = (elementSelector.charAt(0) === '?');
- elementSelector = optional ? elementSelector.substring(1) : elementSelector;
-
- slotMap[elementSelector] = slotName;
-
- // We explicitly assign `null` since this implies that a slot was defined but not filled.
- // Later when calling boundTransclusion functions with a slot name we only error if the
- // slot is `undefined`
- slots[slotName] = null;
-
- // filledSlots contains `true` for all slots that are either optional or have been
- // filled. This is used to check that we have not missed any required slots
- filledSlots[slotName] = optional;
- });
-
- // Add the matching elements into their slot
- forEach($compileNode.contents(), function(node) {
- var slotName = slotMap[directiveNormalize(nodeName_(node))];
- if (slotName) {
- filledSlots[slotName] = true;
- slots[slotName] = slots[slotName] || [];
- slots[slotName].push(node);
- } else {
- $template.push(node);
- }
- });
-
- // Check for required slots that were not filled
- forEach(filledSlots, function(filled, slotName) {
- if (!filled) {
- throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName);
- }
- });
-
- for (var slotName in slots) {
- if (slots[slotName]) {
- // Only define a transclusion function if the slot was filled
- slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
- }
- }
- }
-
- $compileNode.empty(); // clear contents
- childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined,
- undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
- childTranscludeFn.$$slots = slots;
- }
- }
-
- if (directive.template) {
- hasTemplate = true;
- assertNoDuplicate('template', templateDirective, directive, $compileNode);
- templateDirective = directive;
-
- directiveValue = (isFunction(directive.template))
- ? directive.template($compileNode, templateAttrs)
- : directive.template;
-
- directiveValue = denormalizeTemplate(directiveValue);
-
- if (directive.replace) {
- replaceDirective = directive;
- if (jqLiteIsTextNode(directiveValue)) {
- $template = [];
- } else {
- $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue)));
- }
- compileNode = $template[0];
-
- if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
- throw $compileMinErr('tplrt',
- "Template for directive '{0}' must have exactly one root element. {1}",
- directiveName, '');
- }
-
- replaceWith(jqCollection, $compileNode, compileNode);
-
- var newTemplateAttrs = {$attr: {}};
-
- // combine directives from the original node and from the template:
- // - take the array of directives for this element
- // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed)
- // - collect directives from the template and sort them by priority
- // - combine directives as: processed + template + unprocessed
- var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
- var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));
-
- if (newIsolateScopeDirective || newScopeDirective) {
- // The original directive caused the current element to be replaced but this element
- // also needs to have a new scope, so we need to tell the template directives
- // that they would need to get their scope from further up, if they require transclusion
- markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective);
- }
- directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
- mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
-
- ii = directives.length;
- } else {
- $compileNode.html(directiveValue);
- }
- }
-
- if (directive.templateUrl) {
- hasTemplate = true;
- assertNoDuplicate('template', templateDirective, directive, $compileNode);
- templateDirective = directive;
-
- if (directive.replace) {
- replaceDirective = directive;
- }
-
- /* jshint -W021 */
- nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
- /* jshint +W021 */
- templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
- controllerDirectives: controllerDirectives,
- newScopeDirective: (newScopeDirective !== directive) && newScopeDirective,
- newIsolateScopeDirective: newIsolateScopeDirective,
- templateDirective: templateDirective,
- nonTlbTranscludeDirective: nonTlbTranscludeDirective
- });
- ii = directives.length;
- } else if (directive.compile) {
- try {
- linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
- var context = directive.$$originalDirective || directive;
- if (isFunction(linkFn)) {
- addLinkFns(null, bind(context, linkFn), attrStart, attrEnd);
- } else if (linkFn) {
- addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd);
- }
- } catch (e) {
- $exceptionHandler(e, startingTag($compileNode));
- }
- }
-
- if (directive.terminal) {
- nodeLinkFn.terminal = true;
- terminalPriority = Math.max(terminalPriority, directive.priority);
- }
-
- }
-
- nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true;
- nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective;
- nodeLinkFn.templateOnThisElement = hasTemplate;
- nodeLinkFn.transclude = childTranscludeFn;
-
- previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective;
-
- // might be normal or delayed nodeLinkFn depending on if templateUrl is present
- return nodeLinkFn;
-
- ////////////////////
-
- function addLinkFns(pre, post, attrStart, attrEnd) {
- if (pre) {
- if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);
- pre.require = directive.require;
- pre.directiveName = directiveName;
- if (newIsolateScopeDirective === directive || directive.$$isolateScope) {
- pre = cloneAndAnnotateFn(pre, {isolateScope: true});
- }
- preLinkFns.push(pre);
- }
- if (post) {
- if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);
- post.require = directive.require;
- post.directiveName = directiveName;
- if (newIsolateScopeDirective === directive || directive.$$isolateScope) {
- post = cloneAndAnnotateFn(post, {isolateScope: true});
- }
- postLinkFns.push(post);
- }
- }
-
- function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
- var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
- attrs, scopeBindingInfo;
-
- if (compileNode === linkNode) {
- attrs = templateAttrs;
- $element = templateAttrs.$$element;
- } else {
- $element = jqLite(linkNode);
- attrs = new Attributes($element, templateAttrs);
- }
-
- controllerScope = scope;
- if (newIsolateScopeDirective) {
- isolateScope = scope.$new(true);
- } else if (newScopeDirective) {
- controllerScope = scope.$parent;
- }
-
- if (boundTranscludeFn) {
- // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn`
- // is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
- transcludeFn = controllersBoundTransclude;
- transcludeFn.$$boundTransclude = boundTranscludeFn;
- // expose the slots on the `$transclude` function
- transcludeFn.isSlotFilled = function(slotName) {
- return !!boundTranscludeFn.$$slots[slotName];
- };
- }
-
- if (controllerDirectives) {
- elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective);
- }
-
- if (newIsolateScopeDirective) {
- // Initialize isolate scope bindings for new isolate scope directive.
- compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective ||
- templateDirective === newIsolateScopeDirective.$$originalDirective)));
- compile.$$addScopeClass($element, true);
- isolateScope.$$isolateBindings =
- newIsolateScopeDirective.$$isolateBindings;
- scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope,
- isolateScope.$$isolateBindings,
- newIsolateScopeDirective);
- if (scopeBindingInfo.removeWatches) {
- isolateScope.$on('$destroy', scopeBindingInfo.removeWatches);
- }
- }
-
- // Initialize bindToController bindings
- for (var name in elementControllers) {
- var controllerDirective = controllerDirectives[name];
- var controller = elementControllers[name];
- var bindings = controllerDirective.$$bindings.bindToController;
-
- if (controller.identifier && bindings) {
- controller.bindingInfo =
- initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
- } else {
- controller.bindingInfo = {};
- }
-
- var controllerResult = controller();
- if (controllerResult !== controller.instance) {
- // If the controller constructor has a return value, overwrite the instance
- // from setupControllers
- controller.instance = controllerResult;
- $element.data('$' + controllerDirective.name + 'Controller', controllerResult);
- controller.bindingInfo.removeWatches && controller.bindingInfo.removeWatches();
- controller.bindingInfo =
- initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
- }
- }
-
- // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy
- forEach(controllerDirectives, function(controllerDirective, name) {
- var require = controllerDirective.require;
- if (controllerDirective.bindToController && !isArray(require) && isObject(require)) {
- extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers));
- }
- });
-
- // Handle the init and destroy lifecycle hooks on all controllers that have them
- forEach(elementControllers, function(controller) {
- var controllerInstance = controller.instance;
- if (isFunction(controllerInstance.$onChanges)) {
- try {
- controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
- } catch (e) {
- $exceptionHandler(e);
- }
- }
- if (isFunction(controllerInstance.$onInit)) {
- try {
- controllerInstance.$onInit();
- } catch (e) {
- $exceptionHandler(e);
- }
- }
- if (isFunction(controllerInstance.$doCheck)) {
- controllerScope.$watch(function() { controllerInstance.$doCheck(); });
- controllerInstance.$doCheck();
- }
- if (isFunction(controllerInstance.$onDestroy)) {
- controllerScope.$on('$destroy', function callOnDestroyHook() {
- controllerInstance.$onDestroy();
- });
- }
- });
-
- // PRELINKING
- for (i = 0, ii = preLinkFns.length; i < ii; i++) {
- linkFn = preLinkFns[i];
- invokeLinkFn(linkFn,
- linkFn.isolateScope ? isolateScope : scope,
- $element,
- attrs,
- linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
- transcludeFn
- );
- }
-
- // RECURSION
- // We only pass the isolate scope, if the isolate directive has a template,
- // otherwise the child elements do not belong to the isolate directive.
- var scopeToChild = scope;
- if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {
- scopeToChild = isolateScope;
- }
- childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
-
- // POSTLINKING
- for (i = postLinkFns.length - 1; i >= 0; i--) {
- linkFn = postLinkFns[i];
- invokeLinkFn(linkFn,
- linkFn.isolateScope ? isolateScope : scope,
- $element,
- attrs,
- linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
- transcludeFn
- );
- }
-
- // Trigger $postLink lifecycle hooks
- forEach(elementControllers, function(controller) {
- var controllerInstance = controller.instance;
- if (isFunction(controllerInstance.$postLink)) {
- controllerInstance.$postLink();
- }
- });
-
- // This is the function that is injected as `$transclude`.
- // Note: all arguments are optional!
- function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
- var transcludeControllers;
- // No scope passed in:
- if (!isScope(scope)) {
- slotName = futureParentElement;
- futureParentElement = cloneAttachFn;
- cloneAttachFn = scope;
- scope = undefined;
- }
-
- if (hasElementTranscludeDirective) {
- transcludeControllers = elementControllers;
- }
- if (!futureParentElement) {
- futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
- }
- if (slotName) {
- // slotTranscludeFn can be one of three things:
- // * a transclude function - a filled slot
- // * `null` - an optional slot that was not filled
- // * `undefined` - a slot that was not declared (i.e. invalid)
- var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
- if (slotTranscludeFn) {
- return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
- } else if (isUndefined(slotTranscludeFn)) {
- throw $compileMinErr('noslot',
- 'No parent directive that requires a transclusion with slot name "{0}". ' +
- 'Element: {1}',
- slotName, startingTag($element));
- }
- } else {
- return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
- }
- }
- }
- }
-
- function getControllers(directiveName, require, $element, elementControllers) {
- var value;
-
- if (isString(require)) {
- var match = require.match(REQUIRE_PREFIX_REGEXP);
- var name = require.substring(match[0].length);
- var inheritType = match[1] || match[3];
- var optional = match[2] === '?';
-
- //If only parents then start at the parent element
- if (inheritType === '^^') {
- $element = $element.parent();
- //Otherwise attempt getting the controller from elementControllers in case
- //the element is transcluded (and has no data) and to avoid .data if possible
- } else {
- value = elementControllers && elementControllers[name];
- value = value && value.instance;
- }
-
- if (!value) {
- var dataName = '$' + name + 'Controller';
- value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
- }
-
- if (!value && !optional) {
- throw $compileMinErr('ctreq',
- "Controller '{0}', required by directive '{1}', can't be found!",
- name, directiveName);
- }
- } else if (isArray(require)) {
- value = [];
- for (var i = 0, ii = require.length; i < ii; i++) {
- value[i] = getControllers(directiveName, require[i], $element, elementControllers);
- }
- } else if (isObject(require)) {
- value = {};
- forEach(require, function(controller, property) {
- value[property] = getControllers(directiveName, controller, $element, elementControllers);
- });
- }
-
- return value || null;
- }
-
- function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) {
- var elementControllers = createMap();
- for (var controllerKey in controllerDirectives) {
- var directive = controllerDirectives[controllerKey];
- var locals = {
- $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
- $element: $element,
- $attrs: attrs,
- $transclude: transcludeFn
- };
-
- var controller = directive.controller;
- if (controller == '@') {
- controller = attrs[directive.name];
- }
-
- var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
-
- // For directives with element transclusion the element is a comment.
- // In this case .data will not attach any data.
- // Instead, we save the controllers for the element in a local hash and attach to .data
- // later, once we have the actual element.
- elementControllers[directive.name] = controllerInstance;
- $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
- }
- return elementControllers;
- }
-
- // Depending upon the context in which a directive finds itself it might need to have a new isolated
- // or child scope created. For instance:
- // * if the directive has been pulled into a template because another directive with a higher priority
- // asked for element transclusion
- // * if the directive itself asks for transclusion but it is at the root of a template and the original
- // element was replaced. See https://github.com/angular/angular.js/issues/12936
- function markDirectiveScope(directives, isolateScope, newScope) {
- for (var j = 0, jj = directives.length; j < jj; j++) {
- directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope});
- }
- }
-
- /**
- * looks up the directive and decorates it with exception handling and proper parameters. We
- * call this the boundDirective.
- *
- * @param {string} name name of the directive to look up.
- * @param {string} location The directive must be found in specific format.
- * String containing any of theses characters:
- *
- * * `E`: element name
- * * `A': attribute
- * * `C`: class
- * * `M`: comment
- * @returns {boolean} true if directive was added.
- */
- function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName,
- endAttrName) {
- if (name === ignoreDirective) return null;
- var match = null;
- if (hasDirectives.hasOwnProperty(name)) {
- for (var directive, directives = $injector.get(name + Suffix),
- i = 0, ii = directives.length; i < ii; i++) {
- try {
- directive = directives[i];
- if ((isUndefined(maxPriority) || maxPriority > directive.priority) &&
- directive.restrict.indexOf(location) != -1) {
- if (startAttrName) {
- directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
- }
- if (!directive.$$bindings) {
- var bindings = directive.$$bindings =
- parseDirectiveBindings(directive, directive.name);
- if (isObject(bindings.isolateScope)) {
- directive.$$isolateBindings = bindings.isolateScope;
- }
- }
- tDirectives.push(directive);
- match = directive;
- }
- } catch (e) { $exceptionHandler(e); }
- }
- }
- return match;
- }
-
-
- /**
- * looks up the directive and returns true if it is a multi-element directive,
- * and therefore requires DOM nodes between -start and -end markers to be grouped
- * together.
- *
- * @param {string} name name of the directive to look up.
- * @returns true if directive was registered as multi-element.
- */
- function directiveIsMultiElement(name) {
- if (hasDirectives.hasOwnProperty(name)) {
- for (var directive, directives = $injector.get(name + Suffix),
- i = 0, ii = directives.length; i < ii; i++) {
- directive = directives[i];
- if (directive.multiElement) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * When the element is replaced with HTML template then the new attributes
- * on the template need to be merged with the existing attributes in the DOM.
- * The desired effect is to have both of the attributes present.
- *
- * @param {object} dst destination attributes (original DOM)
- * @param {object} src source attributes (from the directive template)
- */
- function mergeTemplateAttributes(dst, src) {
- var srcAttr = src.$attr,
- dstAttr = dst.$attr,
- $element = dst.$$element;
-
- // reapply the old attributes to the new element
- forEach(dst, function(value, key) {
- if (key.charAt(0) != '$') {
- if (src[key] && src[key] !== value) {
- value += (key === 'style' ? ';' : ' ') + src[key];
- }
- dst.$set(key, value, true, srcAttr[key]);
- }
- });
-
- // copy the new attributes on the old attrs object
- forEach(src, function(value, key) {
- // Check if we already set this attribute in the loop above.
- // `dst` will never contain hasOwnProperty as DOM parser won't let it.
- // You will get an "InvalidCharacterError: DOM Exception 5" error if you
- // have an attribute like "has-own-property" or "data-has-own-property", etc.
- if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') {
- dst[key] = value;
-
- if (key !== 'class' && key !== 'style') {
- dstAttr[key] = srcAttr[key];
- }
- }
- });
- }
-
-
- function compileTemplateUrl(directives, $compileNode, tAttrs,
- $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) {
- var linkQueue = [],
- afterTemplateNodeLinkFn,
- afterTemplateChildLinkFn,
- beforeTemplateCompileNode = $compileNode[0],
- origAsyncDirective = directives.shift(),
- derivedSyncDirective = inherit(origAsyncDirective, {
- templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective
- }),
- templateUrl = (isFunction(origAsyncDirective.templateUrl))
- ? origAsyncDirective.templateUrl($compileNode, tAttrs)
- : origAsyncDirective.templateUrl,
- templateNamespace = origAsyncDirective.templateNamespace;
-
- $compileNode.empty();
-
- $templateRequest(templateUrl)
- .then(function(content) {
- var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn;
-
- content = denormalizeTemplate(content);
-
- if (origAsyncDirective.replace) {
- if (jqLiteIsTextNode(content)) {
- $template = [];
- } else {
- $template = removeComments(wrapTemplate(templateNamespace, trim(content)));
- }
- compileNode = $template[0];
-
- 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);
- }
-
- tempTemplateAttrs = {$attr: {}};
- replaceWith($rootElement, $compileNode, compileNode);
- var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs);
-
- if (isObject(origAsyncDirective.scope)) {
- // the original directive that caused the template to be loaded async required
- // an isolate scope
- markDirectiveScope(templateDirectives, true);
- }
- directives = templateDirectives.concat(directives);
- mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
- } else {
- compileNode = beforeTemplateCompileNode;
- $compileNode.html(content);
- }
-
- directives.unshift(derivedSyncDirective);
-
- afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs,
- childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns,
- previousCompileContext);
- forEach($rootElement, function(node, i) {
- if (node == compileNode) {
- $rootElement[i] = $compileNode[0];
- }
- });
- afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
-
- while (linkQueue.length) {
- var scope = linkQueue.shift(),
- 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)) {
- // it was cloned therefore we have to clone as well.
- linkNode = jqLiteClone(compileNode);
- }
- replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
-
- // Copy in CSS classes from original node
- safeAddClass(jqLite(linkNode), oldClasses);
- }
- if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
- childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
- } else {
- childBoundTranscludeFn = boundTranscludeFn;
- }
- afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement,
- childBoundTranscludeFn);
- }
- linkQueue = null;
- });
-
- return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
- var childBoundTranscludeFn = boundTranscludeFn;
- if (scope.$$destroyed) return;
- if (linkQueue) {
- linkQueue.push(scope,
- node,
- rootElement,
- childBoundTranscludeFn);
- } else {
- if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
- childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
- }
- afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
- }
- };
- }
-
-
- /**
- * Sorting function for bound directives.
- */
- function byPriority(a, b) {
- var diff = b.priority - a.priority;
- if (diff !== 0) return diff;
- if (a.name !== b.name) return (a.name < b.name) ? -1 : 1;
- return a.index - b.index;
- }
-
- function assertNoDuplicate(what, previousDirective, directive, element) {
-
- function wrapModuleNameIfDefined(moduleName) {
- return moduleName ?
- (' (module: ' + moduleName + ')') :
- '';
- }
-
- if (previousDirective) {
- throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}',
- previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName),
- directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element));
- }
- }
-
-
- function addTextInterpolateDirective(directives, text) {
- var interpolateFn = $interpolate(text, true);
- if (interpolateFn) {
- directives.push({
- priority: 0,
- compile: function textInterpolateCompileFn(templateNode) {
- var templateNodeParent = templateNode.parent(),
- hasCompileParent = !!templateNodeParent.length;
-
- // When transcluding a template that has bindings in the root
- // we don't have a parent and thus need to add the class during linking fn.
- if (hasCompileParent) compile.$$addBindingClass(templateNodeParent);
-
- return function textInterpolateLinkFn(scope, node) {
- var parent = node.parent();
- if (!hasCompileParent) compile.$$addBindingClass(parent);
- compile.$$addBindingInfo(parent, interpolateFn.expressions);
- scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
- node[0].nodeValue = value;
- });
- };
- }
- });
- }
- }
-
-
- function wrapTemplate(type, template) {
- type = lowercase(type || 'html');
- switch (type) {
- case 'svg':
- case 'math':
- var wrapper = window.document.createElement('div');
- wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>';
- return wrapper.childNodes[0].childNodes;
- default:
- return template;
- }
- }
-
-
- function getTrustedContext(node, attrNormalizedName) {
- if (attrNormalizedName == "srcdoc") {
- return $sce.HTML;
- }
- var tag = nodeName_(node);
- // maction[xlink:href] can source SVG. It's not limited to <maction>.
- if (attrNormalizedName == "xlinkHref" ||
- (tag == "form" && attrNormalizedName == "action") ||
- (tag != "img" && (attrNormalizedName == "src" ||
- attrNormalizedName == "ngSrc"))) {
- return $sce.RESOURCE_URL;
- }
- }
-
-
- function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) {
- var trustedContext = getTrustedContext(node, name);
- allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing;
-
- var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing);
-
- // no interpolation found -> ignore
- if (!interpolateFn) return;
-
-
- if (name === "multiple" && nodeName_(node) === "select") {
- throw $compileMinErr("selmulti",
- "Binding to the 'multiple' attribute is not supported. Element: {0}",
- startingTag(node));
- }
-
- directives.push({
- priority: 100,
- compile: function() {
- return {
- pre: function attrInterpolatePreLinkFn(scope, element, attr) {
- var $$observers = (attr.$$observers || (attr.$$observers = createMap()));
-
- if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
- throw $compileMinErr('nodomevents',
- "Interpolations for HTML DOM event attributes are disallowed. Please use the " +
- "ng- versions (such as ng-click instead of onclick) instead.");
- }
-
- // If the attribute has changed since last $interpolate()ed
- var newValue = attr[name];
- if (newValue !== value) {
- // we need to interpolate again since the attribute value has been updated
- // (e.g. by another directive's compile function)
- // ensure unset/empty values make interpolateFn falsy
- interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing);
- value = newValue;
- }
-
- // if attribute was updated so that there is no interpolation going on we don't want to
- // register any observers
- if (!interpolateFn) return;
-
- // initialize attr object so that it's ready in case we need the value for isolate
- // scope initialization, otherwise the value would not be available from isolate
- // directive's linking fn during linking phase
- attr[name] = interpolateFn(scope);
-
- ($$observers[name] || ($$observers[name] = [])).$$inter = true;
- (attr.$$observers && attr.$$observers[name].$$scope || scope).
- $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
- //special case for class attribute addition + removal
- //so that class changes can tap into the animation
- //hooks provided by the $animate service. Be sure to
- //skip animations when the first digest occurs (when
- //both the new and the old values are the same) since
- //the CSS classes are the non-interpolated values
- if (name === 'class' && newValue != oldValue) {
- attr.$updateClass(newValue, oldValue);
- } else {
- attr.$set(name, newValue);
- }
- });
- }
- };
- }
- });
- }
-
-
- /**
- * This is a special jqLite.replaceWith, which can replace items which
- * have no parents, provided that the containing jqLite collection is provided.
- *
- * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
- * in the root of the tree.
- * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep
- * the shell, but replace its DOM node reference.
- * @param {Node} newNode The new DOM node.
- */
- function replaceWith($rootElement, elementsToRemove, newNode) {
- var firstElementToRemove = elementsToRemove[0],
- removeCount = elementsToRemove.length,
- parent = firstElementToRemove.parentNode,
- i, ii;
-
- if ($rootElement) {
- for (i = 0, ii = $rootElement.length; i < ii; i++) {
- if ($rootElement[i] == firstElementToRemove) {
- $rootElement[i++] = newNode;
- for (var j = i, j2 = j + removeCount - 1,
- jj = $rootElement.length;
- j < jj; j++, j2++) {
- if (j2 < jj) {
- $rootElement[j] = $rootElement[j2];
- } else {
- delete $rootElement[j];
- }
- }
- $rootElement.length -= removeCount - 1;
-
- // If the replaced element is also the jQuery .context then replace it
- // .context is a deprecated jQuery api, so we should set it only when jQuery set it
- // http://api.jquery.com/context/
- if ($rootElement.context === firstElementToRemove) {
- $rootElement.context = newNode;
- }
- break;
- }
- }
- }
-
- if (parent) {
- parent.replaceChild(newNode, firstElementToRemove);
- }
-
- // Append all the `elementsToRemove` to a fragment. This will...
- // - remove them from the DOM
- // - allow them to still be traversed with .nextSibling
- // - allow a single fragment.qSA to fetch all elements being removed
- var fragment = window.document.createDocumentFragment();
- for (i = 0; i < removeCount; i++) {
- fragment.appendChild(elementsToRemove[i]);
- }
-
- if (jqLite.hasData(firstElementToRemove)) {
- // Copy over user data (that includes Angular's $scope etc.). Don't copy private
- // data here because there's no public interface in jQuery to do that and copying over
- // event listeners (which is the main use of private data) wouldn't work anyway.
- jqLite.data(newNode, jqLite.data(firstElementToRemove));
-
- // Remove $destroy event listeners from `firstElementToRemove`
- jqLite(firstElementToRemove).off('$destroy');
- }
-
- // Cleanup any data/listeners on the elements and children.
- // This includes invoking the $destroy event on any elements with listeners.
- jqLite.cleanData(fragment.querySelectorAll('*'));
-
- // Update the jqLite collection to only contain the `newNode`
- for (i = 1; i < removeCount; i++) {
- delete elementsToRemove[i];
- }
- elementsToRemove[0] = newNode;
- elementsToRemove.length = 1;
- }
-
-
- function cloneAndAnnotateFn(fn, annotation) {
- return extend(function() { return fn.apply(null, arguments); }, fn, annotation);
- }
-
-
- function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) {
- try {
- linkFn(scope, $element, attrs, controllers, transcludeFn);
- } catch (e) {
- $exceptionHandler(e, startingTag($element));
- }
- }
-
-
- // Set up $watches for isolate scope and controller bindings. This process
- // only occurs for isolate scopes and new scopes with controllerAs.
- function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
- var removeWatchCollection = [];
- var initialChanges = {};
- var changes;
- forEach(bindings, function initializeBinding(definition, scopeName) {
- var attrName = definition.attrName,
- optional = definition.optional,
- mode = definition.mode, // @, =, <, or &
- lastValue,
- parentGet, parentSet, compare, removeWatch;
-
- switch (mode) {
-
- case '@':
- if (!optional && !hasOwnProperty.call(attrs, attrName)) {
- destination[scopeName] = attrs[attrName] = void 0;
- }
- attrs.$observe(attrName, function(value) {
- if (isString(value) || isBoolean(value)) {
- var oldValue = destination[scopeName];
- recordChanges(scopeName, value, oldValue);
- destination[scopeName] = value;
- }
- });
- attrs.$$observers[attrName].$$scope = scope;
- lastValue = attrs[attrName];
- if (isString(lastValue)) {
- // If the attribute has been provided then we trigger an interpolation to ensure
- // the value is there for use in the link fn
- destination[scopeName] = $interpolate(lastValue)(scope);
- } else if (isBoolean(lastValue)) {
- // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted
- // the value to boolean rather than a string, so we special case this situation
- destination[scopeName] = lastValue;
- }
- initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
- break;
-
- case '=':
- if (!hasOwnProperty.call(attrs, attrName)) {
- if (optional) break;
- attrs[attrName] = void 0;
- }
- if (optional && !attrs[attrName]) break;
-
- parentGet = $parse(attrs[attrName]);
- if (parentGet.literal) {
- compare = equals;
- } else {
- compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); };
- }
- parentSet = parentGet.assign || function() {
- // reset the change, or we will throw this exception on every $digest
- lastValue = destination[scopeName] = parentGet(scope);
- throw $compileMinErr('nonassign',
- "Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!",
- attrs[attrName], attrName, directive.name);
- };
- lastValue = destination[scopeName] = parentGet(scope);
- var parentValueWatch = function parentValueWatch(parentValue) {
- if (!compare(parentValue, destination[scopeName])) {
- // we are out of sync and need to copy
- if (!compare(parentValue, lastValue)) {
- // parent changed and it has precedence
- destination[scopeName] = parentValue;
- } else {
- // if the parent can be assigned then do so
- parentSet(scope, parentValue = destination[scopeName]);
- }
- }
- return lastValue = parentValue;
- };
- parentValueWatch.$stateful = true;
- if (definition.collection) {
- removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
- } else {
- removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
- }
- removeWatchCollection.push(removeWatch);
- break;
-
- case '<':
- if (!hasOwnProperty.call(attrs, attrName)) {
- if (optional) break;
- attrs[attrName] = void 0;
- }
- if (optional && !attrs[attrName]) break;
-
- parentGet = $parse(attrs[attrName]);
-
- var initialValue = destination[scopeName] = parentGet(scope);
- initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
-
- removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
- if (oldValue === newValue) {
- if (oldValue === initialValue) return;
- oldValue = initialValue;
- }
- recordChanges(scopeName, newValue, oldValue);
- destination[scopeName] = newValue;
- }, parentGet.literal);
-
- removeWatchCollection.push(removeWatch);
- break;
-
- case '&':
- // Don't assign Object.prototype method to scope
- parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
-
- // Don't assign noop to destination if expression is not valid
- if (parentGet === noop && optional) break;
-
- destination[scopeName] = function(locals) {
- return parentGet(scope, locals);
- };
- break;
- }
- });
-
- function recordChanges(key, currentValue, previousValue) {
- if (isFunction(destination.$onChanges) && currentValue !== previousValue) {
- // If we have not already scheduled the top level onChangesQueue handler then do so now
- if (!onChangesQueue) {
- scope.$$postDigest(flushOnChangesQueue);
- onChangesQueue = [];
- }
- // If we have not already queued a trigger of onChanges for this controller then do so now
- if (!changes) {
- changes = {};
- onChangesQueue.push(triggerOnChangesHook);
- }
- // If the has been a change on this property already then we need to reuse the previous value
- if (changes[key]) {
- previousValue = changes[key].previousValue;
- }
- // Store this change
- changes[key] = new SimpleChange(previousValue, currentValue);
- }
- }
-
- function triggerOnChangesHook() {
- destination.$onChanges(changes);
- // Now clear the changes so that we schedule onChanges when more changes arrive
- changes = undefined;
- }
-
- return {
- initialChanges: initialChanges,
- removeWatches: removeWatchCollection.length && function removeWatches() {
- for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
- removeWatchCollection[i]();
- }
- }
- };
- }
- }];
-}
-
-function SimpleChange(previous, current) {
- this.previousValue = previous;
- this.currentValue = current;
-}
-SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
-
-
-var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
-/**
- * Converts all accepted directives format into proper directive name.
- * @param name Name to normalize
- */
-function directiveNormalize(name) {
- return camelCase(name.replace(PREFIX_REGEXP, ''));
-}
-
-/**
- * @ngdoc type
- * @name $compile.directive.Attributes
- *
- * @description
- * A shared object between directive compile / linking functions which contains normalized DOM
- * element attributes. The values reflect current binding state `{{ }}`. The normalization is
- * needed since all of these are treated as equivalent in Angular:
- *
- * ```
- * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
- * ```
- */
-
-/**
- * @ngdoc property
- * @name $compile.directive.Attributes#$attr
- *
- * @description
- * A map of DOM element attribute names to the normalized name. This is
- * needed to do reverse lookup from normalized name back to actual name.
- */
-
-
-/**
- * @ngdoc method
- * @name $compile.directive.Attributes#$set
- * @kind function
- *
- * @description
- * Set DOM element attribute value.
- *
- *
- * @param {string} name Normalized element attribute name of the property to modify. The name is
- * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
- * property to the original name.
- * @param {string} value Value to set the attribute to. The value can be an interpolated string.
- */
-
-
-
-/**
- * Closure compiler type information
- */
-
-function nodesetLinkingFn(
- /* angular.Scope */ scope,
- /* NodeList */ nodeList,
- /* Element */ rootElement,
- /* function(Function) */ boundTranscludeFn
-) {}
-
-function directiveLinkingFn(
- /* nodesetLinkingFn */ nodesetLinkingFn,
- /* angular.Scope */ scope,
- /* Node */ node,
- /* Element */ rootElement,
- /* function(Function) */ boundTranscludeFn
-) {}
-
-function tokenDifference(str1, str2) {
- var values = '',
- tokens1 = str1.split(/\s+/),
- tokens2 = str2.split(/\s+/);
-
- outer:
- for (var i = 0; i < tokens1.length; i++) {
- var token = tokens1[i];
- for (var j = 0; j < tokens2.length; j++) {
- if (token == tokens2[j]) continue outer;
- }
- 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;
-}
-
-var $controllerMinErr = minErr('$controller');
-
-
-var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/;
-function identifierForController(controller, ident) {
- if (ident && isString(ident)) return ident;
- if (isString(controller)) {
- var match = CNTRL_REG.exec(controller);
- if (match) return match[3];
- }
-}
-
-
-/**
- * @ngdoc provider
- * @name $controllerProvider
- * @description
- * The {@link ng.$controller $controller service} is used by Angular to create new
- * controllers.
- *
- * This provider allows controller registration via the
- * {@link ng.$controllerProvider#register register} method.
- */
-function $ControllerProvider() {
- var controllers = {},
- globals = false;
-
- /**
- * @ngdoc method
- * @name $controllerProvider#has
- * @param {string} name Controller name to check.
- */
- this.has = function(name) {
- return controllers.hasOwnProperty(name);
- };
-
- /**
- * @ngdoc method
- * @name $controllerProvider#register
- * @param {string|Object} name Controller name, or an object map of controllers where the keys are
- * the names and the values are the constructors.
- * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI
- * annotations in the array notation).
- */
- this.register = function(name, constructor) {
- assertNotHasOwnProperty(name, 'controller');
- if (isObject(name)) {
- extend(controllers, name);
- } else {
- controllers[name] = constructor;
- }
- };
-
- /**
- * @ngdoc method
- * @name $controllerProvider#allowGlobals
- * @description If called, allows `$controller` to find controller constructors on `window`
- */
- this.allowGlobals = function() {
- globals = true;
- };
-
-
- this.$get = ['$injector', '$window', function($injector, $window) {
-
- /**
- * @ngdoc service
- * @name $controller
- * @requires $injector
- *
- * @param {Function|string} constructor If called with a function then it's considered to be the
- * controller constructor function. Otherwise it's considered to be a string which is used
- * to retrieve the controller constructor using the following steps:
- *
- * * check if a controller with given name is registered via `$controllerProvider`
- * * check if evaluating the string on the current scope returns a constructor
- * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
- * `window` object (not recommended)
- *
- * The string can use the `controller as property` syntax, where the controller instance is published
- * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
- * to work correctly.
- *
- * @param {Object} locals Injection locals for Controller.
- * @return {Object} Instance of given controller.
- *
- * @description
- * `$controller` service is responsible for instantiating controllers.
- *
- * It's just a simple call to {@link auto.$injector $injector}, but extracted into
- * a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
- */
- return function $controller(expression, locals, later, ident) {
- // PRIVATE API:
- // param `later` --- indicates that the controller's constructor is invoked at a later time.
- // If true, $controller will allocate the object with the correct
- // prototype chain, but will not invoke the controller until a returned
- // callback is invoked.
- // param `ident` --- An optional label which overrides the label parsed from the controller
- // expression, if any.
- var instance, match, constructor, identifier;
- later = later === true;
- if (ident && isString(ident)) {
- identifier = ident;
- }
-
- if (isString(expression)) {
- match = expression.match(CNTRL_REG);
- if (!match) {
- throw $controllerMinErr('ctrlfmt',
- "Badly formed controller string '{0}'. " +
- "Must match `__name__ as __id__` or `__name__`.", expression);
- }
- constructor = match[1],
- identifier = identifier || match[3];
- expression = controllers.hasOwnProperty(constructor)
- ? controllers[constructor]
- : getter(locals.$scope, constructor, true) ||
- (globals ? getter($window, constructor, true) : undefined);
-
- assertArgFn(expression, constructor, true);
- }
-
- if (later) {
- // Instantiate controller later:
- // This machinery is used to create an instance of the object before calling the
- // controller's constructor itself.
- //
- // This allows properties to be added to the controller before the constructor is
- // invoked. Primarily, this is used for isolate scope bindings in $compile.
- //
- // This feature is not intended for use by applications, and is thus not documented
- // publicly.
- // Object creation: http://jsperf.com/create-constructor/2
- var controllerPrototype = (isArray(expression) ?
- expression[expression.length - 1] : expression).prototype;
- instance = Object.create(controllerPrototype || null);
-
- if (identifier) {
- addIdentifier(locals, identifier, instance, constructor || expression.name);
- }
-
- var instantiate;
- return instantiate = extend(function $controllerInit() {
- var result = $injector.invoke(expression, instance, locals, constructor);
- if (result !== instance && (isObject(result) || isFunction(result))) {
- instance = result;
- if (identifier) {
- // If result changed, re-assign controllerAs value to scope.
- addIdentifier(locals, identifier, instance, constructor || expression.name);
- }
- }
- return instance;
- }, {
- instance: instance,
- identifier: identifier
- });
- }
-
- instance = $injector.instantiate(expression, locals, constructor);
-
- if (identifier) {
- addIdentifier(locals, identifier, instance, constructor || expression.name);
- }
-
- return instance;
- };
-
- function addIdentifier(locals, identifier, instance, name) {
- if (!(locals && isObject(locals.$scope))) {
- throw minErr('$controller')('noscp',
- "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
- name, identifier);
- }
-
- locals.$scope[identifier] = instance;
- }
- }];
-}
-
-/**
- * @ngdoc service
- * @name $document
- * @requires $window
- *
- * @description
- * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object.
- *
- * @example
- <example module="documentExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <p>$document title: <b ng-bind="title"></b></p>
- <p>window.document title: <b ng-bind="windowTitle"></b></p>
- </div>
- </file>
- <file name="script.js">
- angular.module('documentExample', [])
- .controller('ExampleController', ['$scope', '$document', function($scope, $document) {
- $scope.title = $document[0].title;
- $scope.windowTitle = angular.element(window.document)[0].title;
- }]);
- </file>
- </example>
- */
-function $DocumentProvider() {
- this.$get = ['$window', function(window) {
- return jqLite(window.document);
- }];
-}
-
-/**
- * @ngdoc service
- * @name $exceptionHandler
- * @requires ng.$log
- *
- * @description
- * Any uncaught exception in angular expressions is delegated to this service.
- * The default implementation simply delegates to `$log.error` which logs it into
- * the browser console.
- *
- * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by
- * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing.
- *
- * ## Example:
- *
- * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught
- * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead
- * of `$log.error()`.
- *
- * ```js
- * angular.
- * module('exceptionOverwrite', []).
- * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) {
- * return function myExceptionHandler(exception, cause) {
- * logErrorsToBackend(exception, cause);
- * $log.warn(exception, cause);
- * };
- * }]);
- * ```
- *
- * <hr />
- * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind`
- * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler}
- * (unless executed during a digest).
- *
- * If you wish, you can manually delegate exceptions, e.g.
- * `try { ... } catch(e) { $exceptionHandler(e); }`
- *
- * @param {Error} exception Exception associated with the error.
- * @param {string=} cause Optional information about the context in which
- * the error was thrown.
- *
- */
-function $ExceptionHandlerProvider() {
- this.$get = ['$log', function($log) {
- return function(exception, cause) {
- $log.error.apply($log, arguments);
- };
- }];
-}
-
-var $$ForceReflowProvider = function() {
- this.$get = ['$document', function($document) {
- return function(domNode) {
- //the line below will force the browser to perform a repaint so
- //that all the animated elements within the animation frame will
- //be properly updated and drawn on screen. This is required to
- //ensure that the preparation animation is properly flushed so that
- //the active state picks up from there. DO NOT REMOVE THIS LINE.
- //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH
- //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND
- //WILL TAKE YEARS AWAY FROM YOUR LIFE.
- if (domNode) {
- if (!domNode.nodeType && domNode instanceof jqLite) {
- domNode = domNode[0];
- }
- } else {
- domNode = $document[0].body;
- }
- return domNode.offsetWidth + 1;
- };
- }];
-};
-
-var APPLICATION_JSON = 'application/json';
-var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'};
-var JSON_START = /^\[|^\{(?!\{)/;
-var JSON_ENDS = {
- '[': /]$/,
- '{': /}$/
-};
-var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
-var $httpMinErr = minErr('$http');
-var $httpMinErrLegacyFn = function(method) {
- return function() {
- throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method);
- };
-};
-
-function serializeValue(v) {
- if (isObject(v)) {
- return isDate(v) ? v.toISOString() : toJson(v);
- }
- return v;
-}
-
-
-function $HttpParamSerializerProvider() {
- /**
- * @ngdoc service
- * @name $httpParamSerializer
- * @description
- *
- * Default {@link $http `$http`} params serializer that converts objects to strings
- * according to the following rules:
- *
- * * `{'foo': 'bar'}` results in `foo=bar`
- * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object)
- * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element)
- * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object)
- *
- * Note that serializer will sort the request parameters alphabetically.
- * */
-
- this.$get = function() {
- return function ngParamSerializer(params) {
- if (!params) return '';
- var parts = [];
- forEachSorted(params, function(value, key) {
- if (value === null || isUndefined(value)) return;
- if (isArray(value)) {
- forEach(value, function(v) {
- parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
- });
- } else {
- parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value)));
- }
- });
-
- return parts.join('&');
- };
- };
-}
-
-function $HttpParamSerializerJQLikeProvider() {
- /**
- * @ngdoc service
- * @name $httpParamSerializerJQLike
- * @description
- *
- * Alternative {@link $http `$http`} params serializer that follows
- * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic.
- * The serializer will also sort the params alphabetically.
- *
- * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property:
- *
- * ```js
- * $http({
- * url: myUrl,
- * method: 'GET',
- * params: myParams,
- * paramSerializer: '$httpParamSerializerJQLike'
- * });
- * ```
- *
- * It is also possible to set it as the default `paramSerializer` in the
- * {@link $httpProvider#defaults `$httpProvider`}.
- *
- * Additionally, you can inject the serializer and use it explicitly, for example to serialize
- * form data for submission:
- *
- * ```js
- * .controller(function($http, $httpParamSerializerJQLike) {
- * //...
- *
- * $http({
- * url: myUrl,
- * method: 'POST',
- * data: $httpParamSerializerJQLike(myData),
- * headers: {
- * 'Content-Type': 'application/x-www-form-urlencoded'
- * }
- * });
- *
- * });
- * ```
- *
- * */
- this.$get = function() {
- return function jQueryLikeParamSerializer(params) {
- if (!params) return '';
- var parts = [];
- serialize(params, '', true);
- return parts.join('&');
-
- function serialize(toSerialize, prefix, topLevel) {
- if (toSerialize === null || isUndefined(toSerialize)) return;
- if (isArray(toSerialize)) {
- forEach(toSerialize, function(value, index) {
- serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']');
- });
- } else if (isObject(toSerialize) && !isDate(toSerialize)) {
- forEachSorted(toSerialize, function(value, key) {
- serialize(value, prefix +
- (topLevel ? '' : '[') +
- key +
- (topLevel ? '' : ']'));
- });
- } else {
- parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize)));
- }
- }
- };
- };
-}
-
-function defaultHttpResponseTransform(data, headers) {
- if (isString(data)) {
- // Strip json vulnerability protection prefix and trim whitespace
- var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim();
-
- if (tempData) {
- var contentType = headers('Content-Type');
- if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) {
- data = fromJson(tempData);
- }
- }
- }
-
- return data;
-}
-
-function isJsonLike(str) {
- var jsonStart = str.match(JSON_START);
- return jsonStart && JSON_ENDS[jsonStart[0]].test(str);
-}
-
-/**
- * Parse headers into key value object
- *
- * @param {string} headers Raw headers as a string
- * @returns {Object} Parsed headers as key value object
- */
-function parseHeaders(headers) {
- var parsed = createMap(), i;
-
- function fillInParsed(key, val) {
- if (key) {
- parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
- }
- }
-
- if (isString(headers)) {
- forEach(headers.split('\n'), function(line) {
- i = line.indexOf(':');
- fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1)));
- });
- } else if (isObject(headers)) {
- forEach(headers, function(headerVal, headerKey) {
- fillInParsed(lowercase(headerKey), trim(headerVal));
- });
- }
-
- return parsed;
-}
-
-
-/**
- * Returns a function that provides access to parsed headers.
- *
- * Headers are lazy parsed when first requested.
- * @see parseHeaders
- *
- * @param {(string|Object)} headers Headers to provide access to.
- * @returns {function(string=)} Returns a getter function which if called with:
- *
- * - if called with single an argument returns a single header value or null
- * - if called with no arguments returns an object containing all headers.
- */
-function headersGetter(headers) {
- var headersObj;
-
- return function(name) {
- if (!headersObj) headersObj = parseHeaders(headers);
-
- if (name) {
- var value = headersObj[lowercase(name)];
- if (value === void 0) {
- value = null;
- }
- return value;
- }
-
- return headersObj;
- };
-}
-
-
-/**
- * Chain all given functions
- *
- * This function is used for both request and response transforming
- *
- * @param {*} data Data to transform.
- * @param {function(string=)} headers HTTP headers getter fn.
- * @param {number} status HTTP status code of the response.
- * @param {(Function|Array.<Function>)} fns Function or an array of functions.
- * @returns {*} Transformed data.
- */
-function transformData(data, headers, status, fns) {
- if (isFunction(fns)) {
- return fns(data, headers, status);
- }
-
- forEach(fns, function(fn) {
- data = fn(data, headers, status);
- });
-
- return data;
-}
-
-
-function isSuccess(status) {
- return 200 <= status && status < 300;
-}
-
-
-/**
- * @ngdoc provider
- * @name $httpProvider
- * @description
- * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
- * */
-function $HttpProvider() {
- /**
- * @ngdoc property
- * @name $httpProvider#defaults
- * @description
- *
- * Object containing default values for all {@link ng.$http $http} requests.
- *
- * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with
- * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses
- * by default. See {@link $http#caching $http Caching} for more information.
- *
- * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token.
- * Defaults value is `'XSRF-TOKEN'`.
- *
- * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the
- * XSRF token. Defaults value is `'X-XSRF-TOKEN'`.
- *
- * - **`defaults.headers`** - {Object} - Default headers for all $http requests.
- * Refer to {@link ng.$http#setting-http-headers $http} for documentation on
- * setting default headers.
- * - **`defaults.headers.common`**
- * - **`defaults.headers.post`**
- * - **`defaults.headers.put`**
- * - **`defaults.headers.patch`**
- *
- *
- * - **`defaults.paramSerializer`** - `{string|function(Object<string,string>):string}` - A function
- * used to the prepare string representation of request parameters (specified as an object).
- * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
- * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
- *
- **/
- var defaults = this.defaults = {
- // transform incoming response data
- transformResponse: [defaultHttpResponseTransform],
-
- // transform outgoing request data
- transformRequest: [function(d) {
- return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d;
- }],
-
- // default headers
- headers: {
- common: {
- 'Accept': 'application/json, text/plain, */*'
- },
- post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON),
- put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON),
- patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON)
- },
-
- xsrfCookieName: 'XSRF-TOKEN',
- xsrfHeaderName: 'X-XSRF-TOKEN',
-
- paramSerializer: '$httpParamSerializer'
- };
-
- var useApplyAsync = false;
- /**
- * @ngdoc method
- * @name $httpProvider#useApplyAsync
- * @description
- *
- * Configure $http service to combine processing of multiple http responses received at around
- * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in
- * significant performance improvement for bigger applications that make many HTTP requests
- * concurrently (common during application bootstrap).
- *
- * Defaults to false. If no value is specified, returns the current configured value.
- *
- * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred
- * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window
- * to load and share the same digest cycle.
- *
- * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
- * otherwise, returns the current configured value.
- **/
- this.useApplyAsync = function(value) {
- if (isDefined(value)) {
- useApplyAsync = !!value;
- return this;
- }
- return useApplyAsync;
- };
-
- var useLegacyPromise = true;
- /**
- * @ngdoc method
- * @name $httpProvider#useLegacyPromiseExtensions
- * @description
- *
- * Configure `$http` service to return promises without the shorthand methods `success` and `error`.
- * This should be used to make sure that applications work without these methods.
- *
- * Defaults to true. If no value is specified, returns the current configured value.
- *
- * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods.
- *
- * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
- * otherwise, returns the current configured value.
- **/
- this.useLegacyPromiseExtensions = function(value) {
- if (isDefined(value)) {
- useLegacyPromise = !!value;
- return this;
- }
- return useLegacyPromise;
- };
-
- /**
- * @ngdoc property
- * @name $httpProvider#interceptors
- * @description
- *
- * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http}
- * pre-processing of request or postprocessing of responses.
- *
- * These service factories are ordered by request, i.e. they are applied in the same order as the
- * array, on request, but reverse order, on response.
- *
- * {@link ng.$http#interceptors Interceptors detailed info}
- **/
- var interceptorFactories = this.interceptors = [];
-
- this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
- function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
-
- var defaultCache = $cacheFactory('$http');
-
- /**
- * Make sure that default param serializer is exposed as a function
- */
- defaults.paramSerializer = isString(defaults.paramSerializer) ?
- $injector.get(defaults.paramSerializer) : defaults.paramSerializer;
-
- /**
- * Interceptors stored in reverse order. Inner interceptors before outer interceptors.
- * The reversal is needed so that we can build up the interception chain around the
- * server request.
- */
- var reversedInterceptors = [];
-
- forEach(interceptorFactories, function(interceptorFactory) {
- reversedInterceptors.unshift(isString(interceptorFactory)
- ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
- });
-
- /**
- * @ngdoc service
- * @kind function
- * @name $http
- * @requires ng.$httpBackend
- * @requires $cacheFactory
- * @requires $rootScope
- * @requires $q
- * @requires $injector
- *
- * @description
- * The `$http` service is a core Angular service that facilitates communication with the remote
- * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest)
- * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP).
- *
- * For unit testing applications that use `$http` service, see
- * {@link ngMock.$httpBackend $httpBackend mock}.
- *
- * For a higher level of abstraction, please check out the {@link ngResource.$resource
- * $resource} service.
- *
- * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by
- * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage
- * it is important to familiarize yourself with these APIs and the guarantees they provide.
- *
- *
- * ## General usage
- * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} —
- * that is used to generate an HTTP request and returns a {@link ng.$q promise}.
- *
- * ```js
- * // Simple GET request example:
- * $http({
- * method: 'GET',
- * url: '/someUrl'
- * }).then(function successCallback(response) {
- * // this callback will be called asynchronously
- * // when the response is available
- * }, function errorCallback(response) {
- * // called asynchronously if an error occurs
- * // or server returns response with an error status.
- * });
- * ```
- *
- * The response object has these properties:
- *
- * - **data** – `{string|Object}` – The response body transformed with the transform
- * functions.
- * - **status** – `{number}` – HTTP status code of the response.
- * - **headers** – `{function([headerName])}` – Header getter function.
- * - **config** – `{Object}` – The configuration object that was used to generate the request.
- * - **statusText** – `{string}` – HTTP status text of the response.
- *
- * A response status code between 200 and 299 is considered a success status and will result in
- * the success callback being called. Any response status code outside of that range is
- * considered an error status and will result in the error callback being called.
- * Also, status codes less than -1 are normalized to zero. -1 usually means the request was
- * aborted, e.g. using a `config.timeout`.
- * Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning
- * that the outcome (success or error) will be determined by the final response status code.
- *
- *
- * ## Shortcut methods
- *
- * Shortcut methods are also available. All shortcut methods require passing in the URL, and
- * request data must be passed in for POST/PUT requests. An optional config can be passed as the
- * last argument.
- *
- * ```js
- * $http.get('/someUrl', config).then(successCallback, errorCallback);
- * $http.post('/someUrl', data, config).then(successCallback, errorCallback);
- * ```
- *
- * Complete list of shortcut methods:
- *
- * - {@link ng.$http#get $http.get}
- * - {@link ng.$http#head $http.head}
- * - {@link ng.$http#post $http.post}
- * - {@link ng.$http#put $http.put}
- * - {@link ng.$http#delete $http.delete}
- * - {@link ng.$http#jsonp $http.jsonp}
- * - {@link ng.$http#patch $http.patch}
- *
- *
- * ## Writing Unit Tests that use $http
- * When unit testing (using {@link ngMock ngMock}), it is necessary to call
- * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
- * request using trained responses.
- *
- * ```
- * $httpBackend.expectGET(...);
- * $http.get(...);
- * $httpBackend.flush();
- * ```
- *
- * ## Deprecation Notice
- * <div class="alert alert-danger">
- * The `$http` legacy promise methods `success` and `error` have been deprecated.
- * Use the standard `then` method instead.
- * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to
- * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error.
- * </div>
- *
- * ## Setting HTTP Headers
- *
- * The $http service will automatically add certain HTTP headers to all requests. These defaults
- * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration
- * object, which currently contains this default configuration:
- *
- * - `$httpProvider.defaults.headers.common` (headers that are common for all requests):
- * - `Accept: application/json, text/plain, * / *`
- * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests)
- * - `Content-Type: application/json`
- * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests)
- * - `Content-Type: application/json`
- *
- * To add or overwrite these defaults, simply add or remove a property from these configuration
- * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object
- * with the lowercased HTTP method name as the key, e.g.
- * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`.
- *
- * The defaults can also be set at runtime via the `$http.defaults` object in the same
- * fashion. For example:
- *
- * ```
- * module.run(function($http) {
- * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w';
- * });
- * ```
- *
- * In addition, you can supply a `headers` property in the config object passed when
- * calling `$http(config)`, which overrides the defaults without changing them globally.
- *
- * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis,
- * Use the `headers` property, setting the desired header to `undefined`. For example:
- *
- * ```js
- * var req = {
- * method: 'POST',
- * url: 'http://example.com',
- * headers: {
- * 'Content-Type': undefined
- * },
- * data: { test: 'test' }
- * }
- *
- * $http(req).then(function(){...}, function(){...});
- * ```
- *
- * ## Transforming Requests and Responses
- *
- * Both requests and responses can be transformed using transformation functions: `transformRequest`
- * and `transformResponse`. These properties can be a single function that returns
- * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions,
- * which allows you to `push` or `unshift` a new transformation function into the transformation chain.
- *
- * <div class="alert alert-warning">
- * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline.
- * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference).
- * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest
- * function will be reflected on the scope and in any templates where the object is data-bound.
- * To prevent this, transform functions should have no side-effects.
- * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return.
- * </div>
- *
- * ### Default Transformations
- *
- * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and
- * `defaults.transformResponse` properties. If a request does not provide its own transformations
- * then these will be applied.
- *
- * You can augment or replace the default transformations by modifying these properties by adding to or
- * replacing the array.
- *
- * Angular provides the following default transformations:
- *
- * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`):
- *
- * - If the `data` property of the request configuration object contains an object, serialize it
- * into JSON format.
- *
- * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`):
- *
- * - If XSRF prefix is detected, strip it (see Security Considerations section below).
- * - If JSON response is detected, deserialize it using a JSON parser.
- *
- *
- * ### Overriding the Default Transformations Per Request
- *
- * If you wish to override the request/response transformations only for a single request then provide
- * `transformRequest` and/or `transformResponse` properties on the configuration object passed
- * into `$http`.
- *
- * Note that if you provide these properties on the config object the default transformations will be
- * overwritten. If you wish to augment the default transformations then you must include them in your
- * local transformation array.
- *
- * The following code demonstrates adding a new response transformation to be run after the default response
- * transformations have been run.
- *
- * ```js
- * function appendTransform(defaults, transform) {
- *
- * // We can't guarantee that the default transformation is an array
- * defaults = angular.isArray(defaults) ? defaults : [defaults];
- *
- * // Append the new transformation to the defaults
- * return defaults.concat(transform);
- * }
- *
- * $http({
- * url: '...',
- * method: 'GET',
- * transformResponse: appendTransform($http.defaults.transformResponse, function(value) {
- * return doTransform(value);
- * })
- * });
- * ```
- *
- *
- * ## Caching
- *
- * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must
- * set the config.cache value or the default cache value to TRUE or to a cache object (created
- * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes
- * precedence over the default cache value.
- *
- * In order to:
- * * cache all responses - set the default cache value to TRUE or to a cache object
- * * cache a specific response - set config.cache value to TRUE or to a cache object
- *
- * If caching is enabled, but neither the default cache nor config.cache are set to a cache object,
- * then the default `$cacheFactory("$http")` object is used.
- *
- * The default cache value can be set by updating the
- * {@link ng.$http#defaults `$http.defaults.cache`} property or the
- * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property.
- *
- * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using
- * the relevant cache object. The next time the same request is made, the response is returned
- * from the cache without sending a request to the server.
- *
- * Take note that:
- *
- * * Only GET and JSONP requests are cached.
- * * The cache key is the request URL including search parameters; headers are not considered.
- * * Cached responses are returned asynchronously, in the same way as responses from the server.
- * * If multiple identical requests are made using the same cache, which is not yet populated,
- * one request will be made to the server and remaining requests will return the same response.
- * * A cache-control header on the response does not affect if or how responses are cached.
- *
- *
- * ## Interceptors
- *
- * Before you start creating interceptors, be sure to understand the
- * {@link ng.$q $q and deferred/promise APIs}.
- *
- * For purposes of global error handling, authentication, or any kind of synchronous or
- * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be
- * able to intercept requests before they are handed to the server and
- * responses before they are handed over to the application code that
- * initiated these requests. The interceptors leverage the {@link ng.$q
- * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing.
- *
- * The interceptors are service factories that are registered with the `$httpProvider` by
- * adding them to the `$httpProvider.interceptors` array. The factory is called and
- * injected with dependencies (if specified) and returns the interceptor.
- *
- * There are two kinds of interceptors (and two kinds of rejection interceptors):
- *
- * * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to
- * modify the `config` object or create a new one. The function needs to return the `config`
- * object directly, or a promise containing the `config` or a new `config` object.
- * * `requestError`: interceptor gets called when a previous interceptor threw an error or
- * resolved with a rejection.
- * * `response`: interceptors get called with http `response` object. The function is free to
- * modify the `response` object or create a new one. The function needs to return the `response`
- * object directly, or as a promise containing the `response` or a new `response` object.
- * * `responseError`: interceptor gets called when a previous interceptor threw an error or
- * resolved with a rejection.
- *
- *
- * ```js
- * // register the interceptor as a service
- * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
- * return {
- * // optional method
- * 'request': function(config) {
- * // do something on success
- * return config;
- * },
- *
- * // optional method
- * 'requestError': function(rejection) {
- * // do something on error
- * if (canRecover(rejection)) {
- * return responseOrNewPromise
- * }
- * return $q.reject(rejection);
- * },
- *
- *
- *
- * // optional method
- * 'response': function(response) {
- * // do something on success
- * return response;
- * },
- *
- * // optional method
- * 'responseError': function(rejection) {
- * // do something on error
- * if (canRecover(rejection)) {
- * return responseOrNewPromise
- * }
- * return $q.reject(rejection);
- * }
- * };
- * });
- *
- * $httpProvider.interceptors.push('myHttpInterceptor');
- *
- *
- * // alternatively, register the interceptor via an anonymous factory
- * $httpProvider.interceptors.push(function($q, dependency1, dependency2) {
- * return {
- * 'request': function(config) {
- * // same as above
- * },
- *
- * 'response': function(response) {
- * // same as above
- * }
- * };
- * });
- * ```
- *
- * ## Security Considerations
- *
- * When designing web applications, consider security threats from:
- *
- * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx)
- * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery)
- *
- * Both server and the client must cooperate in order to eliminate these threats. Angular comes
- * pre-configured with strategies that address these issues, but for this to work backend server
- * cooperation is required.
- *
- * ### JSON Vulnerability Protection
- *
- * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx)
- * allows third party website to turn your JSON resource URL into
- * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To
- * counter this your server can prefix all JSON requests with following string `")]}',\n"`.
- * Angular will automatically strip the prefix before processing it as JSON.
- *
- * For example if your server needs to return:
- * ```js
- * ['one','two']
- * ```
- *
- * which is vulnerable to attack, your server can return:
- * ```js
- * )]}',
- * ['one','two']
- * ```
- *
- * Angular will strip the prefix, before processing the JSON.
- *
- *
- * ### Cross Site Request Forgery (XSRF) Protection
- *
- * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by
- * which the attacker can trick an authenticated user into unknowingly executing actions on your
- * website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the
- * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
- * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
- * cookie, your server can be assured that the XHR came from JavaScript running on your domain.
- * The header will not be set for cross-domain requests.
- *
- * To take advantage of this, your server needs to set a token in a JavaScript readable session
- * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
- * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure
- * that only JavaScript running on your domain could have sent the request. The token must be
- * unique for each user and must be verifiable by the server (to prevent the JavaScript from
- * making up its own tokens). We recommend that the token is a digest of your site's
- * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography&#41;)
- * for added security.
- *
- * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName
- * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
- * or the per-request config object.
- *
- * In order to prevent collisions in environments where multiple Angular apps share the
- * same domain or subdomain, we recommend that each application uses unique cookie name.
- *
- * @param {object} config Object describing the request to be made and how it should be
- * processed. The object has following properties:
- *
- * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
- * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
- * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized
- * with the `paramSerializer` and appended as GET parameters.
- * - **data** – `{string|Object}` – Data to be sent as the request message data.
- * - **headers** – `{Object}` – Map of strings or functions which return strings representing
- * HTTP headers to send to the server. If the return value of a function is null, the
- * header will not be sent. Functions accept a config object as an argument.
- * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object.
- * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`.
- * The handler will be called in the context of a `$apply` block.
- * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload
- * object. To bind events to the XMLHttpRequest object, use `eventHandlers`.
- * The handler will be called in the context of a `$apply` block.
- * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token.
- * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token.
- * - **transformRequest** –
- * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
- * transform function or an array of such functions. The transform function takes the http
- * request body and headers and returns its transformed (typically serialized) version.
- * See {@link ng.$http#overriding-the-default-transformations-per-request
- * Overriding the Default Transformations}
- * - **transformResponse** –
- * `{function(data, headersGetter, status)|Array.<function(data, headersGetter, status)>}` –
- * transform function or an array of such functions. The transform function takes the http
- * response body, headers and status and returns its transformed (typically deserialized) version.
- * See {@link ng.$http#overriding-the-default-transformations-per-request
- * Overriding the Default Transformations}
- * - **paramSerializer** - `{string|function(Object<string,string>):string}` - A function used to
- * prepare the string representation of request parameters (specified as an object).
- * If specified as string, it is interpreted as function registered with the
- * {@link $injector $injector}, which means you can create your own serializer
- * by registering it as a {@link auto.$provide#service service}.
- * The default serializer is the {@link $httpParamSerializer $httpParamSerializer};
- * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike}
- * - **cache** – `{boolean|Object}` – A boolean value or object created with
- * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response.
- * See {@link $http#caching $http Caching} for more information.
- * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise}
- * that should abort the request when resolved.
- * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the
- * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials)
- * for more information.
- * - **responseType** - `{string}` - see
- * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype).
- *
- * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object
- * when the request succeeds or fails.
- *
- *
- * @property {Array.<Object>} pendingRequests Array of config objects for currently pending
- * requests. This is primarily meant to be used for debugging purposes.
- *
- *
- * @example
-<example module="httpExample">
-<file name="index.html">
- <div ng-controller="FetchController">
- <select ng-model="method" aria-label="Request method">
- <option>GET</option>
- <option>JSONP</option>
- </select>
- <input type="text" ng-model="url" size="80" aria-label="URL" />
- <button id="fetchbtn" ng-click="fetch()">fetch</button><br>
- <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
- <button id="samplejsonpbtn"
- ng-click="updateModel('JSONP',
- 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">
- Sample JSONP
- </button>
- <button id="invalidjsonpbtn"
- ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')">
- Invalid JSONP
- </button>
- <pre>http status code: {{status}}</pre>
- <pre>http response data: {{data}}</pre>
- </div>
-</file>
-<file name="script.js">
- angular.module('httpExample', [])
- .controller('FetchController', ['$scope', '$http', '$templateCache',
- function($scope, $http, $templateCache) {
- $scope.method = 'GET';
- $scope.url = 'http-hello.html';
-
- $scope.fetch = function() {
- $scope.code = null;
- $scope.response = null;
-
- $http({method: $scope.method, url: $scope.url, cache: $templateCache}).
- then(function(response) {
- $scope.status = response.status;
- $scope.data = response.data;
- }, function(response) {
- $scope.data = response.data || "Request failed";
- $scope.status = response.status;
- });
- };
-
- $scope.updateModel = function(method, url) {
- $scope.method = method;
- $scope.url = url;
- };
- }]);
-</file>
-<file name="http-hello.html">
- Hello, $http!
-</file>
-<file name="protractor.js" type="protractor">
- var status = element(by.binding('status'));
- var data = element(by.binding('data'));
- var fetchBtn = element(by.id('fetchbtn'));
- var sampleGetBtn = element(by.id('samplegetbtn'));
- var sampleJsonpBtn = element(by.id('samplejsonpbtn'));
- var invalidJsonpBtn = element(by.id('invalidjsonpbtn'));
-
- it('should make an xhr GET request', function() {
- sampleGetBtn.click();
- fetchBtn.click();
- expect(status.getText()).toMatch('200');
- expect(data.getText()).toMatch(/Hello, \$http!/);
- });
-
-// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185
-// it('should make a JSONP request to angularjs.org', function() {
-// sampleJsonpBtn.click();
-// fetchBtn.click();
-// expect(status.getText()).toMatch('200');
-// expect(data.getText()).toMatch(/Super Hero!/);
-// });
-
- it('should make JSONP request to invalid URL and invoke the error handler',
- function() {
- invalidJsonpBtn.click();
- fetchBtn.click();
- expect(status.getText()).toMatch('0');
- expect(data.getText()).toMatch('Request failed');
- });
-</file>
-</example>
- */
- function $http(requestConfig) {
-
- if (!isObject(requestConfig)) {
- throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
- }
-
- if (!isString(requestConfig.url)) {
- throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url);
- }
-
- var config = extend({
- method: 'get',
- transformRequest: defaults.transformRequest,
- transformResponse: defaults.transformResponse,
- paramSerializer: defaults.paramSerializer
- }, requestConfig);
-
- config.headers = mergeHeaders(requestConfig);
- config.method = uppercase(config.method);
- config.paramSerializer = isString(config.paramSerializer) ?
- $injector.get(config.paramSerializer) : config.paramSerializer;
-
- var requestInterceptors = [];
- var responseInterceptors = [];
- var promise = $q.when(config);
-
- // apply interceptors
- forEach(reversedInterceptors, function(interceptor) {
- if (interceptor.request || interceptor.requestError) {
- requestInterceptors.unshift(interceptor.request, interceptor.requestError);
- }
- if (interceptor.response || interceptor.responseError) {
- responseInterceptors.push(interceptor.response, interceptor.responseError);
- }
- });
-
- promise = chainInterceptors(promise, requestInterceptors);
- promise = promise.then(serverRequest);
- promise = chainInterceptors(promise, responseInterceptors);
-
- if (useLegacyPromise) {
- promise.success = function(fn) {
- assertArgFn(fn, 'fn');
-
- promise.then(function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
-
- promise.error = function(fn) {
- assertArgFn(fn, 'fn');
-
- promise.then(null, function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
- } else {
- promise.success = $httpMinErrLegacyFn('success');
- promise.error = $httpMinErrLegacyFn('error');
- }
-
- return promise;
-
-
- function chainInterceptors(promise, interceptors) {
- for (var i = 0, ii = interceptors.length; i < ii;) {
- var thenFn = interceptors[i++];
- var rejectFn = interceptors[i++];
-
- promise = promise.then(thenFn, rejectFn);
- }
-
- interceptors.length = 0;
-
- return promise;
- }
-
- function executeHeaderFns(headers, config) {
- var headerContent, processedHeaders = {};
-
- forEach(headers, function(headerFn, header) {
- if (isFunction(headerFn)) {
- headerContent = headerFn(config);
- if (headerContent != null) {
- processedHeaders[header] = headerContent;
- }
- } else {
- processedHeaders[header] = headerFn;
- }
- });
-
- return processedHeaders;
- }
-
- function mergeHeaders(config) {
- var defHeaders = defaults.headers,
- reqHeaders = extend({}, config.headers),
- defHeaderName, lowercaseDefHeaderName, reqHeaderName;
-
- defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]);
-
- // using for-in instead of forEach to avoid unnecessary iteration after header has been found
- defaultHeadersIteration:
- for (defHeaderName in defHeaders) {
- lowercaseDefHeaderName = lowercase(defHeaderName);
-
- for (reqHeaderName in reqHeaders) {
- if (lowercase(reqHeaderName) === lowercaseDefHeaderName) {
- continue defaultHeadersIteration;
- }
- }
-
- reqHeaders[defHeaderName] = defHeaders[defHeaderName];
- }
-
- // execute if header value is a function for merged headers
- return executeHeaderFns(reqHeaders, shallowCopy(config));
- }
-
- function serverRequest(config) {
- var headers = config.headers;
- var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest);
-
- // strip content-type if data is undefined
- if (isUndefined(reqData)) {
- forEach(headers, function(value, header) {
- if (lowercase(header) === 'content-type') {
- delete headers[header];
- }
- });
- }
-
- if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
- config.withCredentials = defaults.withCredentials;
- }
-
- // send request
- return sendReq(config, reqData).then(transformResponse, transformResponse);
- }
-
- function transformResponse(response) {
- // make a copy since the response must be cacheable
- var resp = extend({}, response);
- resp.data = transformData(response.data, response.headers, response.status,
- config.transformResponse);
- return (isSuccess(response.status))
- ? resp
- : $q.reject(resp);
- }
- }
-
- $http.pendingRequests = [];
-
- /**
- * @ngdoc method
- * @name $http#get
- *
- * @description
- * Shortcut method to perform `GET` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#delete
- *
- * @description
- * Shortcut method to perform `DELETE` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#head
- *
- * @description
- * Shortcut method to perform `HEAD` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#jsonp
- *
- * @description
- * Shortcut method to perform `JSONP` request.
- * If you would like to customise where and how the callbacks are stored then try overriding
- * or decorating the {@link $jsonpCallbacks} service.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request.
- * The name of the callback should be the string `JSON_CALLBACK`.
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
- createShortMethods('get', 'delete', 'head', 'jsonp');
-
- /**
- * @ngdoc method
- * @name $http#post
- *
- * @description
- * Shortcut method to perform `POST` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {*} data Request content
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#put
- *
- * @description
- * Shortcut method to perform `PUT` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {*} data Request content
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#patch
- *
- * @description
- * Shortcut method to perform `PATCH` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {*} data Request content
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
- createShortMethodsWithData('post', 'put', 'patch');
-
- /**
- * @ngdoc property
- * @name $http#defaults
- *
- * @description
- * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of
- * default headers, withCredentials as well as request and response transformations.
- *
- * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above.
- */
- $http.defaults = defaults;
-
-
- return $http;
-
-
- function createShortMethods(names) {
- forEach(arguments, function(name) {
- $http[name] = function(url, config) {
- return $http(extend({}, config || {}, {
- method: name,
- url: url
- }));
- };
- });
- }
-
-
- function createShortMethodsWithData(name) {
- forEach(arguments, function(name) {
- $http[name] = function(url, data, config) {
- return $http(extend({}, config || {}, {
- method: name,
- url: url,
- data: data
- }));
- };
- });
- }
-
-
- /**
- * Makes the request.
- *
- * !!! ACCESSES CLOSURE VARS:
- * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests
- */
- function sendReq(config, reqData) {
- var deferred = $q.defer(),
- promise = deferred.promise,
- cache,
- cachedResp,
- reqHeaders = config.headers,
- url = buildUrl(config.url, config.paramSerializer(config.params));
-
- $http.pendingRequests.push(config);
- promise.then(removePendingReq, removePendingReq);
-
-
- if ((config.cache || defaults.cache) && config.cache !== false &&
- (config.method === 'GET' || config.method === 'JSONP')) {
- cache = isObject(config.cache) ? config.cache
- : isObject(defaults.cache) ? defaults.cache
- : defaultCache;
- }
-
- if (cache) {
- cachedResp = cache.get(url);
- if (isDefined(cachedResp)) {
- if (isPromiseLike(cachedResp)) {
- // cached request has already been sent, but there is no response yet
- cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult);
- } else {
- // serving from cache
- if (isArray(cachedResp)) {
- resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
- } else {
- resolvePromise(cachedResp, 200, {}, 'OK');
- }
- }
- } else {
- // put the promise for the non-transformed response into cache as a placeholder
- cache.put(url, promise);
- }
- }
-
-
- // if we won't have the response in cache, set the xsrf headers and
- // send the request to the backend
- if (isUndefined(cachedResp)) {
- var xsrfValue = urlIsSameOrigin(config.url)
- ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
- : undefined;
- if (xsrfValue) {
- reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
- }
-
- $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
- config.withCredentials, config.responseType,
- createApplyHandlers(config.eventHandlers),
- createApplyHandlers(config.uploadEventHandlers));
- }
-
- return promise;
-
- function createApplyHandlers(eventHandlers) {
- if (eventHandlers) {
- var applyHandlers = {};
- forEach(eventHandlers, function(eventHandler, key) {
- applyHandlers[key] = function(event) {
- if (useApplyAsync) {
- $rootScope.$applyAsync(callEventHandler);
- } else if ($rootScope.$$phase) {
- callEventHandler();
- } else {
- $rootScope.$apply(callEventHandler);
- }
-
- function callEventHandler() {
- eventHandler(event);
- }
- };
- });
- return applyHandlers;
- }
- }
-
-
- /**
- * Callback registered to $httpBackend():
- * - caches the response if desired
- * - resolves the raw $http promise
- * - calls $apply
- */
- function done(status, response, headersString, statusText) {
- if (cache) {
- if (isSuccess(status)) {
- cache.put(url, [status, response, parseHeaders(headersString), statusText]);
- } else {
- // remove promise from the cache
- cache.remove(url);
- }
- }
-
- function resolveHttpPromise() {
- resolvePromise(response, status, headersString, statusText);
- }
-
- if (useApplyAsync) {
- $rootScope.$applyAsync(resolveHttpPromise);
- } else {
- resolveHttpPromise();
- if (!$rootScope.$$phase) $rootScope.$apply();
- }
- }
-
-
- /**
- * Resolves the raw $http promise.
- */
- function resolvePromise(response, status, headers, statusText) {
- //status: HTTP response status code, 0, -1 (aborted by timeout / promise)
- status = status >= -1 ? status : 0;
-
- (isSuccess(status) ? deferred.resolve : deferred.reject)({
- data: response,
- status: status,
- headers: headersGetter(headers),
- config: config,
- statusText: statusText
- });
- }
-
- function resolvePromiseWithResult(result) {
- resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText);
- }
-
- function removePendingReq() {
- var idx = $http.pendingRequests.indexOf(config);
- if (idx !== -1) $http.pendingRequests.splice(idx, 1);
- }
- }
-
-
- function buildUrl(url, serializedParams) {
- if (serializedParams.length > 0) {
- url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams;
- }
- return url;
- }
- }];
-}
-
-/**
- * @ngdoc service
- * @name $xhrFactory
- *
- * @description
- * Factory function used to create XMLHttpRequest objects.
- *
- * Replace or decorate this service to create your own custom XMLHttpRequest objects.
- *
- * ```
- * angular.module('myApp', [])
- * .factory('$xhrFactory', function() {
- * return function createXhr(method, url) {
- * return new window.XMLHttpRequest({mozSystem: true});
- * };
- * });
- * ```
- *
- * @param {string} method HTTP method of the request (GET, POST, PUT, ..)
- * @param {string} url URL of the request.
- */
-function $xhrFactoryProvider() {
- this.$get = function() {
- return function createXhr() {
- return new window.XMLHttpRequest();
- };
- };
-}
-
-/**
- * @ngdoc service
- * @name $httpBackend
- * @requires $jsonpCallbacks
- * @requires $document
- * @requires $xhrFactory
- *
- * @description
- * HTTP backend used by the {@link ng.$http service} that delegates to
- * XMLHttpRequest object or JSONP and deals with browser incompatibilities.
- *
- * You should never need to use this service directly, instead use the higher-level abstractions:
- * {@link ng.$http $http} or {@link ngResource.$resource $resource}.
- *
- * During testing this implementation is swapped with {@link ngMock.$httpBackend mock
- * $httpBackend} which can be trained with responses.
- */
-function $HttpBackendProvider() {
- this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) {
- return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]);
- }];
-}
-
-function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
- // TODO(vojta): fix the signature
- return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) {
- $browser.$$incOutstandingRequestCount();
- url = url || $browser.url();
-
- if (lowercase(method) === 'jsonp') {
- var callbackPath = callbacks.createCallback(url);
- var jsonpDone = jsonpReq(url, callbackPath, function(status, text) {
- // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING)
- var response = (status === 200) && callbacks.getResponse(callbackPath);
- completeRequest(callback, status, response, "", text);
- callbacks.removeCallback(callbackPath);
- });
- } else {
-
- var xhr = createXhr(method, url);
-
- xhr.open(method, url, true);
- forEach(headers, function(value, key) {
- if (isDefined(value)) {
- xhr.setRequestHeader(key, value);
- }
- });
-
- xhr.onload = function requestLoaded() {
- var statusText = xhr.statusText || '';
-
- // responseText is the old-school way of retrieving response (supported by IE9)
- // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
- var 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;
-
- // 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;
-
- forEach(eventHandlers, function(value, key) {
- xhr.addEventListener(key, value);
- });
-
- forEach(uploadEventHandlers, function(value, key) {
- xhr.upload.addEventListener(key, value);
- });
-
- if (withCredentials) {
- xhr.withCredentials = true;
- }
-
- if (responseType) {
- try {
- xhr.responseType = responseType;
- } catch (e) {
- // WebKit added support for the json responseType value on 09/03/2013
- // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are
- // known to throw when setting the value "json" as the response type. Other older
- // browsers implementing the responseType
- //
- // The json response type can be ignored if not supported, because JSON payloads are
- // parsed on the client-side regardless.
- if (responseType !== 'json') {
- throw e;
- }
- }
- }
-
- xhr.send(isUndefined(post) ? null : post);
- }
-
- if (timeout > 0) {
- var timeoutId = $browserDefer(timeoutRequest, timeout);
- } else if (isPromiseLike(timeout)) {
- timeout.then(timeoutRequest);
- }
-
-
- function timeoutRequest() {
- jsonpDone && jsonpDone();
- xhr && xhr.abort();
- }
-
- function completeRequest(callback, status, response, headersString, statusText) {
- // cancel timeout and subsequent timeout promise resolution
- if (isDefined(timeoutId)) {
- $browserDefer.cancel(timeoutId);
- }
- jsonpDone = xhr = null;
-
- callback(status, response, headersString, statusText);
- $browser.$$completeOutstandingRequest(noop);
- }
- };
-
- function jsonpReq(url, callbackPath, done) {
- url = url.replace('JSON_CALLBACK', callbackPath);
- // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.:
- // - fetches local scripts via XHR and evals them
- // - adds and immediately removes script elements from the document
- var script = rawDocument.createElement('script'), callback = null;
- script.type = "text/javascript";
- script.src = url;
- script.async = true;
-
- callback = function(event) {
- removeEventListenerFn(script, "load", callback);
- removeEventListenerFn(script, "error", callback);
- rawDocument.body.removeChild(script);
- script = null;
- var status = -1;
- var text = "unknown";
-
- if (event) {
- if (event.type === "load" && !callbacks.wasCalled(callbackPath)) {
- event = { type: "error" };
- }
- text = event.type;
- status = event.type === "error" ? 404 : 200;
- }
-
- if (done) {
- done(status, text);
- }
- };
-
- addEventListenerFn(script, "load", callback);
- addEventListenerFn(script, "error", callback);
- rawDocument.body.appendChild(script);
- return callback;
- }
-}
-
-var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate');
-$interpolateMinErr.throwNoconcat = function(text) {
- throw $interpolateMinErr('noconcat',
- "Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
- "interpolations that concatenate multiple expressions when a trusted value is " +
- "required. See http://docs.angularjs.org/api/ng.$sce", text);
-};
-
-$interpolateMinErr.interr = function(text, err) {
- return $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString());
-};
-
-/**
- * @ngdoc provider
- * @name $interpolateProvider
- *
- * @description
- *
- * Used for configuring the interpolation markup. Defaults to `{{` and `}}`.
- *
- * <div class="alert alert-danger">
- * This feature is sometimes used to mix different markup languages, e.g. to wrap an Angular
- * template within a Python Jinja template (or any other template language). Mixing templating
- * languages is **very dangerous**. The embedding template language will not safely escape Angular
- * expressions, so any user-controlled values in the template will cause Cross Site Scripting (XSS)
- * security bugs!
- * </div>
- *
- * @example
-<example name="custom-interpolation-markup" module="customInterpolationApp">
-<file name="index.html">
-<script>
- var customInterpolationApp = angular.module('customInterpolationApp', []);
-
- customInterpolationApp.config(function($interpolateProvider) {
- $interpolateProvider.startSymbol('//');
- $interpolateProvider.endSymbol('//');
- });
-
-
- customInterpolationApp.controller('DemoController', function() {
- this.label = "This binding is brought you by // interpolation symbols.";
- });
-</script>
-<div ng-controller="DemoController as demo">
- //demo.label//
-</div>
-</file>
-<file name="protractor.js" type="protractor">
- it('should interpolate binding with custom symbols', function() {
- expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.');
- });
-</file>
-</example>
- */
-function $InterpolateProvider() {
- var startSymbol = '{{';
- var endSymbol = '}}';
-
- /**
- * @ngdoc method
- * @name $interpolateProvider#startSymbol
- * @description
- * Symbol to denote start of expression in the interpolated string. Defaults to `{{`.
- *
- * @param {string=} value new value to set the starting symbol to.
- * @returns {string|self} Returns the symbol when used as getter and self if used as setter.
- */
- this.startSymbol = function(value) {
- if (value) {
- startSymbol = value;
- return this;
- } else {
- return startSymbol;
- }
- };
-
- /**
- * @ngdoc method
- * @name $interpolateProvider#endSymbol
- * @description
- * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`.
- *
- * @param {string=} value new value to set the ending symbol to.
- * @returns {string|self} Returns the symbol when used as getter and self if used as setter.
- */
- this.endSymbol = function(value) {
- if (value) {
- endSymbol = value;
- return this;
- } else {
- return endSymbol;
- }
- };
-
-
- this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) {
- var startSymbolLength = startSymbol.length,
- endSymbolLength = endSymbol.length,
- escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'),
- escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g');
-
- function escape(ch) {
- return '\\\\\\' + ch;
- }
-
- function unescapeText(text) {
- return text.replace(escapedStartRegexp, startSymbol).
- replace(escapedEndRegexp, endSymbol);
- }
-
- function stringify(value) {
- if (value == null) { // null || undefined
- return '';
- }
- switch (typeof value) {
- case 'string':
- break;
- case 'number':
- value = '' + value;
- break;
- default:
- value = toJson(value);
- }
-
- return value;
- }
-
- //TODO: this is the same as the constantWatchDelegate in parse.js
- function constantWatchDelegate(scope, listener, objectEquality, constantInterp) {
- var unwatch;
- return unwatch = scope.$watch(function constantInterpolateWatch(scope) {
- unwatch();
- return constantInterp(scope);
- }, listener, objectEquality);
- }
-
- /**
- * @ngdoc service
- * @name $interpolate
- * @kind function
- *
- * @requires $parse
- * @requires $sce
- *
- * @description
- *
- * Compiles a string with markup into an interpolation function. This service is used by the
- * HTML {@link ng.$compile $compile} service for data binding. See
- * {@link ng.$interpolateProvider $interpolateProvider} for configuring the
- * interpolation markup.
- *
- *
- * ```js
- * var $interpolate = ...; // injected
- * var exp = $interpolate('Hello {{name | uppercase}}!');
- * expect(exp({name:'Angular'})).toEqual('Hello ANGULAR!');
- * ```
- *
- * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is
- * `true`, the interpolation function will return `undefined` unless all embedded expressions
- * evaluate to a value other than `undefined`.
- *
- * ```js
- * var $interpolate = ...; // injected
- * var context = {greeting: 'Hello', name: undefined };
- *
- * // default "forgiving" mode
- * var exp = $interpolate('{{greeting}} {{name}}!');
- * expect(exp(context)).toEqual('Hello !');
- *
- * // "allOrNothing" mode
- * exp = $interpolate('{{greeting}} {{name}}!', false, null, true);
- * expect(exp(context)).toBeUndefined();
- * context.name = 'Angular';
- * expect(exp(context)).toEqual('Hello Angular!');
- * ```
- *
- * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior.
- *
- * #### Escaped Interpolation
- * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers
- * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash).
- * It will be rendered as a regular start/end marker, and will not be interpreted as an expression
- * or binding.
- *
- * This enables web-servers to prevent script injection attacks and defacing attacks, to some
- * degree, while also enabling code examples to work without relying on the
- * {@link ng.directive:ngNonBindable ngNonBindable} directive.
- *
- * **For security purposes, it is strongly encouraged that web servers escape user-supplied data,
- * replacing angle brackets (&lt;, &gt;) with &amp;lt; and &amp;gt; respectively, and replacing all
- * interpolation start/end markers with their escaped counterparts.**
- *
- * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered
- * output when the $interpolate service processes the text. So, for HTML elements interpolated
- * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter
- * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such,
- * this is typically useful only when user-data is used in rendering a template from the server, or
- * when otherwise untrusted data is used by a directive.
- *
- * <example>
- * <file name="index.html">
- * <div ng-init="username='A user'">
- * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\}
- * </p>
- * <p><strong>{{username}}</strong> attempts to inject code which will deface the
- * application, but fails to accomplish their task, because the server has correctly
- * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash)
- * characters.</p>
- * <p>Instead, the result of the attempted script injection is visible, and can be removed
- * from the database by an administrator.</p>
- * </div>
- * </file>
- * </example>
- *
- * @knownIssue
- * It is currently not possible for an interpolated expression to contain the interpolation end
- * symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e.
- * an interpolated expression consisting of a single-quote (`'`) and the `' }}` string.
- *
- * @knownIssue
- * All directives and components must use the standard `{{` `}}` interpolation symbols
- * in their templates. If you change the application interpolation symbols the {@link $compile}
- * service will attempt to denormalize the standard symbols to the custom symbols.
- * The denormalization process is not clever enough to know not to replace instances of the standard
- * symbols where they would not normally be treated as interpolation symbols. For example in the following
- * code snippet the closing braces of the literal object will get incorrectly denormalized:
- *
- * ```
- * <div data-context='{"context":{"id":3,"type":"page"}}">
- * ```
- *
- * The workaround is to ensure that such instances are separated by whitespace:
- * ```
- * <div data-context='{"context":{"id":3,"type":"page"} }">
- * ```
- *
- * See https://github.com/angular/angular.js/pull/14610#issuecomment-219401099 for more information.
- *
- * @param {string} text The text with markup to interpolate.
- * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have
- * embedded expression in order to return an interpolation function. Strings with no
- * embedded expression will return null for the interpolation function.
- * @param {string=} trustedContext when provided, the returned function passes the interpolated
- * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult,
- * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that
- * provides Strict Contextual Escaping for details.
- * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined
- * unless all embedded expressions evaluate to a value other than `undefined`.
- * @returns {function(context)} an interpolation function which is used to compute the
- * interpolated string. The function has these parameters:
- *
- * - `context`: evaluation context for all expressions embedded in the interpolated text
- */
- function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
- // Provide a quick exit and simplified result function for text with no interpolation
- if (!text.length || text.indexOf(startSymbol) === -1) {
- var constantInterp;
- if (!mustHaveExpression) {
- var unescapedText = unescapeText(text);
- constantInterp = valueFn(unescapedText);
- constantInterp.exp = text;
- constantInterp.expressions = [];
- constantInterp.$$watchDelegate = constantWatchDelegate;
- }
- return constantInterp;
- }
-
- allOrNothing = !!allOrNothing;
- var startIndex,
- endIndex,
- index = 0,
- expressions = [],
- parseFns = [],
- textLength = text.length,
- exp,
- concat = [],
- expressionPositions = [];
-
- while (index < textLength) {
- if (((startIndex = text.indexOf(startSymbol, index)) != -1) &&
- ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) {
- if (index !== startIndex) {
- concat.push(unescapeText(text.substring(index, startIndex)));
- }
- exp = text.substring(startIndex + startSymbolLength, endIndex);
- expressions.push(exp);
- parseFns.push($parse(exp, parseStringifyInterceptor));
- index = endIndex + endSymbolLength;
- expressionPositions.push(concat.length);
- concat.push('');
- } else {
- // we did not find an interpolation, so we have to add the remainder to the separators array
- if (index !== textLength) {
- concat.push(unescapeText(text.substring(index)));
- }
- break;
- }
- }
-
- // Concatenating expressions makes it hard to reason about whether some combination of
- // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a
- // single expression be used for iframe[src], object[src], etc., we ensure that the value
- // that's used is assigned or constructed by some JS code somewhere that is more testable or
- // make it obvious that you bound the value to some user controlled value. This helps reduce
- // the load when auditing for XSS issues.
- if (trustedContext && concat.length > 1) {
- $interpolateMinErr.throwNoconcat(text);
- }
-
- if (!mustHaveExpression || expressions.length) {
- var compute = function(values) {
- for (var i = 0, ii = expressions.length; i < ii; i++) {
- if (allOrNothing && isUndefined(values[i])) return;
- concat[expressionPositions[i]] = values[i];
- }
- return concat.join('');
- };
-
- var getValue = function(value) {
- return trustedContext ?
- $sce.getTrusted(trustedContext, value) :
- $sce.valueOf(value);
- };
-
- return extend(function interpolationFn(context) {
- var i = 0;
- var ii = expressions.length;
- var values = new Array(ii);
-
- try {
- for (; i < ii; i++) {
- values[i] = parseFns[i](context);
- }
-
- return compute(values);
- } catch (err) {
- $exceptionHandler($interpolateMinErr.interr(text, err));
- }
-
- }, {
- // all of these properties are undocumented for now
- exp: text, //just for compatibility with regular watchers created via $watch
- expressions: expressions,
- $$watchDelegate: function(scope, listener) {
- var lastValue;
- return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
- var currValue = compute(values);
- if (isFunction(listener)) {
- listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
- }
- lastValue = currValue;
- });
- }
- });
- }
-
- function parseStringifyInterceptor(value) {
- try {
- value = getValue(value);
- return allOrNothing && !isDefined(value) ? value : stringify(value);
- } catch (err) {
- $exceptionHandler($interpolateMinErr.interr(text, err));
- }
- }
- }
-
-
- /**
- * @ngdoc method
- * @name $interpolate#startSymbol
- * @description
- * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`.
- *
- * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change
- * the symbol.
- *
- * @returns {string} start symbol.
- */
- $interpolate.startSymbol = function() {
- return startSymbol;
- };
-
-
- /**
- * @ngdoc method
- * @name $interpolate#endSymbol
- * @description
- * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`.
- *
- * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change
- * the symbol.
- *
- * @returns {string} end symbol.
- */
- $interpolate.endSymbol = function() {
- return endSymbol;
- };
-
- return $interpolate;
- }];
-}
-
-function $IntervalProvider() {
- this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
- function($rootScope, $window, $q, $$q, $browser) {
- var intervals = {};
-
-
- /**
- * @ngdoc service
- * @name $interval
- *
- * @description
- * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay`
- * milliseconds.
- *
- * The return value of registering an interval function is a promise. This promise will be
- * notified upon each tick of the interval, and will be resolved after `count` iterations, or
- * run indefinitely if `count` is not defined. The value of the notification will be the
- * number of iterations that have run.
- * To cancel an interval, call `$interval.cancel(promise)`.
- *
- * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
- * move forward by `millis` milliseconds and trigger any functions scheduled to run in that
- * time.
- *
- * <div class="alert alert-warning">
- * **Note**: Intervals created by this service must be explicitly destroyed when you are finished
- * with them. In particular they are not automatically destroyed when a controller's scope or a
- * directive's element are destroyed.
- * You should take this into consideration and make sure to always cancel the interval at the
- * appropriate moment. See the example below for more details on how and when to do this.
- * </div>
- *
- * @param {function()} fn A function that should be called repeatedly.
- * @param {number} delay Number of milliseconds between each function call.
- * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
- * indefinitely.
- * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
- * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @param {...*=} Pass additional parameters to the executed function.
- * @returns {promise} A promise which will be notified on each iteration.
- *
- * @example
- * <example module="intervalExample">
- * <file name="index.html">
- * <script>
- * angular.module('intervalExample', [])
- * .controller('ExampleController', ['$scope', '$interval',
- * function($scope, $interval) {
- * $scope.format = 'M/d/yy h:mm:ss a';
- * $scope.blood_1 = 100;
- * $scope.blood_2 = 120;
- *
- * var stop;
- * $scope.fight = function() {
- * // Don't start a new fight if we are already fighting
- * if ( angular.isDefined(stop) ) return;
- *
- * stop = $interval(function() {
- * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) {
- * $scope.blood_1 = $scope.blood_1 - 3;
- * $scope.blood_2 = $scope.blood_2 - 4;
- * } else {
- * $scope.stopFight();
- * }
- * }, 100);
- * };
- *
- * $scope.stopFight = function() {
- * if (angular.isDefined(stop)) {
- * $interval.cancel(stop);
- * stop = undefined;
- * }
- * };
- *
- * $scope.resetFight = function() {
- * $scope.blood_1 = 100;
- * $scope.blood_2 = 120;
- * };
- *
- * $scope.$on('$destroy', function() {
- * // Make sure that the interval is destroyed too
- * $scope.stopFight();
- * });
- * }])
- * // Register the 'myCurrentTime' directive factory method.
- * // We inject $interval and dateFilter service since the factory method is DI.
- * .directive('myCurrentTime', ['$interval', 'dateFilter',
- * function($interval, dateFilter) {
- * // return the directive link function. (compile function not needed)
- * return function(scope, element, attrs) {
- * var format, // date format
- * stopTime; // so that we can cancel the time updates
- *
- * // used to update the UI
- * function updateTime() {
- * element.text(dateFilter(new Date(), format));
- * }
- *
- * // watch the expression, and update the UI on change.
- * scope.$watch(attrs.myCurrentTime, function(value) {
- * format = value;
- * updateTime();
- * });
- *
- * stopTime = $interval(updateTime, 1000);
- *
- * // listen on DOM destroy (removal) event, and cancel the next UI update
- * // to prevent updating time after the DOM element was removed.
- * element.on('$destroy', function() {
- * $interval.cancel(stopTime);
- * });
- * }
- * }]);
- * </script>
- *
- * <div>
- * <div ng-controller="ExampleController">
- * <label>Date format: <input ng-model="format"></label> <hr/>
- * Current time is: <span my-current-time="format"></span>
- * <hr/>
- * Blood 1 : <font color='red'>{{blood_1}}</font>
- * Blood 2 : <font color='red'>{{blood_2}}</font>
- * <button type="button" data-ng-click="fight()">Fight</button>
- * <button type="button" data-ng-click="stopFight()">StopFight</button>
- * <button type="button" data-ng-click="resetFight()">resetFight</button>
- * </div>
- * </div>
- *
- * </file>
- * </example>
- */
- function interval(fn, delay, count, invokeApply) {
- var hasParams = arguments.length > 4,
- args = hasParams ? sliceArgs(arguments, 4) : [],
- setInterval = $window.setInterval,
- clearInterval = $window.clearInterval,
- iteration = 0,
- skipApply = (isDefined(invokeApply) && !invokeApply),
- deferred = (skipApply ? $$q : $q).defer(),
- promise = deferred.promise;
-
- count = isDefined(count) ? count : 0;
-
- promise.$$intervalId = setInterval(function tick() {
- if (skipApply) {
- $browser.defer(callback);
- } else {
- $rootScope.$evalAsync(callback);
- }
- deferred.notify(iteration++);
-
- if (count > 0 && iteration >= count) {
- deferred.resolve(iteration);
- clearInterval(promise.$$intervalId);
- delete intervals[promise.$$intervalId];
- }
-
- if (!skipApply) $rootScope.$apply();
-
- }, delay);
-
- intervals[promise.$$intervalId] = deferred;
-
- return promise;
-
- function callback() {
- if (!hasParams) {
- fn(iteration);
- } else {
- fn.apply(null, args);
- }
- }
- }
-
-
- /**
- * @ngdoc method
- * @name $interval#cancel
- *
- * @description
- * Cancels a task associated with the `promise`.
- *
- * @param {Promise=} promise returned by the `$interval` function.
- * @returns {boolean} Returns `true` if the task was successfully canceled.
- */
- interval.cancel = function(promise) {
- if (promise && promise.$$intervalId in intervals) {
- intervals[promise.$$intervalId].reject('canceled');
- $window.clearInterval(promise.$$intervalId);
- delete intervals[promise.$$intervalId];
- return true;
- }
- return false;
- };
-
- return interval;
- }];
-}
-
-/**
- * @ngdoc service
- * @name $jsonpCallbacks
- * @requires $window
- * @description
- * This service handles the lifecycle of callbacks to handle JSONP requests.
- * Override this service if you wish to customise where the callbacks are stored and
- * how they vary compared to the requested url.
- */
-var $jsonpCallbacksProvider = function() {
- this.$get = ['$window', function($window) {
- var callbacks = $window.angular.callbacks;
- var callbackMap = {};
-
- function createCallback(callbackId) {
- var callback = function(data) {
- callback.data = data;
- callback.called = true;
- };
- callback.id = callbackId;
- return callback;
- }
-
- return {
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#createCallback
- * @param {string} url the url of the JSONP request
- * @returns {string} the callback path to send to the server as part of the JSONP request
- * @description
- * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback
- * to pass to the server, which will be used to call the callback with its payload in the JSONP response.
- */
- createCallback: function(url) {
- var callbackId = '_' + (callbacks.$$counter++).toString(36);
- var callbackPath = 'angular.callbacks.' + callbackId;
- var callback = createCallback(callbackId);
- callbackMap[callbackPath] = callbacks[callbackId] = callback;
- return callbackPath;
- },
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#wasCalled
- * @param {string} callbackPath the path to the callback that was sent in the JSONP request
- * @returns {boolean} whether the callback has been called, as a result of the JSONP response
- * @description
- * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the
- * callback that was passed in the request.
- */
- wasCalled: function(callbackPath) {
- return callbackMap[callbackPath].called;
- },
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#getResponse
- * @param {string} callbackPath the path to the callback that was sent in the JSONP request
- * @returns {*} the data received from the response via the registered callback
- * @description
- * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback
- * in the JSONP response.
- */
- getResponse: function(callbackPath) {
- return callbackMap[callbackPath].data;
- },
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#removeCallback
- * @param {string} callbackPath the path to the callback that was sent in the JSONP request
- * @description
- * {@link $httpBackend} calls this method to remove the callback after the JSONP request has
- * completed or timed-out.
- */
- removeCallback: function(callbackPath) {
- var callback = callbackMap[callbackPath];
- delete callbacks[callback.id];
- delete callbackMap[callbackPath];
- }
- };
- }];
-};
-
-/**
- * @ngdoc service
- * @name $locale
- *
- * @description
- * $locale service provides localization rules for various Angular components. As of right now the
- * only public api is:
- *
- * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
- */
-
-var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/,
+var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
var $locationMinErr = minErr('$location');
@@ -22857,12 +17455,36 @@ function encodePath(path) {
i = segments.length;
while (i--) {
- segments[i] = encodeUriSegment(segments[i]);
+ // decode forward slashes to prevent them from being double encoded
+ segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/'));
+ }
+
+ return segments.join('/');
+}
+
+function decodePath(path, html5Mode) {
+ var segments = path.split('/'),
+ i = segments.length;
+
+ while (i--) {
+ segments[i] = decodeURIComponent(segments[i]);
+ if (html5Mode) {
+ // encode forward slashes to prevent them from being mistaken for path separators
+ segments[i] = segments[i].replace(/\//g, '%2F');
+ }
}
return segments.join('/');
}
+function normalizePath(pathValue, searchValue, hashValue) {
+ var search = toKeyValue(searchValue),
+ hash = hashValue ? '#' + encodeUriSegment(hashValue) : '',
+ path = encodePath(pathValue);
+
+ return path + (search ? '?' + search : '') + hash;
+}
+
function parseAbsoluteUrl(absoluteUrl, locationObj) {
var parsedUrl = urlResolve(absoluteUrl);
@@ -22871,26 +17493,31 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) {
locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
}
+var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/;
+function parseAppUrl(url, locationObj, html5Mode) {
+
+ if (DOUBLE_SLASH_REGEX.test(url)) {
+ throw $locationMinErr('badpath', 'Invalid url "{0}".', url);
+ }
-function parseAppUrl(relativeUrl, locationObj) {
- var prefixed = (relativeUrl.charAt(0) !== '/');
+ var prefixed = (url.charAt(0) !== '/');
if (prefixed) {
- relativeUrl = '/' + relativeUrl;
+ url = '/' + url;
}
- var match = urlResolve(relativeUrl);
- locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ?
- match.pathname.substring(1) : match.pathname);
+ var match = urlResolve(url);
+ var path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname;
+ locationObj.$$path = decodePath(path, html5Mode);
locationObj.$$search = parseKeyValue(match.search);
locationObj.$$hash = decodeURIComponent(match.hash);
// make sure path starts with '/';
- if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') {
+ if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') {
locationObj.$$path = '/' + locationObj.$$path;
}
}
-function startsWith(haystack, needle) {
- return haystack.lastIndexOf(needle, 0) === 0;
+function startsWith(str, search) {
+ return str.slice(0, search.length) === search;
}
/**
@@ -22906,17 +17533,11 @@ function stripBaseUrl(base, url) {
}
}
-
function stripHash(url) {
var index = url.indexOf('#');
- return index == -1 ? url : url.substr(0, index);
-}
-
-function trimEmptyHash(url) {
- return url.replace(/(#.+)|#$/, '$1');
+ return index === -1 ? url : url.substr(0, index);
}
-
function stripFile(url) {
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
}
@@ -22928,13 +17549,13 @@ function serverBase(url) {
/**
- * LocationHtml5Url represents an url
+ * LocationHtml5Url represents a URL
* This object is exposed as $location service when HTML5 mode is enabled and supported
*
* @constructor
* @param {string} appBase application base URL
* @param {string} appBaseNoFile application base URL stripped of any filename
- * @param {string} basePrefix url path prefix
+ * @param {string} basePrefix URL path prefix
*/
function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$html5 = true;
@@ -22943,8 +17564,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
- * Parse given html5 (regular) url string into properties
- * @param {string} url HTML5 url
+ * Parse given HTML5 (regular) URL string into properties
+ * @param {string} url HTML5 URL
* @private
*/
this.$$parse = function(url) {
@@ -22954,7 +17575,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
appBaseNoFile);
}
- parseAppUrl(pathUrl, this);
+ parseAppUrl(pathUrl, this, true);
if (!this.$$path) {
this.$$path = '/';
@@ -22963,16 +17584,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$compose();
};
- /**
- * Compose url and update `absUrl` property
- * @private
- */
- this.$$compose = function() {
- var search = toKeyValue(this.$$search),
- hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
-
- this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
- this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
+ this.$$normalizeUrl = function(url) {
+ return appBaseNoFile + url.substr(1); // first char is always '/'
};
this.$$parseLinkUrl = function(url, relHref) {
@@ -22985,16 +17598,17 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
var appUrl, prevAppUrl;
var rewrittenUrl;
+
if (isDefined(appUrl = stripBaseUrl(appBase, url))) {
prevAppUrl = appUrl;
- if (isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) {
+ if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) {
rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl);
} else {
rewrittenUrl = appBase + prevAppUrl;
}
} else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBaseNoFile + appUrl;
- } else if (appBaseNoFile == url + '/') {
+ } else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
}
if (rewrittenUrl) {
@@ -23006,7 +17620,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
- * LocationHashbangUrl represents url
+ * LocationHashbangUrl represents URL
* This object is exposed as $location service when developer doesn't opt into html5 mode.
* It also serves as the base class for html5 mode fallback on legacy browsers.
*
@@ -23021,8 +17635,8 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
- * Parse given hashbang url into properties
- * @param {string} url Hashbang url
+ * Parse given hashbang URL into properties
+ * @param {string} url Hashbang URL
* @private
*/
this.$$parse = function(url) {
@@ -23031,7 +17645,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') {
- // The rest of the url starts with a hash so we have
+ // The rest of the URL starts with a hash so we have
// got either a hashbang path or a plain hash fragment
withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl);
if (isUndefined(withoutHashUrl)) {
@@ -23049,12 +17663,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
withoutHashUrl = '';
if (isUndefined(withoutBaseUrl)) {
appBase = url;
- this.replace();
+ /** @type {?} */ (this).replace();
}
}
}
- parseAppUrl(withoutHashUrl, this);
+ parseAppUrl(withoutHashUrl, this, false);
this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase);
@@ -23068,7 +17682,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
* * a.setAttribute('href', '/foo')
* * a.pathname === '/C:/foo' //true
*
- * Inside of Angular, we're always using pathnames that
+ * Inside of AngularJS, we're always using pathnames that
* do not include drive names for routing.
*/
function removeWindowsDriveName(path, url, base) {
@@ -23095,20 +17709,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
}
};
- /**
- * Compose hashbang url and update `absUrl` property
- * @private
- */
- this.$$compose = function() {
- var search = toKeyValue(this.$$search),
- hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
-
- this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
- this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
+ this.$$normalizeUrl = function(url) {
+ return appBase + (url ? hashPrefix + url : '');
};
this.$$parseLinkUrl = function(url, relHref) {
- if (stripHash(appBase) == stripHash(url)) {
+ if (stripHash(appBase) === stripHash(url)) {
this.$$parse(url);
return true;
}
@@ -23118,7 +17724,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
- * LocationHashbangUrl represents url
+ * LocationHashbangUrl represents URL
* This object is exposed as $location service when html5 history api is enabled but the browser
* does not support it.
*
@@ -23142,7 +17748,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
var rewrittenUrl;
var appUrl;
- if (appBase == stripHash(url)) {
+ if (appBase === stripHash(url)) {
rewrittenUrl = url;
} else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBase + hashPrefix + appUrl;
@@ -23155,22 +17761,17 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
return !!rewrittenUrl;
};
- this.$$compose = function() {
- var search = toKeyValue(this.$$search),
- hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
-
- this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
+ this.$$normalizeUrl = function(url) {
// include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#'
- this.$$absUrl = appBase + hashPrefix + this.$$url;
+ return appBase + hashPrefix + url;
};
-
}
var locationPrototype = {
/**
- * Ensure absolute url is initialized.
+ * Ensure absolute URL is initialized.
* @private
*/
$$absUrl:'',
@@ -23188,23 +17789,33 @@ var locationPrototype = {
$$replace: false,
/**
+ * Compose url and update `url` and `absUrl` property
+ * @private
+ */
+ $$compose: function() {
+ this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash);
+ this.$$absUrl = this.$$normalizeUrl(this.$$url);
+ this.$$urlUpdatedByLocation = true;
+ },
+
+ /**
* @ngdoc method
* @name $location#absUrl
*
* @description
* This method is getter only.
*
- * Return full url representation with all segments encoded according to rules specified in
+ * Return full URL representation with all segments encoded according to rules specified in
* [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var absUrl = $location.absUrl();
* // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
* ```
*
- * @return {string} full url
+ * @return {string} full URL
*/
absUrl: locationGetter('$$absUrl'),
@@ -23215,18 +17826,18 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return url (e.g. `/path?a=b#hash`) when called without any parameter.
+ * Return URL (e.g. `/path?a=b#hash`) when called without any parameter.
*
* Change path, search and hash, when called with parameter and return `$location`.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var url = $location.url();
* // => "/some/path?foo=bar&baz=xoxo"
* ```
*
- * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
+ * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`)
* @return {string} url
*/
url: function(url) {
@@ -23249,16 +17860,16 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return protocol of current url.
+ * Return protocol of current URL.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var protocol = $location.protocol();
* // => "http"
* ```
*
- * @return {string} protocol of current url
+ * @return {string} protocol of current URL
*/
protocol: locationGetter('$$protocol'),
@@ -23269,24 +17880,24 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return host of current url.
+ * Return host of current URL.
*
- * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
+ * Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var host = $location.host();
* // => "example.com"
*
- * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
* host = $location.host();
* // => "example.com"
* host = location.host;
* // => "example.com:8080"
* ```
*
- * @return {string} host of current url.
+ * @return {string} host of current URL.
*/
host: locationGetter('$$host'),
@@ -23297,11 +17908,11 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return port of current url.
+ * Return port of current URL.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var port = $location.port();
* // => 80
* ```
@@ -23317,7 +17928,7 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return path of current url when called without any parameter.
+ * Return path of current URL when called without any parameter.
*
* Change path when called with parameter and return `$location`.
*
@@ -23326,7 +17937,7 @@ var locationPrototype = {
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var path = $location.path();
* // => "/some/path"
* ```
@@ -23336,7 +17947,7 @@ var locationPrototype = {
*/
path: locationGetterSetter('$$path', function(path) {
path = path !== null ? path.toString() : '';
- return path.charAt(0) == '/' ? path : '/' + path;
+ return path.charAt(0) === '/' ? path : '/' + path;
}),
/**
@@ -23346,13 +17957,13 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return search part (as object) of current url when called without any parameter.
+ * Return search part (as object) of current URL when called without any parameter.
*
* Change search part when called with parameter and return `$location`.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var searchObject = $location.search();
* // => {foo: 'bar', baz: 'xoxo'}
*
@@ -23368,7 +17979,7 @@ var locationPrototype = {
* of `$location` to the specified value.
*
* If the argument is a hash object containing an array of values, these values will be encoded
- * as duplicate search parameters in the url.
+ * as duplicate search parameters in the URL.
*
* @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue`
* will override only a single search property.
@@ -23430,7 +18041,7 @@ var locationPrototype = {
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
* var hash = $location.hash();
* // => "hashValue"
* ```
@@ -23491,6 +18102,7 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun
// 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;
+ this.$$urlUpdatedByLocation = true;
return this;
};
@@ -23498,14 +18110,14 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun
function locationGetter(property) {
- return function() {
+ return /** @this */ function() {
return this[property];
};
}
function locationGetterSetter(property, preprocess) {
- return function(value) {
+ return /** @this */ function(value) {
if (isUndefined(value)) {
return this[property];
}
@@ -23547,11 +18159,13 @@ function locationGetterSetter(property, preprocess) {
/**
* @ngdoc provider
* @name $locationProvider
+ * @this
+ *
* @description
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
*/
function $LocationProvider() {
- var hashPrefix = '',
+ var hashPrefix = '!',
html5Mode = {
enabled: false,
requireBase: true,
@@ -23562,6 +18176,7 @@ function $LocationProvider() {
* @ngdoc method
* @name $locationProvider#hashPrefix
* @description
+ * The default value for the prefix is `'!'`.
* @param {string=} prefix Prefix for hash part (containing path and search)
* @returns {*} current value if used as getter or itself (chaining) if used as setter
*/
@@ -23588,8 +18203,12 @@ function $LocationProvider() {
* 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: `true`) When html5Mode is enabled,
- * enables/disables url rewriting for relative links.
+ * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled,
+ * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will
+ * only happen on links with an attribute that matches the given string. For example, if set
+ * to `'internal-link'`, then the URL will only be rewritten for `<a internal-link>` links.
+ * Note that [attribute name normalization](guide/directive#normalization) does not apply
+ * here, so `'internalLink'` will **not** match `'internal-link'`.
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
@@ -23607,7 +18226,7 @@ function $LocationProvider() {
html5Mode.requireBase = mode.requireBase;
}
- if (isBoolean(mode.rewriteLinks)) {
+ if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) {
html5Mode.rewriteLinks = mode.rewriteLinks;
}
@@ -23667,7 +18286,7 @@ function $LocationProvider() {
if (html5Mode.enabled) {
if (!baseHref && html5Mode.requireBase) {
throw $locationMinErr('nobase',
- "$location in HTML5 mode requires a <base> tag to be present!");
+ '$location in HTML5 mode requires a <base> tag to be present!');
}
appBase = serverBase(initialUrl) + (baseHref || '/');
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
@@ -23684,6 +18303,13 @@ function $LocationProvider() {
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
+ // Determine if two URLs are equal despite potentially having different encoding/normalizing
+ // such as $location.absUrl() vs $browser.url()
+ // See https://github.com/angular/angular.js/issues/16592
+ function urlsEqual(a, b) {
+ return a === b || urlResolve(a).href === urlResolve(b).href;
+ }
+
function setBrowserUrlWithFallback(url, replace, state) {
var oldUrl = $location.url();
var oldState = $location.$$state;
@@ -23704,10 +18330,11 @@ function $LocationProvider() {
}
$rootElement.on('click', function(event) {
+ var rewriteLinks = html5Mode.rewriteLinks;
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
- if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return;
+ if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return;
var elm = jqLite(event.target);
@@ -23717,6 +18344,8 @@ function $LocationProvider() {
if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
}
+ if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return;
+
var absHref = elm.prop('href');
// get the actual href attribute - see
// http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
@@ -23733,15 +18362,13 @@ function $LocationProvider() {
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,
+ // We do a preventDefault for all urls that are part of the AngularJS 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()) {
+ if ($location.absUrl() !== $browser.url()) {
$rootScope.$apply();
- // hack to work around FF6 bug 684208 when scenario runner clicks on links
- $window.angular['ff-684208-preventDefault'] = true;
}
}
}
@@ -23749,7 +18376,7 @@ function $LocationProvider() {
// rewrite hashbang url <> html5 url
- if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) {
+ if ($location.absUrl() !== initialUrl) {
$browser.url($location.absUrl(), true);
}
@@ -23758,7 +18385,7 @@ function $LocationProvider() {
// update $location when $browser url changes
$browser.onUrlChange(function(newUrl, newState) {
- if (isUndefined(stripBaseUrl(appBaseNoFile, newUrl))) {
+ if (!startsWith(newUrl, appBaseNoFile)) {
// If we are navigating outside of the app then force a reload
$window.location.href = newUrl;
return;
@@ -23768,7 +18395,6 @@ function $LocationProvider() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
- newUrl = trimEmptyHash(newUrl);
$location.$$parse(newUrl);
$location.$$state = newState;
@@ -23793,36 +18419,40 @@ function $LocationProvider() {
// update browser
$rootScope.$watch(function $locationWatch() {
- var oldUrl = trimEmptyHash($browser.url());
- var newUrl = trimEmptyHash($location.absUrl());
- var oldState = $browser.state();
- var currentReplace = $location.$$replace;
- var urlOrStateChanged = oldUrl !== newUrl ||
- ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
-
- if (initializing || urlOrStateChanged) {
- initializing = false;
-
- $rootScope.$evalAsync(function() {
- var newUrl = $location.absUrl();
- var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
- $location.$$state, oldState).defaultPrevented;
-
- // if the location was changed by a `$locationChangeStart` handler then stop
- // processing this location change
- if ($location.absUrl() !== newUrl) return;
-
- if (defaultPrevented) {
- $location.$$parse(oldUrl);
- $location.$$state = oldState;
- } else {
- if (urlOrStateChanged) {
- setBrowserUrlWithFallback(newUrl, currentReplace,
- oldState === $location.$$state ? null : $location.$$state);
+ if (initializing || $location.$$urlUpdatedByLocation) {
+ $location.$$urlUpdatedByLocation = false;
+
+ var oldUrl = $browser.url();
+ var newUrl = $location.absUrl();
+ var oldState = $browser.state();
+ var currentReplace = $location.$$replace;
+ var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) ||
+ ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
+
+ if (initializing || urlOrStateChanged) {
+ initializing = false;
+
+ $rootScope.$evalAsync(function() {
+ var newUrl = $location.absUrl();
+ var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
+ $location.$$state, oldState).defaultPrevented;
+
+ // if the location was changed by a `$locationChangeStart` handler then stop
+ // processing this location change
+ if ($location.absUrl() !== newUrl) return;
+
+ if (defaultPrevented) {
+ $location.$$parse(oldUrl);
+ $location.$$state = oldState;
+ } else {
+ if (urlOrStateChanged) {
+ setBrowserUrlWithFallback(newUrl, currentReplace,
+ oldState === $location.$$state ? null : $location.$$state);
+ }
+ afterLocationChange(oldUrl, oldState);
}
- afterLocationChange(oldUrl, oldState);
- }
- });
+ });
+ }
}
$location.$$replace = false;
@@ -23851,11 +18481,19 @@ function $LocationProvider() {
*
* The main purpose of this service is to simplify debugging and troubleshooting.
*
+ * To reveal the location of the calls to `$log` in the JavaScript console,
+ * you can "blackbox" the AngularJS source in your browser:
+ *
+ * [Mozilla description of blackboxing](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Black_box_a_source).
+ * [Chrome description of blackboxing](https://developer.chrome.com/devtools/docs/blackboxing).
+ *
+ * Note: Not all browsers support blackboxing.
+ *
* The default is to log `debug` messages. You can use
* {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this.
*
* @example
- <example module="logExample">
+ <example module="logExample" name="log-service">
<file name="script.js">
angular.module('logExample', [])
.controller('LogController', ['$scope', '$log', function($scope, $log) {
@@ -23881,6 +18519,8 @@ function $LocationProvider() {
/**
* @ngdoc provider
* @name $logProvider
+ * @this
+ *
* @description
* Use the `$logProvider` to configure how the application logs messages
*/
@@ -23898,13 +18538,22 @@ function $LogProvider() {
this.debugEnabled = function(flag) {
if (isDefined(flag)) {
debug = flag;
- return this;
+ return this;
} else {
return debug;
}
};
this.$get = ['$window', function($window) {
+ // Support: IE 9-11, Edge 12-14+
+ // IE/Edge display errors in such a way that it requires the user to click in 4 places
+ // to see the stack trace. There is no way to feature-detect it so there's a chance
+ // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't
+ // break apps. Other browsers display errors in a sensible way and some of them map stack
+ // traces along source maps if available so it makes sense to let browsers display it
+ // as they want.
+ var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent);
+
return {
/**
* @ngdoc method
@@ -23957,12 +18606,12 @@ function $LogProvider() {
fn.apply(self, arguments);
}
};
- }())
+ })()
};
function formatError(arg) {
- if (arg instanceof Error) {
- if (arg.stack) {
+ if (isError(arg)) {
+ if (arg.stack && formatStackTrace) {
arg = (arg.message && arg.stack.indexOf(arg.message) === -1)
? 'Error: ' + arg.message + '\n' + arg.stack
: arg.stack;
@@ -23975,29 +18624,17 @@ function $LogProvider() {
function consoleLog(type) {
var console = $window.console || {},
- logFn = console[type] || console.log || noop,
- hasApply = false;
-
- // Note: reading logFn.apply throws an error in IE11 in IE8 document mode.
- // The reason behind this is that console.log has type "object" in IE8...
- try {
- hasApply = !!logFn.apply;
- } catch (e) {}
-
- if (hasApply) {
- return function() {
- var args = [];
- forEach(arguments, function(arg) {
- args.push(formatError(arg));
- });
- return logFn.apply(console, args);
- };
- }
+ logFn = console[type] || console.log || noop;
- // we are IE which either doesn't have window.console => this is noop and we do nothing,
- // or we are IE where console.log doesn't have apply so we log at least first 2 args
- return function(arg1, arg2) {
- logFn(arg1, arg2 == null ? '' : arg2);
+ return function() {
+ var args = [];
+ forEach(arguments, function(arg) {
+ args.push(formatError(arg));
+ });
+ // Support: IE 9 only
+ // console methods don't inherit from Function.prototype in IE 9 so we can't
+ // call `logFn.apply(console, args)` directly.
+ return Function.prototype.apply.call(logFn, console, args);
};
}
}];
@@ -24016,41 +18653,23 @@ function $LogProvider() {
var $parseMinErr = minErr('$parse');
-// Sandboxing Angular Expressions
+var objectValueOf = {}.constructor.prototype.valueOf;
+
+// Sandboxing AngularJS Expressions
// ------------------------------
-// Angular expressions are generally considered safe because these expressions only have direct
-// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by
-// obtaining a reference to native JS functions such as the Function constructor.
+// AngularJS expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by
+// various means such as obtaining a reference to native JS functions like the Function constructor.
//
-// As an example, consider the following Angular expression:
+// As an example, consider the following AngularJS expression:
//
// {}.toString.constructor('alert("evil JS code")')
//
-// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
-// against the expression language, but not to prevent exploits that were enabled by exposing
-// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good
-// practice and therefore we are not even trying to protect against interaction with an object
-// explicitly exposed in this way.
-//
-// In general, it is not possible to access a Window object from an angular expression unless a
-// window or some DOM object that has a reference to window is published onto a Scope.
-// Similarly we prevent invocations of function known to be dangerous, as well as assignments to
-// native objects.
+// It is important to realize that if you create an expression from a string that contains user provided
+// content then it is possible that your application contains a security vulnerability to an XSS style attack.
//
// See https://docs.angularjs.org/guide/security
-function ensureSafeMemberName(name, fullExpression) {
- if (name === "__defineGetter__" || name === "__defineSetter__"
- || name === "__lookupGetter__" || name === "__lookupSetter__"
- || name === "__proto__") {
- throw $parseMinErr('isecfld',
- 'Attempting to access a disallowed field in Angular expressions! '
- + 'Expression: {0}', fullExpression);
- }
- return name;
-}
-
function getStringValue(name) {
// Property names must be strings. This means that non-string objects cannot be used
// as keys in an object. Any non-string object, including a number, is typecasted
@@ -24069,64 +18688,10 @@ function getStringValue(name) {
return name + '';
}
-function ensureSafeObject(obj, fullExpression) {
- // nifty check if obj is Function that is fast and works across iframes and other contexts
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isWindow(obj)
- obj.window === obj) {
- throw $parseMinErr('isecwindow',
- 'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isElement(obj)
- obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
- throw $parseMinErr('isecdom',
- 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// block Object so that we can't get hold of dangerous Object.* methods
- obj === Object) {
- throw $parseMinErr('isecobj',
- 'Referencing Object in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
- return obj;
-}
-
-var CALL = Function.prototype.call;
-var APPLY = Function.prototype.apply;
-var BIND = Function.prototype.bind;
-
-function ensureSafeFunction(obj, fullExpression) {
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (obj === CALL || obj === APPLY || obj === BIND) {
- throw $parseMinErr('isecff',
- 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
-}
-
-function ensureSafeAssignContext(obj, fullExpression) {
- if (obj) {
- if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor ||
- obj === {}.constructor || obj === [].constructor || obj === Function.constructor) {
- throw $parseMinErr('isecaf',
- 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression);
- }
- }
-}
var OPERATORS = createMap();
forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
-var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
+var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'};
/////////////////////////////////////////
@@ -24135,7 +18700,7 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'
/**
* @constructor
*/
-var Lexer = function(options) {
+var Lexer = function Lexer(options) {
this.options = options;
};
@@ -24149,7 +18714,7 @@ Lexer.prototype = {
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
- if (ch === '"' || ch === "'") {
+ if (ch === '"' || ch === '\'') {
this.readString(ch);
} else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
this.readNumber();
@@ -24188,7 +18753,7 @@ Lexer.prototype = {
},
isNumber: function(ch) {
- return ('0' <= ch && ch <= '9') && typeof ch === "string";
+ return ('0' <= ch && ch <= '9') && typeof ch === 'string';
},
isWhitespace: function(ch) {
@@ -24221,9 +18786,8 @@ Lexer.prototype = {
codePointAt: function(ch) {
if (ch.length === 1) return ch.charCodeAt(0);
- /*jshint bitwise: false*/
+ // eslint-disable-next-line no-bitwise
return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
- /*jshint bitwise: true*/
},
peekMultichar: function() {
@@ -24258,19 +18822,19 @@ Lexer.prototype = {
var start = this.index;
while (this.index < this.text.length) {
var ch = lowercase(this.text.charAt(this.index));
- if (ch == '.' || this.isNumber(ch)) {
+ if (ch === '.' || this.isNumber(ch)) {
number += ch;
} else {
var peekCh = this.peek();
- if (ch == 'e' && this.isExpOperator(peekCh)) {
+ if (ch === 'e' && this.isExpOperator(peekCh)) {
number += ch;
} else if (this.isExpOperator(ch) &&
peekCh && this.isNumber(peekCh) &&
- number.charAt(number.length - 1) == 'e') {
+ number.charAt(number.length - 1) === 'e') {
number += ch;
} else if (this.isExpOperator(ch) &&
(!peekCh || !this.isNumber(peekCh)) &&
- number.charAt(number.length - 1) == 'e') {
+ number.charAt(number.length - 1) === 'e') {
this.throwError('Invalid exponent');
} else {
break;
@@ -24345,7 +18909,7 @@ Lexer.prototype = {
}
};
-var AST = function(lexer, options) {
+var AST = function AST(lexer, options) {
this.lexer = lexer;
this.options = options;
};
@@ -24401,8 +18965,7 @@ AST.prototype = {
filterChain: function() {
var left = this.expression();
- var token;
- while ((token = this.expect('|'))) {
+ while (this.expect('|')) {
left = this.filter(left);
}
return left;
@@ -24415,6 +18978,10 @@ AST.prototype = {
assignment: function() {
var result = this.ternary();
if (this.expect('=')) {
+ if (!isAssignable(result)) {
+ throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
+ }
+
result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='};
}
return result;
@@ -24614,7 +19181,7 @@ AST.prototype = {
this.consume(':');
property.value = this.expression();
} else {
- this.throwError("invalid key", this.peek());
+ this.throwError('invalid key', this.peek());
}
properties.push(property);
} while (this.expect(','));
@@ -24695,14 +19262,47 @@ function isStateless($filter, filterName) {
return !fn.$stateful;
}
-function findConstantAndWatchExpressions(ast, $filter) {
+var PURITY_ABSOLUTE = 1;
+var PURITY_RELATIVE = 2;
+
+// Detect nodes which could depend on non-shallow state of objects
+function isPure(node, parentIsPure) {
+ switch (node.type) {
+ // Computed members might invoke a stateful toString()
+ case AST.MemberExpression:
+ if (node.computed) {
+ return false;
+ }
+ break;
+
+ // Unary always convert to primative
+ case AST.UnaryExpression:
+ return PURITY_ABSOLUTE;
+
+ // The binary + operator can invoke a stateful toString().
+ case AST.BinaryExpression:
+ return node.operator !== '+' ? PURITY_ABSOLUTE : false;
+
+ // Functions / filters probably read state from within objects
+ case AST.CallExpression:
+ return false;
+ }
+
+ return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure;
+}
+
+function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
var allConstants;
var argsToWatch;
+ var isStatelessFilter;
+
+ var astIsPure = ast.isPure = isPure(ast, parentIsPure);
+
switch (ast.type) {
case AST.Program:
allConstants = true;
forEach(ast.body, function(expr) {
- findConstantAndWatchExpressions(expr.expression, $filter);
+ findConstantAndWatchExpressions(expr.expression, $filter, astIsPure);
allConstants = allConstants && expr.expression.constant;
});
ast.constant = allConstants;
@@ -24712,26 +19312,26 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = [];
break;
case AST.UnaryExpression:
- findConstantAndWatchExpressions(ast.argument, $filter);
+ findConstantAndWatchExpressions(ast.argument, $filter, astIsPure);
ast.constant = ast.argument.constant;
ast.toWatch = ast.argument.toWatch;
break;
case AST.BinaryExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch);
break;
case AST.LogicalExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.ConditionalExpression:
- findConstantAndWatchExpressions(ast.test, $filter);
- findConstantAndWatchExpressions(ast.alternate, $filter);
- findConstantAndWatchExpressions(ast.consequent, $filter);
+ findConstantAndWatchExpressions(ast.test, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure);
ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
@@ -24740,29 +19340,28 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = [ast];
break;
case AST.MemberExpression:
- findConstantAndWatchExpressions(ast.object, $filter);
+ findConstantAndWatchExpressions(ast.object, $filter, astIsPure);
if (ast.computed) {
- findConstantAndWatchExpressions(ast.property, $filter);
+ findConstantAndWatchExpressions(ast.property, $filter, astIsPure);
}
ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
- ast.toWatch = [ast];
+ ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.CallExpression:
- allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false;
+ isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false;
+ allConstants = isStatelessFilter;
argsToWatch = [];
forEach(ast.arguments, function(expr) {
- findConstantAndWatchExpressions(expr, $filter);
+ findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
- if (!expr.constant) {
- argsToWatch.push.apply(argsToWatch, expr.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
- ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast];
+ ast.toWatch = isStatelessFilter ? argsToWatch : [ast];
break;
case AST.AssignmentExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = [ast];
break;
@@ -24770,11 +19369,9 @@ function findConstantAndWatchExpressions(ast, $filter) {
allConstants = true;
argsToWatch = [];
forEach(ast.elements, function(expr) {
- findConstantAndWatchExpressions(expr, $filter);
+ findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
- if (!expr.constant) {
- argsToWatch.push.apply(argsToWatch, expr.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
ast.toWatch = argsToWatch;
@@ -24783,10 +19380,14 @@ function findConstantAndWatchExpressions(ast, $filter) {
allConstants = true;
argsToWatch = [];
forEach(ast.properties, function(property) {
- findConstantAndWatchExpressions(property.value, $filter);
- allConstants = allConstants && property.value.constant && !property.computed;
- if (!property.value.constant) {
- argsToWatch.push.apply(argsToWatch, property.value.toWatch);
+ findConstantAndWatchExpressions(property.value, $filter, astIsPure);
+ allConstants = allConstants && property.value.constant;
+ argsToWatch.push.apply(argsToWatch, property.value.toWatch);
+ if (property.computed) {
+ //`{[key]: value}` implicitly does `key.toString()` which may be non-pure
+ findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false);
+ allConstants = allConstants && property.key.constant;
+ argsToWatch.push.apply(argsToWatch, property.key.toWatch);
}
});
ast.constant = allConstants;
@@ -24804,7 +19405,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
}
function getInputs(body) {
- if (body.length != 1) return;
+ if (body.length !== 1) return;
var lastExpression = body[0].expression;
var candidate = lastExpression.toWatch;
if (candidate.length !== 1) return candidate;
@@ -24833,19 +19434,16 @@ function isConstant(ast) {
return ast.constant;
}
-function ASTCompiler(astBuilder, $filter) {
- this.astBuilder = astBuilder;
+function ASTCompiler($filter) {
this.$filter = $filter;
}
ASTCompiler.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(ast) {
var self = this;
- var ast = this.astBuilder.ast(expression);
this.state = {
nextId: 0,
filters: {},
- expensiveChecks: expensiveChecks,
fn: {vars: [], body: [], own: {}},
assign: {vars: [], body: [], own: {}},
inputs: []
@@ -24870,7 +19468,7 @@ ASTCompiler.prototype = {
var intoId = self.nextId();
self.recurse(watch, intoId);
self.return_(intoId);
- self.state.inputs.push(fnKey);
+ self.state.inputs.push({name: fnKey, isPure: watch.isPure});
watch.watchId = key;
});
this.state.computing = 'fn';
@@ -24886,30 +19484,17 @@ ASTCompiler.prototype = {
this.watchFns() +
'return fn;';
- /* jshint -W054 */
+ // eslint-disable-next-line no-new-func
var fn = (new Function('$filter',
- 'ensureSafeMemberName',
- 'ensureSafeObject',
- 'ensureSafeFunction',
'getStringValue',
- 'ensureSafeAssignContext',
'ifDefined',
'plus',
- 'text',
fnString))(
this.$filter,
- ensureSafeMemberName,
- ensureSafeObject,
- ensureSafeFunction,
getStringValue,
- ensureSafeAssignContext,
ifDefined,
- plusFn,
- expression);
- /* jshint +W054 */
+ plusFn);
this.state = this.stage = undefined;
- fn.literal = isLiteral(ast);
- fn.constant = isConstant(ast);
return fn;
},
@@ -24919,13 +19504,16 @@ ASTCompiler.prototype = {
watchFns: function() {
var result = [];
- var fns = this.state.inputs;
+ var inputs = this.state.inputs;
var self = this;
- forEach(fns, function(name) {
- result.push('var ' + name + '=' + self.generateFunction(name, 's'));
+ forEach(inputs, function(input) {
+ result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's'));
+ if (input.isPure) {
+ result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';');
+ }
});
- if (fns.length) {
- result.push('fn.inputs=[' + fns.join(',') + '];');
+ if (inputs.length) {
+ result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];');
}
return result.join('');
},
@@ -24980,7 +19568,7 @@ ASTCompiler.prototype = {
case AST.Literal:
expression = this.escape(ast.value);
this.assign(intoId, expression);
- recursionFn(expression);
+ recursionFn(intoId || expression);
break;
case AST.UnaryExpression:
this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; });
@@ -25020,22 +19608,18 @@ ASTCompiler.prototype = {
nameId.computed = false;
nameId.name = ast.name;
}
- ensureSafeMemberName(ast.name);
self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)),
function() {
self.if_(self.stage === 'inputs' || 's', function() {
if (create && create !== 1) {
self.if_(
- self.not(self.nonComputedMember('s', ast.name)),
+ self.isNull(self.nonComputedMember('s', ast.name)),
self.lazyAssign(self.nonComputedMember('s', ast.name), '{}'));
}
self.assign(intoId, self.nonComputedMember('s', ast.name));
});
}, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name))
);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) {
- self.addEnsureSafeObject(intoId);
- }
recursionFn(intoId);
break;
case AST.MemberExpression:
@@ -25043,32 +19627,24 @@ ASTCompiler.prototype = {
intoId = intoId || this.nextId();
self.recurse(ast.object, left, undefined, function() {
self.if_(self.notNull(left), function() {
- if (create && create !== 1) {
- self.addEnsureSafeAssignContext(left);
- }
if (ast.computed) {
right = self.nextId();
self.recurse(ast.property, right);
self.getStringValue(right);
- self.addEnsureSafeMemberName(right);
if (create && create !== 1) {
self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
}
- expression = self.ensureSafeObject(self.computedMember(left, right));
+ expression = self.computedMember(left, right);
self.assign(intoId, expression);
if (nameId) {
nameId.computed = true;
nameId.name = right;
}
} else {
- ensureSafeMemberName(ast.property.name);
if (create && create !== 1) {
- self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
+ self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
}
expression = self.nonComputedMember(left, ast.property.name);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) {
- expression = self.ensureSafeObject(expression);
- }
self.assign(intoId, expression);
if (nameId) {
nameId.computed = false;
@@ -25100,21 +19676,16 @@ ASTCompiler.prototype = {
args = [];
self.recurse(ast.callee, right, left, function() {
self.if_(self.notNull(right), function() {
- self.addEnsureSafeFunction(right);
forEach(ast.arguments, function(expr) {
- self.recurse(expr, self.nextId(), undefined, function(argument) {
- args.push(self.ensureSafeObject(argument));
+ self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
+ args.push(argument);
});
});
if (left.name) {
- if (!self.state.expensiveChecks) {
- self.addEnsureSafeObject(left.context);
- }
expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')';
} else {
expression = right + '(' + args.join(',') + ')';
}
- expression = self.ensureSafeObject(expression);
self.assign(intoId, expression);
}, function() {
self.assign(intoId, 'undefined');
@@ -25126,14 +19697,9 @@ ASTCompiler.prototype = {
case AST.AssignmentExpression:
right = this.nextId();
left = {};
- if (!isAssignable(ast.left)) {
- throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
- }
this.recurse(ast.left, undefined, left, function() {
self.if_(self.notNull(left.context), function() {
self.recurse(ast.right, right);
- self.addEnsureSafeObject(self.member(left.context, left.name, left.computed));
- self.addEnsureSafeAssignContext(left.context);
expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
self.assign(intoId, expression);
recursionFn(intoId || expression);
@@ -25143,13 +19709,13 @@ ASTCompiler.prototype = {
case AST.ArrayExpression:
args = [];
forEach(ast.elements, function(expr) {
- self.recurse(expr, self.nextId(), undefined, function(argument) {
+ self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
args.push(argument);
});
});
expression = '[' + args.join(',') + ']';
this.assign(intoId, expression);
- recursionFn(expression);
+ recursionFn(intoId || expression);
break;
case AST.ObjectExpression:
args = [];
@@ -25191,15 +19757,15 @@ ASTCompiler.prototype = {
break;
case AST.ThisExpression:
this.assign(intoId, 's');
- recursionFn('s');
+ recursionFn(intoId || 's');
break;
case AST.LocalsExpression:
this.assign(intoId, 'l');
- recursionFn('l');
+ recursionFn(intoId || 'l');
break;
case AST.NGValueParameter:
this.assign(intoId, 'v');
- recursionFn('v');
+ recursionFn(intoId || 'v');
break;
}
},
@@ -25258,12 +19824,16 @@ ASTCompiler.prototype = {
return '!(' + expression + ')';
},
+ isNull: function(expression) {
+ return expression + '==null';
+ },
+
notNull: function(expression) {
return expression + '!=null';
},
nonComputedMember: function(left, right) {
- var SAFE_IDENTIFIER = /[$_a-zA-Z][$_a-zA-Z0-9]*/;
+ var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g;
if (SAFE_IDENTIFIER.test(right)) {
return left + '.' + right;
@@ -25281,42 +19851,10 @@ ASTCompiler.prototype = {
return this.nonComputedMember(left, right);
},
- addEnsureSafeObject: function(item) {
- this.current().body.push(this.ensureSafeObject(item), ';');
- },
-
- addEnsureSafeMemberName: function(item) {
- this.current().body.push(this.ensureSafeMemberName(item), ';');
- },
-
- addEnsureSafeFunction: function(item) {
- this.current().body.push(this.ensureSafeFunction(item), ';');
- },
-
- addEnsureSafeAssignContext: function(item) {
- this.current().body.push(this.ensureSafeAssignContext(item), ';');
- },
-
- ensureSafeObject: function(item) {
- return 'ensureSafeObject(' + item + ',text)';
- },
-
- ensureSafeMemberName: function(item) {
- return 'ensureSafeMemberName(' + item + ',text)';
- },
-
- ensureSafeFunction: function(item) {
- return 'ensureSafeFunction(' + item + ',text)';
- },
-
getStringValue: function(item) {
this.assign(item, 'getStringValue(' + item + ')');
},
- ensureSafeAssignContext: function(item) {
- return 'ensureSafeAssignContext(' + item + ',text)';
- },
-
lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
var self = this;
return function() {
@@ -25338,7 +19876,7 @@ ASTCompiler.prototype = {
},
escape: function(value) {
- if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'";
+ if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\'';
if (isNumber(value)) return value.toString();
if (value === true) return 'true';
if (value === false) return 'false';
@@ -25362,17 +19900,13 @@ ASTCompiler.prototype = {
};
-function ASTInterpreter(astBuilder, $filter) {
- this.astBuilder = astBuilder;
+function ASTInterpreter($filter) {
this.$filter = $filter;
}
ASTInterpreter.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(ast) {
var self = this;
- var ast = this.astBuilder.ast(expression);
- this.expression = expression;
- this.expensiveChecks = expensiveChecks;
findConstantAndWatchExpressions(ast, self.$filter);
var assignable;
var assign;
@@ -25385,6 +19919,7 @@ ASTInterpreter.prototype = {
inputs = [];
forEach(toWatch, function(watch, key) {
var input = self.recurse(watch);
+ input.isPure = watch.isPure;
watch.input = input;
inputs.push(input);
watch.watchId = key;
@@ -25411,13 +19946,11 @@ ASTInterpreter.prototype = {
if (inputs) {
fn.inputs = inputs;
}
- fn.literal = isLiteral(ast);
- fn.constant = isConstant(ast);
return fn;
},
recurse: function(ast, context, create) {
- var left, right, self = this, args, expression;
+ var left, right, self = this, args;
if (ast.input) {
return this.inputs(ast.input, ast.watchId);
}
@@ -25443,20 +19976,16 @@ ASTInterpreter.prototype = {
context
);
case AST.Identifier:
- ensureSafeMemberName(ast.name, self.expression);
- return self.identifier(ast.name,
- self.expensiveChecks || isPossiblyDangerousMemberName(ast.name),
- context, create, self.expression);
+ return self.identifier(ast.name, context, create);
case AST.MemberExpression:
left = this.recurse(ast.object, false, !!create);
if (!ast.computed) {
- ensureSafeMemberName(ast.property.name, self.expression);
right = ast.property.name;
}
if (ast.computed) right = this.recurse(ast.property);
return ast.computed ?
- this.computedMember(left, right, context, create, self.expression) :
- this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression);
+ this.computedMember(left, right, context, create) :
+ this.nonComputedMember(left, right, context, create);
case AST.CallExpression:
args = [];
forEach(ast.arguments, function(expr) {
@@ -25477,13 +20006,11 @@ ASTInterpreter.prototype = {
var rhs = right(scope, locals, assign, inputs);
var value;
if (rhs.value != null) {
- ensureSafeObject(rhs.context, self.expression);
- ensureSafeFunction(rhs.value, self.expression);
var values = [];
for (var i = 0; i < args.length; ++i) {
- values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression));
+ values.push(args[i](scope, locals, assign, inputs));
}
- value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression);
+ value = rhs.value.apply(rhs.context, values);
}
return context ? {value: value} : value;
};
@@ -25493,8 +20020,6 @@ ASTInterpreter.prototype = {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
- ensureSafeObject(lhs.value, self.expression);
- ensureSafeAssignContext(lhs.context);
lhs.context[lhs.name] = rhs;
return context ? {value: rhs} : rhs;
};
@@ -25570,7 +20095,7 @@ ASTInterpreter.prototype = {
if (isDefined(arg)) {
arg = -arg;
} else {
- arg = 0;
+ arg = -0;
}
return context ? {value: arg} : arg;
};
@@ -25629,12 +20154,14 @@ ASTInterpreter.prototype = {
},
'binary==': function(left, right, context) {
return function(scope, locals, assign, inputs) {
+ // eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary!=': function(left, right, context) {
return function(scope, locals, assign, inputs) {
+ // eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
@@ -25684,16 +20211,13 @@ ASTInterpreter.prototype = {
value: function(value, context) {
return function() { return context ? {context: undefined, name: undefined, value: value} : value; };
},
- identifier: function(name, expensiveChecks, context, create, expression) {
+ identifier: function(name, context, create) {
return function(scope, locals, assign, inputs) {
var base = locals && (name in locals) ? locals : scope;
- if (create && create !== 1 && base && !(base[name])) {
+ if (create && create !== 1 && base && base[name] == null) {
base[name] = {};
}
var value = base ? base[name] : undefined;
- if (expensiveChecks) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: base, name: name, value: value};
} else {
@@ -25701,7 +20225,7 @@ ASTInterpreter.prototype = {
}
};
},
- computedMember: function(left, right, context, create, expression) {
+ computedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs;
@@ -25709,15 +20233,12 @@ ASTInterpreter.prototype = {
if (lhs != null) {
rhs = right(scope, locals, assign, inputs);
rhs = getStringValue(rhs);
- ensureSafeMemberName(rhs, expression);
if (create && create !== 1) {
- ensureSafeAssignContext(lhs);
if (lhs && !(lhs[rhs])) {
lhs[rhs] = {};
}
}
value = lhs[rhs];
- ensureSafeObject(value, expression);
}
if (context) {
return {context: lhs, name: rhs, value: value};
@@ -25726,19 +20247,15 @@ ASTInterpreter.prototype = {
}
};
},
- nonComputedMember: function(left, right, expensiveChecks, context, create, expression) {
+ nonComputedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
if (create && create !== 1) {
- ensureSafeAssignContext(lhs);
- if (lhs && !(lhs[right])) {
+ if (lhs && lhs[right] == null) {
lhs[right] = {};
}
}
var value = lhs != null ? lhs[right] : undefined;
- if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: lhs, name: right, value: value};
} else {
@@ -25757,28 +20274,38 @@ ASTInterpreter.prototype = {
/**
* @constructor
*/
-var Parser = function(lexer, $filter, options) {
- this.lexer = lexer;
- this.$filter = $filter;
- this.options = options;
+function Parser(lexer, $filter, options) {
this.ast = new AST(lexer, options);
- this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) :
- new ASTCompiler(this.ast, $filter);
-};
+ this.astCompiler = options.csp ? new ASTInterpreter($filter) :
+ new ASTCompiler($filter);
+}
Parser.prototype = {
constructor: Parser,
parse: function(text) {
- return this.astCompiler.compile(text, this.options.expensiveChecks);
- }
-};
+ var ast = this.getAst(text);
+ var fn = this.astCompiler.compile(ast.ast);
+ fn.literal = isLiteral(ast.ast);
+ fn.constant = isConstant(ast.ast);
+ fn.oneTime = ast.oneTime;
+ return fn;
+ },
-function isPossiblyDangerousMemberName(name) {
- return name == 'constructor';
-}
+ getAst: function(exp) {
+ var oneTime = false;
+ exp = exp.trim();
-var objectValueOf = Object.prototype.valueOf;
+ if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
+ oneTime = true;
+ exp = exp.substring(2);
+ }
+ return {
+ ast: this.ast.ast(exp),
+ oneTime: oneTime
+ };
+ }
+};
function getValueOf(value) {
return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value);
@@ -25793,15 +20320,15 @@ function getValueOf(value) {
*
* @description
*
- * Converts Angular {@link guide/expression expression} into a function.
+ * Converts AngularJS {@link guide/expression expression} into a function.
*
* ```js
* var getter = $parse('user.name');
* var setter = getter.assign;
- * var context = {user:{name:'angular'}};
+ * var context = {user:{name:'AngularJS'}};
* var locals = {user:{name:'local'}};
*
- * expect(getter(context)).toEqual('angular');
+ * expect(getter(context)).toEqual('AngularJS');
* setter(context, 'newValue');
* expect(context.user.name).toEqual('newValue');
* expect(getter(context, locals)).toEqual('local');
@@ -25830,14 +20357,14 @@ function getValueOf(value) {
/**
* @ngdoc provider
* @name $parseProvider
+ * @this
*
* @description
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
* service.
*/
function $ParseProvider() {
- var cacheDefault = createMap();
- var cacheExpensive = createMap();
+ var cache = createMap();
var literals = {
'true': true,
'false': false,
@@ -25864,9 +20391,10 @@ function $ParseProvider() {
/**
* @ngdoc method
* @name $parseProvider#setIdentifierFns
+ *
* @description
*
- * Allows defining the set of characters that are allowed in Angular expressions. The function
+ * Allows defining the set of characters that are allowed in AngularJS expressions. The function
* `identifierStart` will get called to know if a given character is a valid character to be the
* first character for an identifier. The function `identifierContinue` will get called to know if
* a given character is a valid character to be a follow-up identifier character. The functions
@@ -25876,7 +20404,7 @@ function $ParseProvider() {
* representation. It is expected for the function to return `true` or `false`, whether that
* character is allowed or not.
*
- * Since this function will be called extensivelly, keep the implementation of these functions fast,
+ * Since this function will be called extensively, keep the implementation of these functions fast,
* as the performance of these functions have a direct impact on the expressions parsing speed.
*
* @param {function=} identifierStart The function that will decide whether the given character is
@@ -25894,60 +20422,29 @@ function $ParseProvider() {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: noUnsafeEval,
- expensiveChecks: false,
- literals: copy(literals),
- isIdentifierStart: isFunction(identStart) && identStart,
- isIdentifierContinue: isFunction(identContinue) && identContinue
- },
- $parseOptionsExpensive = {
- csp: noUnsafeEval,
- expensiveChecks: true,
literals: copy(literals),
isIdentifierStart: isFunction(identStart) && identStart,
isIdentifierContinue: isFunction(identContinue) && identContinue
};
- var runningChecksEnabled = false;
-
- $parse.$$runningExpensiveChecks = function() {
- return runningChecksEnabled;
- };
-
+ $parse.$$getAst = $$getAst;
return $parse;
- function $parse(exp, interceptorFn, expensiveChecks) {
- var parsedExpression, oneTime, cacheKey;
-
- expensiveChecks = expensiveChecks || runningChecksEnabled;
+ function $parse(exp, interceptorFn) {
+ var parsedExpression, cacheKey;
switch (typeof exp) {
case 'string':
exp = exp.trim();
cacheKey = exp;
- var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
parsedExpression = cache[cacheKey];
if (!parsedExpression) {
- if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
- oneTime = true;
- exp = exp.substring(2);
- }
- var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
- var lexer = new Lexer(parseOptions);
- var parser = new Parser(lexer, $filter, parseOptions);
+ var lexer = new Lexer($parseOptions);
+ var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp);
- if (parsedExpression.constant) {
- parsedExpression.$$watchDelegate = constantWatchDelegate;
- } else if (oneTime) {
- parsedExpression.$$watchDelegate = parsedExpression.literal ?
- oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
- } else if (parsedExpression.inputs) {
- parsedExpression.$$watchDelegate = inputsWatchDelegate;
- }
- if (expensiveChecks) {
- parsedExpression = expensiveChecksInterceptor(parsedExpression);
- }
- cache[cacheKey] = parsedExpression;
+
+ cache[cacheKey] = addWatchDelegate(parsedExpression);
}
return addInterceptor(parsedExpression, interceptorFn);
@@ -25959,31 +20456,13 @@ function $ParseProvider() {
}
}
- function expensiveChecksInterceptor(fn) {
- if (!fn) return fn;
- expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate;
- expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign);
- expensiveCheckFn.constant = fn.constant;
- expensiveCheckFn.literal = fn.literal;
- for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) {
- fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]);
- }
- expensiveCheckFn.inputs = fn.inputs;
-
- return expensiveCheckFn;
-
- function expensiveCheckFn(scope, locals, assign, inputs) {
- var expensiveCheckOldValue = runningChecksEnabled;
- runningChecksEnabled = true;
- try {
- return fn(scope, locals, assign, inputs);
- } finally {
- runningChecksEnabled = expensiveCheckOldValue;
- }
- }
+ function $$getAst(exp) {
+ var lexer = new Lexer($parseOptions);
+ var parser = new Parser(lexer, $filter, $parseOptions);
+ return parser.getAst(exp).ast;
}
- function expressionInputDirtyCheck(newValue, oldValueOfValue) {
+ function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) {
if (newValue == null || oldValueOfValue == null) { // null/undefined
return newValue === oldValueOfValue;
@@ -25996,7 +20475,7 @@ function $ParseProvider() {
// be cheaply dirty-checked
newValue = getValueOf(newValue);
- if (typeof newValue === 'object') {
+ if (typeof newValue === 'object' && !compareObjectIdentity) {
// objects/arrays are not supported - deep-watching them would be too expensive
return false;
}
@@ -26005,6 +20484,7 @@ function $ParseProvider() {
}
//Primitive or NaN
+ // eslint-disable-next-line no-self-compare
return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue);
}
@@ -26017,7 +20497,7 @@ function $ParseProvider() {
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newInputValue = inputExpressions(scope);
- if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) {
+ if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) {
lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
oldInputValueOf = newInputValue && getValueOf(newInputValue);
}
@@ -26037,7 +20517,7 @@ function $ParseProvider() {
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
- if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) {
+ if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) {
oldInputValues[i] = newInputValue;
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
@@ -26051,91 +20531,127 @@ function $ParseProvider() {
}, listener, objectEquality, prettyPrintExpression);
}
- function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
+ function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
+ var isDone = parsedExpression.literal ? isAllDefined : isDefined;
var unwatch, lastValue;
- return unwatch = scope.$watch(function oneTimeWatch(scope) {
- return parsedExpression(scope);
- }, function oneTimeListener(value, old, scope) {
- lastValue = value;
- if (isFunction(listener)) {
- listener.apply(this, arguments);
- }
- if (isDefined(value)) {
- scope.$$postDigest(function() {
- if (isDefined(lastValue)) {
- unwatch();
- }
- });
- }
- }, objectEquality);
- }
- function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
- var unwatch, lastValue;
- return unwatch = scope.$watch(function oneTimeWatch(scope) {
- return parsedExpression(scope);
- }, function oneTimeListener(value, old, scope) {
- lastValue = value;
- if (isFunction(listener)) {
- listener.call(this, value, old, scope);
- }
- if (isAllDefined(value)) {
- scope.$$postDigest(function() {
- if (isAllDefined(lastValue)) unwatch();
- });
+ var exp = parsedExpression.$$intercepted || parsedExpression;
+ var post = parsedExpression.$$interceptor || identity;
+
+ var useInputs = parsedExpression.inputs && !exp.inputs;
+
+ // Propogate the literal/inputs/constant attributes
+ // ... but not oneTime since we are handling it
+ oneTimeWatch.literal = parsedExpression.literal;
+ oneTimeWatch.constant = parsedExpression.constant;
+ oneTimeWatch.inputs = parsedExpression.inputs;
+
+ // Allow other delegates to run on this wrapped expression
+ addWatchDelegate(oneTimeWatch);
+
+ unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
+
+ return unwatch;
+
+ function unwatchIfDone() {
+ if (isDone(lastValue)) {
+ unwatch();
}
- }, objectEquality);
+ }
- function isAllDefined(value) {
- var allDefined = true;
- forEach(value, function(val) {
- if (!isDefined(val)) allDefined = false;
- });
- return allDefined;
+ function oneTimeWatch(scope, locals, assign, inputs) {
+ lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs);
+ if (isDone(lastValue)) {
+ scope.$$postDigest(unwatchIfDone);
+ }
+ return post(lastValue);
}
}
+ function isAllDefined(value) {
+ var allDefined = true;
+ forEach(value, function(val) {
+ if (!isDefined(val)) allDefined = false;
+ });
+ return allDefined;
+ }
+
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
- var unwatch;
- return unwatch = scope.$watch(function constantWatch(scope) {
+ var unwatch = scope.$watch(function constantWatch(scope) {
unwatch();
return parsedExpression(scope);
}, listener, objectEquality);
+ return unwatch;
+ }
+
+ function addWatchDelegate(parsedExpression) {
+ if (parsedExpression.constant) {
+ parsedExpression.$$watchDelegate = constantWatchDelegate;
+ } else if (parsedExpression.oneTime) {
+ parsedExpression.$$watchDelegate = oneTimeWatchDelegate;
+ } else if (parsedExpression.inputs) {
+ parsedExpression.$$watchDelegate = inputsWatchDelegate;
+ }
+
+ return parsedExpression;
+ }
+
+ function chainInterceptors(first, second) {
+ function chainedInterceptor(value) {
+ return second(first(value));
+ }
+ chainedInterceptor.$stateful = first.$stateful || second.$stateful;
+ chainedInterceptor.$$pure = first.$$pure && second.$$pure;
+
+ return chainedInterceptor;
}
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
- var watchDelegate = parsedExpression.$$watchDelegate;
- var useInputs = false;
- var regularWatch =
- watchDelegate !== oneTimeLiteralWatchDelegate &&
- watchDelegate !== oneTimeWatchDelegate;
+ // Extract any existing interceptors out of the parsedExpression
+ // to ensure the original parsedExpression is always the $$intercepted
+ if (parsedExpression.$$interceptor) {
+ interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn);
+ parsedExpression = parsedExpression.$$intercepted;
+ }
+
+ var useInputs = false;
- var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
+ var fn = function interceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
- return interceptorFn(value, scope, locals);
- } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
- var value = parsedExpression(scope, locals, assign, inputs);
- var result = interceptorFn(value, scope, locals);
- // we only return the interceptor's result if the
- // initial value is defined (for bind-once)
- return isDefined(value) ? result : value;
+ return interceptorFn(value);
};
- // Propagate $$watchDelegates other then inputsWatchDelegate
- if (parsedExpression.$$watchDelegate &&
- parsedExpression.$$watchDelegate !== inputsWatchDelegate) {
- fn.$$watchDelegate = parsedExpression.$$watchDelegate;
- } else if (!interceptorFn.$stateful) {
- // If there is an interceptor, but no watchDelegate then treat the interceptor like
- // we treat filters - it is assumed to be a pure function unless flagged with $stateful
- fn.$$watchDelegate = inputsWatchDelegate;
+ // Maintain references to the interceptor/intercepted
+ fn.$$intercepted = parsedExpression;
+ fn.$$interceptor = interceptorFn;
+
+ // Propogate the literal/oneTime/constant attributes
+ fn.literal = parsedExpression.literal;
+ fn.oneTime = parsedExpression.oneTime;
+ fn.constant = parsedExpression.constant;
+
+ // Treat the interceptor like filters.
+ // If it is not $stateful then only watch its inputs.
+ // If the expression itself has no inputs then use the full expression as an input.
+ if (!interceptorFn.$stateful) {
useInputs = !parsedExpression.inputs;
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
+
+ if (!interceptorFn.$$pure) {
+ fn.inputs = fn.inputs.map(function(e) {
+ // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a
+ // non-pure interceptor function.
+ if (e.isPure === PURITY_RELATIVE) {
+ return function depurifier(s) { return e(s); };
+ }
+ return e;
+ });
+ }
}
- return fn;
+ return addWatchDelegate(fn);
}
}];
}
@@ -26149,13 +20665,13 @@ function $ParseProvider() {
* A service that helps you run functions asynchronously, and use their return values (or exceptions)
* when they are done processing.
*
- * This is an implementation of promises/deferred objects inspired by
- * [Kris Kowal's Q](https://github.com/kriskowal/q).
+ * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred
+ * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
* implementations, and the other which resembles ES6 (ES2015) promises to some degree.
*
- * # $q constructor
+ * ## $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
* function as the first argument. This is similar to the native Promise implementation from ES6,
@@ -26243,7 +20759,7 @@ function $ParseProvider() {
* For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the
* section on serial or parallel joining of promises.
*
- * # The Deferred API
+ * ## The Deferred API
*
* A new instance of deferred is constructed by calling `$q.defer()`.
*
@@ -26265,7 +20781,7 @@ function $ParseProvider() {
* - promise – `{Promise}` – promise object associated with this deferred.
*
*
- * # The Promise API
+ * ## The Promise API
*
* A new promise instance is created when a deferred instance is created and can be retrieved by
* calling `deferred.promise`.
@@ -26297,7 +20813,7 @@ function $ParseProvider() {
* specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
* more information.
*
- * # Chaining promises
+ * ## Chaining promises
*
* Because calling the `then` method of a promise returns a new derived promise, it is easily
* possible to create a chain of promises:
@@ -26317,17 +20833,17 @@ function $ParseProvider() {
* $http's response interceptors.
*
*
- * # Differences between Kris Kowal's Q and $q
+ * ## Differences between Kris Kowal's Q and $q
*
* There are two main differences:
*
* - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation
- * mechanism in angular, which means faster propagation of resolution or rejection into your
+ * mechanism in AngularJS, which means faster propagation of resolution or rejection into your
* models and avoiding unnecessary browser repaints, which would result in flickering UI.
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
- * # Testing
+ * ## Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
@@ -26357,21 +20873,61 @@ function $ParseProvider() {
*
* @returns {Promise} The newly created promise.
*/
+/**
+ * @ngdoc provider
+ * @name $qProvider
+ * @this
+ *
+ * @description
+ */
function $QProvider() {
-
+ var errorOnUnhandledRejections = true;
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
return qFactory(function(callback) {
$rootScope.$evalAsync(callback);
- }, $exceptionHandler);
+ }, $exceptionHandler, errorOnUnhandledRejections);
}];
+
+ /**
+ * @ngdoc method
+ * @name $qProvider#errorOnUnhandledRejections
+ * @kind function
+ *
+ * @description
+ * Retrieves or overrides whether to generate an error when a rejected promise is not handled.
+ * This feature is enabled by default.
+ *
+ * @param {boolean=} value Whether to generate an error when a rejected promise is not handled.
+ * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for
+ * chaining otherwise.
+ */
+ this.errorOnUnhandledRejections = function(value) {
+ if (isDefined(value)) {
+ errorOnUnhandledRejections = value;
+ return this;
+ } else {
+ return errorOnUnhandledRejections;
+ }
+ };
}
+/** @this */
function $$QProvider() {
+ var errorOnUnhandledRejections = true;
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
return qFactory(function(callback) {
$browser.defer(callback);
- }, $exceptionHandler);
+ }, $exceptionHandler, errorOnUnhandledRejections);
}];
+
+ this.errorOnUnhandledRejections = function(value) {
+ if (isDefined(value)) {
+ errorOnUnhandledRejections = value;
+ return this;
+ } else {
+ return errorOnUnhandledRejections;
+ }
+ };
}
/**
@@ -26380,10 +20936,14 @@ function $$QProvider() {
* @param {function(function)} nextTick Function for executing functions in the next turn.
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
* debugging purposes.
+ * @param {boolean=} errorOnUnhandledRejections Whether an error should be generated on unhandled
+ * promises rejections.
* @returns {object} Promise manager.
*/
-function qFactory(nextTick, exceptionHandler) {
+function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
var $qMinErr = minErr('$q', TypeError);
+ var queueSize = 0;
+ var checkQueue = [];
/**
* @ngdoc method
@@ -26395,14 +20955,18 @@ function qFactory(nextTick, exceptionHandler) {
*
* @returns {Deferred} Returns a new instance of deferred.
*/
- var defer = function() {
- var d = new Deferred();
- //Necessary to support unbound execution :/
- d.resolve = simpleBind(d, d.resolve);
- d.reject = simpleBind(d, d.reject);
- d.notify = simpleBind(d, d.notify);
- return d;
- };
+ function defer() {
+ return new Deferred();
+ }
+
+ function Deferred() {
+ var promise = this.promise = new Promise();
+ //Non prototype methods necessary to support unbound execution :/
+ this.resolve = function(val) { resolvePromise(promise, val); };
+ this.reject = function(reason) { rejectPromise(promise, reason); };
+ this.notify = function(progress) { notifyPromise(promise, progress); };
+ }
+
function Promise() {
this.$$state = { status: 0 };
@@ -26413,144 +20977,166 @@ function qFactory(nextTick, exceptionHandler) {
if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) {
return this;
}
- var result = new Deferred();
+ var result = new Promise();
this.$$state.pending = this.$$state.pending || [];
this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
- return result.promise;
+ return result;
},
- "catch": function(callback) {
+ 'catch': function(callback) {
return this.then(null, callback);
},
- "finally": function(callback, progressBack) {
+ 'finally': function(callback, progressBack) {
return this.then(function(value) {
- return handleCallback(value, true, callback);
+ return handleCallback(value, resolve, callback);
}, function(error) {
- return handleCallback(error, false, callback);
+ return handleCallback(error, reject, callback);
}, progressBack);
}
});
- //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native
- function simpleBind(context, fn) {
- return function(value) {
- fn.call(context, value);
- };
- }
-
function processQueue(state) {
- var fn, deferred, pending;
+ var fn, promise, pending;
pending = state.pending;
state.processScheduled = false;
state.pending = undefined;
- for (var i = 0, ii = pending.length; i < ii; ++i) {
- deferred = pending[i][0];
- fn = pending[i][state.status];
- try {
- if (isFunction(fn)) {
- deferred.resolve(fn(state.value));
- } else if (state.status === 1) {
- deferred.resolve(state.value);
+ try {
+ for (var i = 0, ii = pending.length; i < ii; ++i) {
+ markQStateExceptionHandled(state);
+ promise = pending[i][0];
+ fn = pending[i][state.status];
+ try {
+ if (isFunction(fn)) {
+ resolvePromise(promise, fn(state.value));
+ } else if (state.status === 1) {
+ resolvePromise(promise, state.value);
+ } else {
+ rejectPromise(promise, state.value);
+ }
+ } catch (e) {
+ rejectPromise(promise, e);
+ // This error is explicitly marked for being passed to the $exceptionHandler
+ if (e && e.$$passToExceptionHandler === true) {
+ exceptionHandler(e);
+ }
+ }
+ }
+ } finally {
+ --queueSize;
+ if (errorOnUnhandledRejections && queueSize === 0) {
+ nextTick(processChecks);
+ }
+ }
+ }
+
+ function processChecks() {
+ // eslint-disable-next-line no-unmodified-loop-condition
+ while (!queueSize && checkQueue.length) {
+ var toCheck = checkQueue.shift();
+ if (!isStateExceptionHandled(toCheck)) {
+ markQStateExceptionHandled(toCheck);
+ var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value);
+ if (isError(toCheck.value)) {
+ exceptionHandler(toCheck.value, errorMessage);
} else {
- deferred.reject(state.value);
+ exceptionHandler(errorMessage);
}
- } catch (e) {
- deferred.reject(e);
- exceptionHandler(e);
}
}
}
function scheduleProcessQueue(state) {
+ if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) {
+ if (queueSize === 0 && checkQueue.length === 0) {
+ nextTick(processChecks);
+ }
+ checkQueue.push(state);
+ }
if (state.processScheduled || !state.pending) return;
state.processScheduled = true;
+ ++queueSize;
nextTick(function() { processQueue(state); });
}
- function Deferred() {
- this.promise = new Promise();
+ function resolvePromise(promise, val) {
+ if (promise.$$state.status) return;
+ if (val === promise) {
+ $$reject(promise, $qMinErr(
+ 'qcycle',
+ 'Expected promise to be resolved with value other than itself \'{0}\'',
+ val));
+ } else {
+ $$resolve(promise, val);
+ }
+
}
- extend(Deferred.prototype, {
- resolve: function(val) {
- if (this.promise.$$state.status) return;
- if (val === this.promise) {
- this.$$reject($qMinErr(
- 'qcycle',
- "Expected promise to be resolved with value other than itself '{0}'",
- val));
+ function $$resolve(promise, val) {
+ var then;
+ var done = false;
+ try {
+ if (isObject(val) || isFunction(val)) then = val.then;
+ if (isFunction(then)) {
+ promise.$$state.status = -1;
+ then.call(val, doResolve, doReject, doNotify);
} else {
- this.$$resolve(val);
- }
-
- },
-
- $$resolve: function(val) {
- var then;
- var that = this;
- var done = false;
- try {
- if ((isObject(val) || isFunction(val))) then = val && val.then;
- if (isFunction(then)) {
- this.promise.$$state.status = -1;
- then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify));
- } else {
- this.promise.$$state.value = val;
- this.promise.$$state.status = 1;
- scheduleProcessQueue(this.promise.$$state);
- }
- } catch (e) {
- rejectPromise(e);
- exceptionHandler(e);
+ promise.$$state.value = val;
+ promise.$$state.status = 1;
+ scheduleProcessQueue(promise.$$state);
}
+ } catch (e) {
+ doReject(e);
+ }
- function resolvePromise(val) {
- if (done) return;
- done = true;
- that.$$resolve(val);
- }
- function rejectPromise(val) {
- if (done) return;
- done = true;
- that.$$reject(val);
- }
- },
+ function doResolve(val) {
+ if (done) return;
+ done = true;
+ $$resolve(promise, val);
+ }
+ function doReject(val) {
+ if (done) return;
+ done = true;
+ $$reject(promise, val);
+ }
+ function doNotify(progress) {
+ notifyPromise(promise, progress);
+ }
+ }
- reject: function(reason) {
- if (this.promise.$$state.status) return;
- this.$$reject(reason);
- },
+ function rejectPromise(promise, reason) {
+ if (promise.$$state.status) return;
+ $$reject(promise, reason);
+ }
- $$reject: function(reason) {
- this.promise.$$state.value = reason;
- this.promise.$$state.status = 2;
- scheduleProcessQueue(this.promise.$$state);
- },
+ function $$reject(promise, reason) {
+ promise.$$state.value = reason;
+ promise.$$state.status = 2;
+ scheduleProcessQueue(promise.$$state);
+ }
- notify: function(progress) {
- var callbacks = this.promise.$$state.pending;
+ function notifyPromise(promise, progress) {
+ var callbacks = promise.$$state.pending;
- if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) {
- nextTick(function() {
- var callback, result;
- for (var i = 0, ii = callbacks.length; i < ii; i++) {
- result = callbacks[i][0];
- callback = callbacks[i][3];
- try {
- result.notify(isFunction(callback) ? callback(progress) : progress);
- } catch (e) {
- exceptionHandler(e);
- }
+ if ((promise.$$state.status <= 0) && callbacks && callbacks.length) {
+ nextTick(function() {
+ var callback, result;
+ for (var i = 0, ii = callbacks.length; i < ii; i++) {
+ result = callbacks[i][0];
+ callback = callbacks[i][3];
+ try {
+ notifyPromise(result, isFunction(callback) ? callback(progress) : progress);
+ } catch (e) {
+ exceptionHandler(e);
}
- });
- }
+ }
+ });
}
- });
+ }
/**
* @ngdoc method
@@ -26588,39 +21174,27 @@ function qFactory(nextTick, exceptionHandler) {
* @param {*} reason Constant, message, exception or an object representing the rejection reason.
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
*/
- var reject = function(reason) {
- var result = new Deferred();
- result.reject(reason);
- return result.promise;
- };
-
- var makePromise = function makePromise(value, resolved) {
- var result = new Deferred();
- if (resolved) {
- result.resolve(value);
- } else {
- result.reject(value);
- }
- return result.promise;
- };
+ function reject(reason) {
+ var result = new Promise();
+ rejectPromise(result, reason);
+ return result;
+ }
- var handleCallback = function handleCallback(value, isResolved, callback) {
+ function handleCallback(value, resolver, callback) {
var callbackOutput = null;
try {
if (isFunction(callback)) callbackOutput = callback();
} catch (e) {
- return makePromise(e, false);
+ return reject(e);
}
if (isPromiseLike(callbackOutput)) {
return callbackOutput.then(function() {
- return makePromise(value, isResolved);
- }, function(error) {
- return makePromise(error, false);
- });
+ return resolver(value);
+ }, reject);
} else {
- return makePromise(value, isResolved);
+ return resolver(value);
}
- };
+ }
/**
* @ngdoc method
@@ -26640,11 +21214,11 @@ function qFactory(nextTick, exceptionHandler) {
*/
- var when = function(value, callback, errback, progressBack) {
- var result = new Deferred();
- result.resolve(value);
- return result.promise.then(callback, errback, progressBack);
- };
+ function when(value, callback, errback, progressBack) {
+ var result = new Promise();
+ resolvePromise(result, value);
+ return result.then(callback, errback, progressBack);
+ }
/**
* @ngdoc method
@@ -26679,27 +21253,25 @@ function qFactory(nextTick, exceptionHandler) {
*/
function all(promises) {
- var deferred = new Deferred(),
+ var result = new Promise(),
counter = 0,
results = isArray(promises) ? [] : {};
forEach(promises, function(promise, key) {
counter++;
when(promise).then(function(value) {
- if (results.hasOwnProperty(key)) return;
results[key] = value;
- if (!(--counter)) deferred.resolve(results);
+ if (!(--counter)) resolvePromise(result, results);
}, function(reason) {
- if (results.hasOwnProperty(key)) return;
- deferred.reject(reason);
+ rejectPromise(result, reason);
});
});
if (counter === 0) {
- deferred.resolve(results);
+ resolvePromise(result, results);
}
- return deferred.promise;
+ return result;
}
/**
@@ -26726,25 +21298,25 @@ function qFactory(nextTick, exceptionHandler) {
return deferred.promise;
}
- var $Q = function Q(resolver) {
+ function $Q(resolver) {
if (!isFunction(resolver)) {
- throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
+ throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver);
}
- var deferred = new Deferred();
+ var promise = new Promise();
function resolveFn(value) {
- deferred.resolve(value);
+ resolvePromise(promise, value);
}
function rejectFn(reason) {
- deferred.reject(reason);
+ rejectPromise(promise, reason);
}
resolver(resolveFn, rejectFn);
- return deferred.promise;
- };
+ return promise;
+ }
// Let's make the instanceof operator work for promises, so that
// `new $q(fn) instanceof $q` would evaluate to true.
@@ -26760,6 +21332,23 @@ function qFactory(nextTick, exceptionHandler) {
return $Q;
}
+function isStateExceptionHandled(state) {
+ return !!state.pur;
+}
+function markQStateExceptionHandled(state) {
+ state.pur = true;
+}
+function markQExceptionHandled(q) {
+ // Built-in `$q` promises will always have a `$$state` property. This check is to allow
+ // overwriting `$q` with a different promise library (e.g. Bluebird + angular-bluebird-promises).
+ // (Currently, this is the only method that might be called with a promise, even if it is not
+ // created by the built-in `$q`.)
+ if (q.$$state) {
+ markQStateExceptionHandled(q.$$state);
+ }
+}
+
+/** @this */
function $$RAFProvider() { //rAF
this.$get = ['$window', '$timeout', function($window, $timeout) {
var requestAnimationFrame = $window.requestAnimationFrame ||
@@ -26849,6 +21438,8 @@ function $$RAFProvider() { //rAF
/**
* @ngdoc service
* @name $rootScope
+ * @this
+ *
* @description
*
* Every application has a single root {@link ng.$rootScope.Scope scope}.
@@ -26879,6 +21470,7 @@ function $RootScopeProvider() {
this.$$watchersCount = 0;
this.$id = nextUid();
this.$$ChildScope = null;
+ this.$$suspended = false;
}
ChildScope.prototype = parent;
return ChildScope;
@@ -26893,14 +21485,19 @@ function $RootScopeProvider() {
function cleanUpScope($scope) {
+ // Support: IE 9 only
if (msie === 9) {
// There is a memory leak in IE9 if all child scopes are not disconnected
// completely when a scope is destroyed. So this code will recurse up through
// all this scopes children
//
// See issue https://github.com/angular/angular.js/issues/10706
- $scope.$$childHead && cleanUpScope($scope.$$childHead);
- $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling);
+ if ($scope.$$childHead) {
+ cleanUpScope($scope.$$childHead);
+ }
+ if ($scope.$$nextSibling) {
+ cleanUpScope($scope.$$nextSibling);
+ }
}
// The code below works around IE9 and V8's memory leaks
@@ -26926,7 +21523,7 @@ function $RootScopeProvider() {
* an in-depth introduction and usage examples.
*
*
- * # Inheritance
+ * ## Inheritance
* A scope can inherit from a parent scope, as in this example:
* ```js
var parent = $rootScope;
@@ -26961,6 +21558,7 @@ function $RootScopeProvider() {
this.$$childHead = this.$$childTail = null;
this.$root = this;
this.$$destroyed = false;
+ this.$$suspended = false;
this.$$listeners = {};
this.$$listenerCount = {};
this.$$watchersCount = 0;
@@ -27052,7 +21650,7 @@ function $RootScopeProvider() {
// 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', destroyChildScope);
+ if (isolate || parent !== this) child.$on('$destroy', destroyChildScope);
return child;
},
@@ -27069,7 +21667,7 @@ function $RootScopeProvider() {
* $digest()} and should return the value that will be watched. (`watchExpression` should not change
* its value when executed multiple times with the same input because it may be executed multiple
* times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be
- * [idempotent](http://en.wikipedia.org/wiki/Idempotence).
+ * [idempotent](http://en.wikipedia.org/wiki/Idempotence).)
* - The `listener` is called only when the value from the current `watchExpression` and the
* previous call to `watchExpression` are not equal (with the exception of the initial run,
* see below). Inequality is determined according to reference inequality,
@@ -27080,6 +21678,8 @@ function $RootScopeProvider() {
* according to the {@link angular.equals} function. To save the value of the object for
* later comparison, the {@link angular.copy} function is used. This therefore means that
* watching complex objects will have adverse memory and performance implications.
+ * - This should not be used to watch for changes in objects that are (or contain)
+ * [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}.
* - The watch `listener` may change the model, which may trigger other `listener`s to fire.
* This is achieved by rerunning the watchers until no changes are detected. The rerun
* iteration limit is 10 to prevent an infinite loop deadlock.
@@ -27099,7 +21699,7 @@ function $RootScopeProvider() {
*
*
*
- * # Example
+ * @example
* ```js
// let's assume that scope was dependency injected as the $rootScope
var scope = $rootScope;
@@ -27175,14 +21775,15 @@ function $RootScopeProvider() {
*/
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
var get = $parse(watchExp);
+ var fn = isFunction(listener) ? listener : noop;
if (get.$$watchDelegate) {
- return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
+ return get.$$watchDelegate(this, fn, objectEquality, get, watchExp);
}
var scope = this,
array = scope.$$watchers,
watcher = {
- fn: listener,
+ fn: fn,
last: initWatchVal,
get: get,
exp: prettyPrintExpression || watchExp,
@@ -27191,21 +21792,23 @@ function $RootScopeProvider() {
lastDirtyWatch = null;
- if (!isFunction(listener)) {
- watcher.fn = noop;
- }
-
if (!array) {
array = scope.$$watchers = [];
+ array.$$digestWatchIndex = -1;
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
+ array.$$digestWatchIndex++;
incrementWatchersCount(this, 1);
return function deregisterWatch() {
- if (arrayRemove(array, watcher) >= 0) {
+ var index = arrayRemove(array, watcher);
+ if (index >= 0) {
incrementWatchersCount(scope, -1);
+ if (index < array.$$digestWatchIndex) {
+ array.$$digestWatchIndex--;
+ }
}
lastDirtyWatch = null;
};
@@ -27220,8 +21823,8 @@ function $RootScopeProvider() {
* A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`.
* If any one expression in the collection changes the `listener` is executed.
*
- * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every
- * call to $digest() to see if any items changes.
+ * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return
+ * values are examined for changes on every call to `$digest`.
* - The `listener` is called whenever any expression in the `watchExpressions` array changes.
*
* @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually
@@ -27265,9 +21868,8 @@ function $RootScopeProvider() {
}
forEach(watchExpressions, function(expr, i) {
- var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) {
+ var unwatchFn = self.$watch(expr, function watchGroupSubAction(value) {
newValues[i] = value;
- oldValues[i] = oldValue;
if (!changeReactionScheduled) {
changeReactionScheduled = true;
self.$evalAsync(watchGroupAction);
@@ -27279,11 +21881,17 @@ function $RootScopeProvider() {
function watchGroupAction() {
changeReactionScheduled = false;
- if (firstRun) {
- firstRun = false;
- listener(newValues, newValues, self);
- } else {
- listener(newValues, oldValues, self);
+ try {
+ if (firstRun) {
+ firstRun = false;
+ listener(newValues, newValues, self);
+ } else {
+ listener(newValues, oldValues, self);
+ }
+ } finally {
+ for (var i = 0; i < watchExpressions.length; i++) {
+ oldValues[i] = newValues[i];
+ }
}
}
@@ -27311,7 +21919,7 @@ function $RootScopeProvider() {
* adding, removing, and moving items belonging to an object or array.
*
*
- * # Example
+ * @example
* ```js
$scope.names = ['igor', 'matias', 'misko', 'james'];
$scope.dataCount = 4;
@@ -27351,7 +21959,11 @@ function $RootScopeProvider() {
* de-registration function is executed, the internal watch operation is terminated.
*/
$watchCollection: function(obj, listener) {
- $watchCollectionInterceptor.$stateful = true;
+ // Mark the interceptor as
+ // ... $$pure when literal since the instance will change when any input changes
+ $watchCollectionInterceptor.$$pure = $parse(obj).literal;
+ // ... $stateful when non-literal since we must read the state of the collection
+ $watchCollectionInterceptor.$stateful = !$watchCollectionInterceptor.$$pure;
var self = this;
// the current value, updated on each dirty-check run
@@ -27402,6 +22014,7 @@ function $RootScopeProvider() {
oldItem = oldValue[i];
newItem = newValue[i];
+ // eslint-disable-next-line no-self-compare
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
@@ -27424,6 +22037,7 @@ function $RootScopeProvider() {
oldItem = oldValue[key];
if (key in oldValue) {
+ // eslint-disable-next-line no-self-compare
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
@@ -27507,7 +22121,7 @@ function $RootScopeProvider() {
*
* In unit tests, you may need to call `$digest()` to simulate the scope life cycle.
*
- * # Example
+ * @example
* ```js
var scope = ...;
scope.name = 'misko';
@@ -27536,9 +22150,8 @@ function $RootScopeProvider() {
$digest: function() {
var watch, value, last, fn, get,
watchers,
- length,
dirty, ttl = TTL,
- next, current, target = this,
+ next, current, target = asyncQueue.length ? $rootScope : this,
watchLog = [],
logIdx, asyncTask;
@@ -27560,12 +22173,13 @@ function $RootScopeProvider() {
current = target;
// It's safe for asyncQueuePosition to be a local variable here because this loop can't
- // be reentered recursively. Calling $digest from a function passed to $applyAsync would
+ // be reentered recursively. Calling $digest from a function passed to $evalAsync would
// lead to a '$digest already in progress' error.
for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) {
try {
asyncTask = asyncQueue[asyncQueuePosition];
- asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
+ fn = asyncTask.fn;
+ fn(asyncTask.scope, asyncTask.locals);
} catch (e) {
$exceptionHandler(e);
}
@@ -27575,12 +22189,12 @@ function $RootScopeProvider() {
traverseScopesLoop:
do { // "traverse the scopes" loop
- if ((watchers = current.$$watchers)) {
+ if ((watchers = !current.$$suspended && current.$$watchers)) {
// process our watches
- length = watchers.length;
- while (length--) {
+ watchers.$$digestWatchIndex = watchers.length;
+ while (watchers.$$digestWatchIndex--) {
try {
- watch = watchers[length];
+ watch = watchers[watchers.$$digestWatchIndex];
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch) {
@@ -27588,8 +22202,7 @@ function $RootScopeProvider() {
if ((value = get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
- : (typeof value === 'number' && typeof last === 'number'
- && isNaN(value) && isNaN(last)))) {
+ : (isNumberNaN(value) && isNumberNaN(last)))) {
dirty = true;
lastDirtyWatch = watch;
watch.last = watch.eq ? copy(value, null) : value;
@@ -27620,7 +22233,9 @@ function $RootScopeProvider() {
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast
- if (!(next = ((current.$$watchersCount && current.$$childHead) ||
+ // (though it differs due to having the extra check for $$suspended and does not
+ // check $$listenerCount)
+ if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
@@ -27651,8 +22266,101 @@ function $RootScopeProvider() {
}
}
postDigestQueue.length = postDigestQueuePosition = 0;
+
+ // Check for changes to browser url that happened during the $digest
+ // (for which no event is fired; e.g. via `history.pushState()`)
+ $browser.$$checkUrlChange();
+ },
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$suspend
+ * @kind function
+ *
+ * @description
+ * Suspend watchers of this scope subtree so that they will not be invoked during digest.
+ *
+ * This can be used to optimize your application when you know that running those watchers
+ * is redundant.
+ *
+ * **Warning**
+ *
+ * Suspending scopes from the digest cycle can have unwanted and difficult to debug results.
+ * Only use this approach if you are confident that you know what you are doing and have
+ * ample tests to ensure that bindings get updated as you expect.
+ *
+ * Some of the things to consider are:
+ *
+ * * Any external event on a directive/component will not trigger a digest while the hosting
+ * scope is suspended - even if the event handler calls `$apply()` or `$rootScope.$digest()`.
+ * * Transcluded content exists on a scope that inherits from outside a directive but exists
+ * as a child of the directive's containing scope. If the containing scope is suspended the
+ * transcluded scope will also be suspended, even if the scope from which the transcluded
+ * scope inherits is not suspended.
+ * * Multiple directives trying to manage the suspended status of a scope can confuse each other:
+ * * A call to `$suspend()` on an already suspended scope is a no-op.
+ * * A call to `$resume()` on a non-suspended scope is a no-op.
+ * * If two directives suspend a scope, then one of them resumes the scope, the scope will no
+ * longer be suspended. This could result in the other directive believing a scope to be
+ * suspended when it is not.
+ * * If a parent scope is suspended then all its descendants will be also excluded from future
+ * digests whether or not they have been suspended themselves. Note that this also applies to
+ * isolate child scopes.
+ * * Calling `$digest()` directly on a descendant of a suspended scope will still run the watchers
+ * for that scope and its descendants. When digesting we only check whether the current scope is
+ * locally suspended, rather than checking whether it has a suspended ancestor.
+ * * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be
+ * included in future digests until all its ancestors have been resumed.
+ * * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply()`
+ * against the `$rootScope` and so will still trigger a global digest even if the promise was
+ * initiated by a component that lives on a suspended scope.
+ */
+ $suspend: function() {
+ this.$$suspended = true;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$isSuspended
+ * @kind function
+ *
+ * @description
+ * Call this method to determine if this scope has been explicitly suspended. It will not
+ * tell you whether an ancestor has been suspended.
+ * To determine if this scope will be excluded from a digest triggered at the $rootScope,
+ * for example, you must check all its ancestors:
+ *
+ * ```
+ * function isExcludedFromDigest(scope) {
+ * while(scope) {
+ * if (scope.$isSuspended()) return true;
+ * scope = scope.$parent;
+ * }
+ * return false;
+ * ```
+ *
+ * Be aware that a scope may not be included in digests if it has a suspended ancestor,
+ * even if `$isSuspended()` returns false.
+ *
+ * @returns true if the current scope has been suspended.
+ */
+ $isSuspended: function() {
+ return this.$$suspended;
},
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$resume
+ * @kind function
+ *
+ * @description
+ * Resume watchers of this scope subtree in case it was suspended.
+ *
+ * See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach.
+ */
+ $resume: function() {
+ this.$$suspended = false;
+ },
/**
* @ngdoc event
@@ -27708,8 +22416,8 @@ function $RootScopeProvider() {
// sever all the references to parent scopes (after this cleanup, the current scope should
// not be retained by any of our references and should be eligible for garbage collection)
- if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
- if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
+ if (parent && parent.$$childHead === this) parent.$$childHead = this.$$nextSibling;
+ if (parent && parent.$$childTail === this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
@@ -27730,10 +22438,10 @@ function $RootScopeProvider() {
*
* @description
* Executes the `expression` on the current scope and returns the result. Any exceptions in
- * the expression are propagated (uncaught). This is useful when evaluating Angular
+ * the expression are propagated (uncaught). This is useful when evaluating AngularJS
* expressions.
*
- * # Example
+ * @example
* ```js
var scope = ng.$rootScope.Scope();
scope.a = 1;
@@ -27743,7 +22451,7 @@ function $RootScopeProvider() {
expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
* ```
*
- * @param {(string|function())=} expression An angular expression to be executed.
+ * @param {(string|function())=} expression An AngularJS expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
@@ -27778,7 +22486,7 @@ function $RootScopeProvider() {
* will be scheduled. However, it is encouraged to always call code that changes the model
* from within an `$apply` call. That includes code evaluated via `$evalAsync`.
*
- * @param {(string|function())=} expression An angular expression to be executed.
+ * @param {(string|function())=} expression An AngularJS expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
@@ -27793,10 +22501,10 @@ function $RootScopeProvider() {
if (asyncQueue.length) {
$rootScope.$digest();
}
- });
+ }, null, '$evalAsync');
}
- asyncQueue.push({scope: this, expression: $parse(expr), locals: locals});
+ asyncQueue.push({scope: this, fn: $parse(expr), locals: locals});
},
$$postDigest: function(fn) {
@@ -27809,15 +22517,14 @@ function $RootScopeProvider() {
* @kind function
*
* @description
- * `$apply()` is used to execute an expression in angular from outside of the angular
+ * `$apply()` is used to execute an expression in AngularJS from outside of the AngularJS
* framework. (For example from browser DOM events, setTimeout, XHR or third party libraries).
- * Because we are calling into the angular framework we need to perform proper scope life
+ * Because we are calling into the AngularJS framework we need to perform proper scope life
* cycle of {@link ng.$exceptionHandler exception handling},
* {@link ng.$rootScope.Scope#$digest executing watches}.
*
- * ## Life cycle
+ * **Life cycle: Pseudo-Code of `$apply()`**
*
- * # Pseudo-Code of `$apply()`
* ```js
function $apply(expr) {
try {
@@ -27841,7 +22548,7 @@ function $RootScopeProvider() {
* expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method.
*
*
- * @param {(string|function())=} exp An angular expression to be executed.
+ * @param {(string|function())=} exp An AngularJS expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with current `scope` parameter.
@@ -27863,6 +22570,7 @@ function $RootScopeProvider() {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
+ // eslint-disable-next-line no-unsafe-finally
throw e;
}
}
@@ -27880,14 +22588,16 @@ function $RootScopeProvider() {
* This can be used to queue up multiple expressions which need to be evaluated in the same
* digest.
*
- * @param {(string|function())=} exp An angular expression to be executed.
+ * @param {(string|function())=} exp An AngularJS expression to be executed.
*
* - `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 && applyAsyncQueue.push($applyAsyncExpression);
+ if (expr) {
+ applyAsyncQueue.push($applyAsyncExpression);
+ }
expr = $parse(expr);
scheduleApplyAsync();
@@ -27942,7 +22652,10 @@ function $RootScopeProvider() {
return function() {
var indexOfListener = namedListeners.indexOf(listener);
if (indexOfListener !== -1) {
- namedListeners[indexOfListener] = null;
+ // Use delete in the hope of the browser deallocating the memory for the array entry,
+ // while not shifting the array indexes of other listeners.
+ // See issue https://github.com/angular/angular.js/issues/16135
+ delete namedListeners[indexOfListener];
decrementListenerCount(self, 1, name);
}
};
@@ -28009,8 +22722,7 @@ function $RootScopeProvider() {
}
//if any listener on the current scope stops propagation, prevent bubbling
if (stopPropagation) {
- event.currentScope = null;
- return event;
+ break;
}
//traverse upwards
scope = scope.$parent;
@@ -28084,7 +22796,8 @@ function $RootScopeProvider() {
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $digest
- // (though it differs due to having the extra check for $$listenerCount)
+ // (though it differs due to having the extra check for $$listenerCount and
+ // does not check $$suspended)
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
@@ -28159,7 +22872,7 @@ function $RootScopeProvider() {
if (applyAsyncId === null) {
applyAsyncId = $browser.defer(function() {
$rootScope.$apply(flushApplyAsync);
- });
+ }, null, '$applyAsync');
}
}
}];
@@ -28170,7 +22883,7 @@ function $RootScopeProvider() {
* @name $rootElement
*
* @description
- * The root element of Angular application. This is either the element where {@link
+ * The root element of AngularJS application. This is either the element where {@link
* ng.directive:ngApp ngApp} was declared or the element passed into
* {@link angular.bootstrap}. The element represents the root element of application. It is also the
* location where the application's {@link auto.$injector $injector} service gets
@@ -28181,67 +22894,79 @@ function $RootScopeProvider() {
// the implementation is in angular.bootstrap
/**
+ * @this
* @description
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
function $$SanitizeUriProvider() {
- var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
- imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
+
+ var aHrefSanitizationTrustedUrlList = /^\s*(https?|s?ftp|mailto|tel|file):/,
+ imgSrcSanitizationTrustedUrlList = /^\s*((https?|ftp|file|blob):|data:image\/)/;
/**
* @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
+ * Retrieves or overrides the default regular expression that is used for determining trusted safe
* urls during a[href] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML anchor links.
+ *
+ * Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring
+ * the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)`
+ * which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL.
*
- * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * If the URL matches the `aHrefSanitizationTrustedUrlList` regular expression, it is returned unchanged.
*
- * @param {RegExp=} regexp New regexp to whitelist urls with.
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
+ *
+ * @param {RegExp=} regexp New regexp to trust urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
- this.aHrefSanitizationWhitelist = function(regexp) {
+ this.aHrefSanitizationTrustedUrlList = function(regexp) {
if (isDefined(regexp)) {
- aHrefSanitizationWhitelist = regexp;
+ aHrefSanitizationTrustedUrlList = regexp;
return this;
}
- return aHrefSanitizationWhitelist;
+ return aHrefSanitizationTrustedUrlList;
};
/**
* @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
+ * Retrieves or overrides the default regular expression that is used for determining trusted safe
* urls during img[src] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links.
+ *
+ * Any URL due to be assigned to an `img[src]` attribute via interpolation is marked as requiring
+ * the $sce.MEDIA_URL security context. When interpolation occurs a call is made to
+ * `$sce.trustAsMediaUrl(url)` which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize
+ * the potentially malicious URL.
+ *
+ * If the URL matches the `imgSrcSanitizationTrustedUrlList` regular expression, it is returned
+ * unchanged.
*
- * Any url about to be assigned to img[src] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
*
- * @param {RegExp=} regexp New regexp to whitelist urls with.
+ * @param {RegExp=} regexp New regexp to trust urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
- this.imgSrcSanitizationWhitelist = function(regexp) {
+ this.imgSrcSanitizationTrustedUrlList = function(regexp) {
if (isDefined(regexp)) {
- imgSrcSanitizationWhitelist = regexp;
+ imgSrcSanitizationTrustedUrlList = regexp;
return this;
}
- return imgSrcSanitizationWhitelist;
+ return imgSrcSanitizationTrustedUrlList;
};
this.$get = function() {
- return function sanitizeUri(uri, isImage) {
- var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
- var normalizedVal;
- normalizedVal = urlResolve(uri).href;
+ return function sanitizeUri(uri, isMediaUrl) {
+ // if (!uri) return uri;
+ var regex = isMediaUrl ? imgSrcSanitizationTrustedUrlList : aHrefSanitizationTrustedUrlList;
+ var normalizedVal = urlResolve(uri && uri.trim()).href;
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
return 'unsafe:' + normalizedVal;
}
@@ -28261,20 +22986,43 @@ function $$SanitizeUriProvider() {
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+/* exported $SceProvider, $SceDelegateProvider */
+
var $sceMinErr = minErr('$sce');
var SCE_CONTEXTS = {
+ // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding).
HTML: 'html',
+
+ // Style statements or stylesheets. Currently unused in AngularJS.
CSS: 'css',
+
+ // An URL used in a context where it refers to the source of media, which are not expected to be run
+ // as scripts, such as an image, audio, video, etc.
+ MEDIA_URL: 'mediaUrl',
+
+ // An URL used in a context where it does not refer to a resource that loads code.
+ // A value that can be trusted as a URL can also trusted as a MEDIA_URL.
URL: 'url',
- // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a
- // url. (e.g. ng-include, script src, templateUrl)
+
+ // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as
+ // code. (e.g. ng-include, script src binding, templateUrl)
+ // A value that can be trusted as a RESOURCE_URL, can also trusted as a URL and a MEDIA_URL.
RESOURCE_URL: 'resourceUrl',
+
+ // Script. Currently unused in AngularJS.
JS: 'js'
};
// Helper functions follow.
+var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g;
+
+function snakeToCamel(name) {
+ return name
+ .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace);
+}
+
function adjustMatcher(matcher) {
if (matcher === 'self') {
return matcher;
@@ -28288,8 +23036,8 @@ function adjustMatcher(matcher) {
'Illegal sequence *** in string matcher. String: {0}', matcher);
}
matcher = escapeForRegexp(matcher).
- replace('\\*\\*', '.*').
- replace('\\*', '[^:/.?&;]*');
+ replace(/\\\*\\\*/g, '.*').
+ replace(/\\\*/g, '[^:/.?&;]*');
return new RegExp('^' + matcher + '$');
} else if (isRegExp(matcher)) {
// The only other type of matcher allowed is a Regexp.
@@ -28324,6 +23072,16 @@ function adjustMatchers(matchers) {
* `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict
* Contextual Escaping (SCE)} services to AngularJS.
*
+ * For an overview of this service and the functionnality it provides in AngularJS, see the main
+ * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how
+ * SCE works in their application, which shouldn't be needed in most cases.
+ *
+ * <div class="alert alert-danger">
+ * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or
+ * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners,
+ * changes to this service will also influence users, so be extra careful and document your changes.
+ * </div>
+ *
* Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of
* the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is
* because, while the `$sce` provides numerous shorthand methods, etc., you really only need to
@@ -28335,125 +23093,177 @@ function adjustMatchers(matchers) {
* The default instance of `$sceDelegate` should work out of the box with little pain. While you
* can override it completely to change the behavior of `$sce`, the common case would
* involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting
- * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as
- * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist
- * $sceDelegateProvider.resourceUrlWhitelist} and {@link
- * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}
+ * your own trusted and banned resource lists for trusting URLs used for loading AngularJS resources
+ * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * $sceDelegateProvider.trustedResourceUrlList} and {@link
+ * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}
*/
/**
* @ngdoc provider
* @name $sceDelegateProvider
+ * @this
+ *
* @description
*
* The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
- * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure
- * that the URLs used for sourcing Angular templates are safe. Refer {@link
- * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and
- * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}
+ * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}.
*
- * For the general details about this service in Angular, read the main page for {@link ng.$sce
+ * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and
+ * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularJS templates and
+ * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See
+ * {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * $sceDelegateProvider.trustedResourceUrlList} and
+ * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList},
+ *
+ * For the general details about this service in AngularJS, read the main page for {@link ng.$sce
* Strict Contextual Escaping (SCE)}.
*
* **Example**: Consider the following case. <a name="example"></a>
*
* - your app is hosted at url `http://myapp.example.com/`
* - but some of your templates are hosted on other domains you control such as
- * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc.
+ * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc.
* - and you have an open redirect at `http://myapp.example.com/clickThru?...`.
*
* Here is what a secure configuration for this scenario might look like:
*
* ```
* angular.module('myApp', []).config(function($sceDelegateProvider) {
- * $sceDelegateProvider.resourceUrlWhitelist([
+ * $sceDelegateProvider.trustedResourceUrlList([
* // Allow same origin resource loads.
* 'self',
* // Allow loading from our assets domain. Notice the difference between * and **.
* 'http://srv*.assets.example.com/**'
* ]);
*
- * // The blacklist overrides the whitelist so the open redirect here is blocked.
- * $sceDelegateProvider.resourceUrlBlacklist([
+ * // The banned resource URL list overrides the trusted resource URL list so the open redirect
+ * // here is blocked.
+ * $sceDelegateProvider.bannedResourceUrlList([
* 'http://myapp.example.com/clickThru**'
* ]);
* });
* ```
+ * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require
+ * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates
+ * requested by {@link ng.$templateRequest $templateRequest} that are present in
+ * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism
+ * to populate your templates in that cache at config time, then it is a good idea to remove 'self'
+ * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types
+ * of issues, like for instance attacker-controlled `ng-includes`.
*/
function $SceDelegateProvider() {
this.SCE_CONTEXTS = SCE_CONTEXTS;
// Resource URLs can also be trusted by policy.
- var resourceUrlWhitelist = ['self'],
- resourceUrlBlacklist = [];
+ var trustedResourceUrlList = ['self'],
+ bannedResourceUrlList = [];
/**
* @ngdoc method
- * @name $sceDelegateProvider#resourceUrlWhitelist
+ * @name $sceDelegateProvider#trustedResourceUrlList
* @kind function
*
- * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
+ * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with
+ * the value provided. This must be an array or null. A snapshot of this array is used so
+ * further changes to the array are ignored.
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.
*
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
+ * @return {Array} The currently set trusted resource URL array.
*
- * <div class="alert alert-warning">
- * **Note:** an empty whitelist array will block all URLs!
- * </div>
- *
- * @return {Array} the currently set whitelist array.
+ * @description
+ * Sets/Gets the list trusted of resource URLs.
*
- * The **default value** when no whitelist has been explicitly set is `['self']` allowing only
- * same origin resource requests.
+ * The **default value** when no `trustedResourceUrlList` has been explicitly set is `['self']`
+ * allowing only same origin resource requests.
*
- * @description
- * Sets/Gets the whitelist of trusted resource URLs.
+ * <div class="alert alert-warning">
+ * **Note:** the default `trustedResourceUrlList` of 'self' is not recommended if your app shares
+ * its origin with other apps! It is a good idea to limit it to only your application's directory.
+ * </div>
*/
- this.resourceUrlWhitelist = function(value) {
+ this.trustedResourceUrlList = function(value) {
if (arguments.length) {
- resourceUrlWhitelist = adjustMatchers(value);
+ trustedResourceUrlList = adjustMatchers(value);
}
- return resourceUrlWhitelist;
+ return trustedResourceUrlList;
};
/**
* @ngdoc method
- * @name $sceDelegateProvider#resourceUrlBlacklist
+ * @name $sceDelegateProvider#resourceUrlWhitelist
* @kind function
*
- * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
+ * @deprecated
+ * sinceVersion="1.8.1"
*
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
- *
- * The typical usage for the blacklist is to **block
- * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
- * these would otherwise be trusted but actually return content from the redirected domain.
- *
- * Finally, **the blacklist overrides the whitelist** and has the final say.
+ * This method is deprecated. Use {@link $sceDelegateProvider#trustedResourceUrlList
+ * trustedResourceUrlList} instead.
+ */
+ Object.defineProperty(this, 'resourceUrlWhitelist', {
+ get: function() {
+ return this.trustedResourceUrlList;
+ },
+ set: function(value) {
+ this.trustedResourceUrlList = value;
+ }
+ });
+
+ /**
+ * @ngdoc method
+ * @name $sceDelegateProvider#bannedResourceUrlList
+ * @kind function
*
- * @return {Array} the currently set blacklist array.
+ * @param {Array=} bannedResourceUrlList When provided, replaces the `bannedResourceUrlList` with
+ * the value provided. This must be an array or null. A snapshot of this array is used so
+ * further changes to the array are ignored.</p><p>
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.</p><p>
+ * The typical usage for the `bannedResourceUrlList` is to **block
+ * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
+ * these would otherwise be trusted but actually return content from the redirected domain.
+ * </p><p>
+ * Finally, **the banned resource URL list overrides the trusted resource URL list** and has
+ * the final say.
*
- * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there
- * is no blacklist.)
+ * @return {Array} The currently set `bannedResourceUrlList` array.
*
* @description
- * Sets/Gets the blacklist of trusted resource URLs.
+ * Sets/Gets the `bannedResourceUrlList` of trusted resource URLs.
+ *
+ * The **default value** when no trusted resource URL list has been explicitly set is the empty
+ * array (i.e. there is no `bannedResourceUrlList`.)
*/
-
- this.resourceUrlBlacklist = function(value) {
+ this.bannedResourceUrlList = function(value) {
if (arguments.length) {
- resourceUrlBlacklist = adjustMatchers(value);
+ bannedResourceUrlList = adjustMatchers(value);
}
- return resourceUrlBlacklist;
+ return bannedResourceUrlList;
};
- this.$get = ['$injector', function($injector) {
+ /**
+ * @ngdoc method
+ * @name $sceDelegateProvider#resourceUrlBlacklist
+ * @kind function
+ *
+ * @deprecated
+ * sinceVersion="1.8.1"
+ *
+ * This method is deprecated. Use {@link $sceDelegateProvider#bannedResourceUrlList
+ * bannedResourceUrlList} instead.
+ */
+ Object.defineProperty(this, 'resourceUrlBlacklist', {
+ get: function() {
+ return this.bannedResourceUrlList;
+ },
+ set: function(value) {
+ this.bannedResourceUrlList = value;
+ }
+ });
+
+ this.$get = ['$injector', '$$sanitizeUri', function($injector, $$sanitizeUri) {
var htmlSanitizer = function htmlSanitizer(html) {
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
@@ -28466,7 +23276,7 @@ function $SceDelegateProvider() {
function matchUrl(matcher, parsedUrl) {
if (matcher === 'self') {
- return urlIsSameOrigin(parsedUrl);
+ return urlIsSameOrigin(parsedUrl) || urlIsSameOriginAsBaseUrl(parsedUrl);
} else {
// definitely a regex. See adjustMatchers()
return !!matcher.exec(parsedUrl.href);
@@ -28476,17 +23286,17 @@ function $SceDelegateProvider() {
function isResourceUrlAllowedByPolicy(url) {
var parsedUrl = urlResolve(url.toString());
var i, n, allowed = false;
- // Ensure that at least one item from the whitelist allows this url.
- for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) {
- if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) {
+ // Ensure that at least one item from the trusted resource URL list allows this url.
+ for (i = 0, n = trustedResourceUrlList.length; i < n; i++) {
+ if (matchUrl(trustedResourceUrlList[i], parsedUrl)) {
allowed = true;
break;
}
}
if (allowed) {
- // Ensure that no item from the blacklist blocked this url.
- for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) {
- if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) {
+ // Ensure that no item from the banned resource URL list has blocked this url.
+ for (i = 0, n = bannedResourceUrlList.length; i < n; i++) {
+ if (matchUrl(bannedResourceUrlList[i], parsedUrl)) {
allowed = false;
break;
}
@@ -28518,7 +23328,8 @@ function $SceDelegateProvider() {
byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase);
- byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.MEDIA_URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.URL] = generateHolderType(byType[SCE_CONTEXTS.MEDIA_URL]);
byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]);
@@ -28527,17 +23338,24 @@ function $SceDelegateProvider() {
* @name $sceDelegate#trustAs
*
* @description
- * Returns an object that is trusted by angular for use in specified strict
- * contextual escaping contexts (such as ng-bind-html, ng-include, any src
- * attribute interpolation, any dom event binding attribute interpolation
- * such as for onclick, etc.) that uses the provided value.
- * See {@link ng.$sce $sce} for enabling strict contextual escaping.
+ * Returns a trusted representation of the parameter for the specified context. This trusted
+ * object will later on be used as-is, without any security check, by bindings or directives
+ * that require this security context.
+ * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass
+ * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as
+ * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the
+ * sanitizer loaded, passing the value itself will render all the HTML that does not pose a
+ * security risk.
+ *
+ * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those
+ * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual
+ * escaping.
+ *
+ * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`,
+ * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`.
*
- * @param {string} type The kind of context in which this value is safe for use. e.g. url,
- * resourceUrl, html, js and css.
- * @param {*} value The value that that should be considered trusted/safe.
- * @returns {*} A value that can be used to stand in for the provided `value` in places
- * where Angular expects a $sce.trustAs() return value.
+ * @param {*} value The value that should be considered trusted.
+ * @return {*} A trusted representation of value, that can be used in the given context.
*/
function trustAs(type, trustedValue) {
var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
@@ -28569,11 +23387,11 @@ function $SceDelegateProvider() {
* ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}.
*
* If the passed parameter is not a value that had been returned by {@link
- * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is.
+ * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is.
*
* @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}
- * call or anything else.
- * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs
+ * call or anything else.
+ * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs
* `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns
* `value` unchanged.
*/
@@ -28590,33 +23408,56 @@ function $SceDelegateProvider() {
* @name $sceDelegate#getTrusted
*
* @description
- * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and
- * returns the originally supplied value if the queried context type is a supertype of the
- * created type. If this condition isn't satisfied, throws an exception.
+ * Given an object and a security context in which to assign it, returns a value that's safe to
+ * use in this context, which was represented by the parameter. To do so, this function either
+ * unwraps the safe type it has been given (for instance, a {@link ng.$sceDelegate#trustAs
+ * `$sceDelegate.trustAs`} result), or it might try to sanitize the value given, depending on
+ * the context and sanitizer availablility.
+ *
+ * The contexts that can be sanitized are $sce.MEDIA_URL, $sce.URL and $sce.HTML. The first two are available
+ * by default, and the third one relies on the `$sanitize` service (which may be loaded through
+ * the `ngSanitize` module). Furthermore, for $sce.RESOURCE_URL context, a plain string may be
+ * accepted if the resource url policy defined by {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * `$sceDelegateProvider.trustedResourceUrlList`} and {@link ng.$sceDelegateProvider#bannedResourceUrlList
+ * `$sceDelegateProvider.bannedResourceUrlList`} accepts that resource.
+ *
+ * This function will throw if the safe type isn't appropriate for this context, or if the
+ * value given cannot be accepted in the context (which might be caused by sanitization not
+ * being available, or the value not being recognized as safe).
*
* <div class="alert alert-danger">
* Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
* (XSS) vulnerability in your application.
* </div>
*
- * @param {string} type The kind of context in which this value is to be used.
+ * @param {string} type The context in which this value is to be used (such as `$sce.HTML`).
* @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs
- * `$sceDelegate.trustAs`} call.
- * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs
- * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception.
+ * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.)
+ * @return {*} A version of the value that's safe to use in the given context, or throws an
+ * exception if this is impossible.
*/
function getTrusted(type, maybeTrusted) {
if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') {
return maybeTrusted;
}
var constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
+ // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return
+ // as-is.
if (constructor && maybeTrusted instanceof constructor) {
return maybeTrusted.$$unwrapTrustedValue();
}
- // If we get here, then we may only take one of two actions.
- // 1. sanitize the value for the requested type, or
- // 2. throw an exception.
- if (type === SCE_CONTEXTS.RESOURCE_URL) {
+
+ // If maybeTrusted is a trusted class instance but not of the correct trusted type
+ // then unwrap it and allow it to pass through to the rest of the checks
+ if (isFunction(maybeTrusted.$$unwrapTrustedValue)) {
+ maybeTrusted = maybeTrusted.$$unwrapTrustedValue();
+ }
+
+ // If we get here, then we will either sanitize the value or throw an exception.
+ if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) {
+ // we attempt to sanitize non-resource URLs
+ return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL);
+ } else if (type === SCE_CONTEXTS.RESOURCE_URL) {
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
return maybeTrusted;
} else {
@@ -28625,8 +23466,10 @@ function $SceDelegateProvider() {
maybeTrusted.toString());
}
} else if (type === SCE_CONTEXTS.HTML) {
+ // htmlSanitizer throws its own error when no sanitizer is available.
return htmlSanitizer(maybeTrusted);
}
+ // Default error when the $sce service has no way to make the input safe.
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
}
@@ -28640,6 +23483,8 @@ function $SceDelegateProvider() {
/**
* @ngdoc provider
* @name $sceProvider
+ * @this
+ *
* @description
*
* The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service.
@@ -28649,8 +23494,6 @@ function $SceDelegateProvider() {
* Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}.
*/
-/* jshint maxlen: false*/
-
/**
* @ngdoc service
* @name $sce
@@ -28660,23 +23503,29 @@ function $SceDelegateProvider() {
*
* `$sce` is a service that provides Strict Contextual Escaping services to AngularJS.
*
- * # Strict Contextual Escaping
+ * ## Strict Contextual Escaping
+ *
+ * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render
+ * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and
+ * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
+ *
+ * ### Overview
*
- * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain
- * contexts to result in a value that is marked as safe to use for that context. One example of
- * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer
- * to these contexts as privileged or SCE contexts.
+ * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in
+ * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically
+ * run security checks on them (sanitizations, trusted URL resource, depending on context), or throw
+ * when it cannot guarantee the security of the result. That behavior depends strongly on contexts:
+ * HTML can be sanitized, but template URLs cannot, for instance.
*
- * As of version 1.2, Angular ships with SCE enabled by default.
+ * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML:
+ * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it
+ * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and
+ * render the input as-is, you will need to mark it as trusted for that context before attempting
+ * to bind it.
*
- * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow
- * one to execute arbitrary javascript by the use of the expression() syntax. Refer
- * <http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx> to learn more about them.
- * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>`
- * to the top of your HTML document.
+ * As of version 1.2, AngularJS ships with SCE enabled by default.
*
- * SCE assists in writing code in a way that (a) is secure by default and (b) makes auditing for
- * security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
+ * ### In practice
*
* Here's an example of a binding in a privileged context:
*
@@ -28686,10 +23535,10 @@ function $SceDelegateProvider() {
* ```
*
* Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE
- * disabled, this application allows the user to render arbitrary HTML into the DIV.
- * In a more realistic example, one may be rendering user comments, blog articles, etc. via
- * bindings. (HTML is just one example of a context where rendering user controlled input creates
- * security vulnerabilities.)
+ * disabled, this application allows the user to render arbitrary HTML into the DIV, which would
+ * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog
+ * articles, etc. via bindings. (HTML is just one example of a context where rendering user
+ * controlled input creates security vulnerabilities.)
*
* For the case of HTML, you might use a library, either on the client side, or on the server side,
* to sanitize unsafe HTML before binding to the value and rendering it in the document.
@@ -28699,25 +23548,29 @@ function $SceDelegateProvider() {
* ensure that you didn't accidentally delete the line that sanitized the value, or renamed some
* properties/fields and forgot to update the binding to the sanitized value?
*
- * To be secure by default, you want to ensure that any such bindings are disallowed unless you can
- * determine that something explicitly says it's safe to use a value for binding in that
- * context. You can then audit your code (a simple grep would do) to ensure that this is only done
- * for those values that you can easily tell are safe - because they were received from your server,
- * sanitized by your library, etc. You can organize your codebase to help with this - perhaps
- * allowing only the files in a specific directory to do this. Ensuring that the internal API
- * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task.
+ * To be secure by default, AngularJS makes sure bindings go through that sanitization, or
+ * any similar validation process, unless there's a good reason to trust the given value in this
+ * context. That trust is formalized with a function call. This means that as a developer, you
+ * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues,
+ * you just need to ensure the values you mark as trusted indeed are safe - because they were
+ * received from your server, sanitized by your library, etc. You can organize your codebase to
+ * help with this - perhaps allowing only the files in a specific directory to do this.
+ * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then
+ * becomes a more manageable task.
*
* In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs}
* (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to
- * obtain values that will be accepted by SCE / privileged contexts.
+ * build the trusted versions of your values.
*
- *
- * ## How does it work?
+ * ### How does it work?
*
* In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted
- * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link
- * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
- * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals.
+ * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as
+ * a way to enforce the required security context in your data sink. Directives use {@link
+ * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs
+ * the {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. Also,
+ * when binding without directives, AngularJS will understand the context of your bindings
+ * automatically.
*
* As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
* ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly
@@ -28733,16 +23586,16 @@ function $SceDelegateProvider() {
* }];
* ```
*
- * ## Impact on loading templates
+ * ### Impact on loading templates
*
* This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as
* `templateUrl`'s specified by {@link guide/directive directives}.
*
- * By default, Angular only loads templates from the same domain and protocol as the application
+ * By default, AngularJS only loads templates from the same domain and protocol as the application
* document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
* $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or
- * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
- * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
+ * protocols, you may either add them to the {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * trustedResourceUrlList} or {@link ng.$sce#trustAsResourceUrl wrap them} into trusted values.
*
* *Please note*:
* The browser's
@@ -28753,40 +23606,58 @@ function $SceDelegateProvider() {
* won't work on all browsers. Also, loading templates from `file://` URL does not work on some
* browsers.
*
- * ## This feels like too much overhead
+ * ### This feels like too much overhead
*
* It's important to remember that SCE only applies to interpolation expressions.
*
* If your expressions are constant literals, they're automatically trusted and you don't need to
- * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g.
- * `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works.
- *
- * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them
- * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here.
+ * call `$sce.trustAs` on them (e.g.
+ * `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works (remember to include the
+ * `ngSanitize` module). The `$sceDelegate` will also use the `$sanitize` service if it is available
+ * when binding untrusted values to `$sce.HTML` context.
+ * AngularJS provides an implementation in `angular-sanitize.js`, and if you
+ * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in
+ * your application.
*
* The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load
* templates in `ng-include` from your application's domain without having to even know about SCE.
* It blocks loading templates from other domains or loading templates over http from an https
* served document. You can change these by setting your own custom {@link
- * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link
- * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs.
+ * ng.$sceDelegateProvider#trustedResourceUrlList trusted resource URL list} and {@link
+ * ng.$sceDelegateProvider#bannedResourceUrlList banned resource URL list} for matching such URLs.
*
* This significantly reduces the overhead. It is far easier to pay the small overhead and have an
* application that's secure and can be audited to verify that with much more ease than bolting
* security onto an application later.
*
* <a name="contexts"></a>
- * ## What trusted context types are supported?
+ * ### What trusted context types are supported?
*
* | Context | Notes |
* |---------------------|----------------|
* | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. |
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
- * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. |
- * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
+ * | `$sce.MEDIA_URL` | For URLs that are safe to render as media. Is automatically converted from string by sanitizing when needed. |
+ * | `$sce.URL` | For URLs that are safe to follow as links. Is automatically converted from string by sanitizing when needed. Note that `$sce.URL` makes a stronger statement about the URL than `$sce.MEDIA_URL` does and therefore contexts requiring values trusted for `$sce.URL` can be used anywhere that values trusted for `$sce.MEDIA_URL` are required.|
+ * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` or `$sce.MEDIA_URL` do and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` or `$sce.MEDIA_URL` are required. <br><br> The {@link $sceDelegateProvider#trustedResourceUrlList $sceDelegateProvider#trustedResourceUrlList()} and {@link $sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider#bannedResourceUrlList()} can be used to restrict trusted origins for `RESOURCE_URL` |
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
*
- * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a>
+ *
+ * <div class="alert alert-warning">
+ * Be aware that, before AngularJS 1.7.0, `a[href]` and `img[src]` used to sanitize their
+ * interpolated values directly rather than rely upon {@link ng.$sce#getTrusted `$sce.getTrusted`}.
+ *
+ * **As of 1.7.0, this is no longer the case.**
+ *
+ * Now such interpolations are marked as requiring `$sce.URL` (for `a[href]`) or `$sce.MEDIA_URL`
+ * (for `img[src]`), so that the sanitization happens (via `$sce.getTrusted...`) when the `$interpolate`
+ * service evaluates the expressions.
+ * </div>
+ *
+ * There are no CSS or JS context bindings in AngularJS currently, so their corresponding `$sce.trustAs`
+ * functions aren't useful yet. This might evolve.
+ *
+ * ### Format of items in {@link ng.$sceDelegateProvider#trustedResourceUrlList trustedResourceUrlList}/{@link ng.$sceDelegateProvider#bannedResourceUrlList bannedResourceUrlList} <a name="resourceUrlPatternItem"></a>
*
* Each element in these arrays must be one of the following:
*
@@ -28800,7 +23671,7 @@ function $SceDelegateProvider() {
* match themselves.
* - `*`: matches zero or more occurrences of any character other than one of the following 6
* characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use
- * in a whitelist.
+ * for matching resource URL lists.
* - `**`: matches zero or more occurrences of *any* character. As such, it's not
* appropriate for use in a scheme, domain, etc. as it would match too much. (e.g.
* http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might
@@ -28833,9 +23704,9 @@ function $SceDelegateProvider() {
*
* Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example.
*
- * ## Show me an example using SCE.
+ * ### Show me an example using SCE.
*
- * <example module="mySceApp" deps="angular-sanitize.js">
+ * <example module="mySceApp" deps="angular-sanitize.js" name="sce-service">
* <file name="index.html">
* <div ng-controller="AppController as myCtrl">
* <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br>
@@ -28856,10 +23727,10 @@ function $SceDelegateProvider() {
* <file name="script.js">
* angular.module('mySceApp', ['ngSanitize'])
* .controller('AppController', ['$http', '$templateCache', '$sce',
- * function($http, $templateCache, $sce) {
+ * function AppController($http, $templateCache, $sce) {
* var self = this;
- * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
- * self.userComments = userComments;
+ * $http.get('test_data.json', {cache: $templateCache}).then(function(response) {
+ * self.userComments = response.data;
* });
* self.explicitlyTrustedHtml = $sce.trustAsHtml(
* '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
@@ -28882,12 +23753,12 @@ function $SceDelegateProvider() {
* <file name="protractor.js" type="protractor">
* describe('SCE doc demo', function() {
* it('should sanitize untrusted values', function() {
- * expect(element.all(by.css('.htmlComment')).first().getInnerHtml())
+ * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML'))
* .toBe('<span>Is <i>anyone</i> reading this?</span>');
* });
*
* it('should NOT sanitize explicitly trusted values', function() {
- * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe(
+ * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe(
* '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
* 'sanitization.&quot;">Hover over this text.</span>');
* });
@@ -28903,20 +23774,20 @@ function $SceDelegateProvider() {
* for little coding overhead. It will be much harder to take an SCE disabled application and
* either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE
* for cases where you have a lot of existing code that was written before SCE was introduced and
- * you're migrating them a module at a time.
+ * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if
+ * you are writing a library, you will cause security bugs applications using it.
*
* That said, here's how you can completely disable SCE:
*
* ```
* angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
* // Completely disable SCE. For demonstration purposes only!
- * // Do not use in new projects.
+ * // Do not use in new projects or libraries.
* $sceProvider.enabled(false);
* });
* ```
*
*/
-/* jshint maxlen: 100 */
function $SceProvider() {
var enabled = true;
@@ -28926,8 +23797,8 @@ function $SceProvider() {
* @name $sceProvider#enabled
* @kind function
*
- * @param {boolean=} value If provided, then enables/disables SCE.
- * @return {boolean} true if SCE is enabled, false otherwise.
+ * @param {boolean=} value If provided, then enables/disables SCE application-wide.
+ * @return {boolean} True if SCE is enabled, false otherwise.
*
* @description
* Enables/disables SCE and returns the current value.
@@ -28958,7 +23829,7 @@ function $SceProvider() {
* such a value.
*
* - getTrusted(contextEnum, value)
- * This function should return the a value that is safe to use in the context specified by
+ * This function should return the value that is safe to use in the context specified by
* contextEnum or throw and exception otherwise.
*
* NOTE: This contract deliberately does NOT state that values returned by trustAs() must be
@@ -28981,13 +23852,14 @@ function $SceProvider() {
* getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value)
* will also succeed.
*
- * Inheritance happens to capture this in a natural way. In some future, we
- * may not use inheritance anymore. That is OK because no code outside of
- * sce.js and sceSpecs.js would need to be aware of this detail.
+ * Inheritance happens to capture this in a natural way. In some future, we may not use
+ * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to
+ * be aware of this detail.
*/
this.$get = ['$parse', '$sceDelegate', function(
$parse, $sceDelegate) {
+ // Support: IE 9-11 only
// Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow
// the "expression(javascript expression)" syntax which is insecure.
if (enabled && msie < 8) {
@@ -29004,8 +23876,8 @@ function $SceProvider() {
* @name $sce#isEnabled
* @kind function
*
- * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you
- * have to do it at module config time on {@link ng.$sceProvider $sceProvider}.
+ * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you
+ * have to do it at module config time on {@link ng.$sceProvider $sceProvider}.
*
* @description
* Returns a boolean indicating if SCE is enabled.
@@ -29027,19 +23899,19 @@ function $SceProvider() {
* @name $sce#parseAs
*
* @description
- * Converts Angular {@link guide/expression expression} into a function. This is like {@link
+ * Converts AngularJS {@link guide/expression expression} into a function. This is like {@link
* ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it
* wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*,
* *result*)}
*
- * @param {string} type The kind of SCE context in which this result will be used.
+ * @param {string} type The SCE context in which this result will be used.
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
sce.parseAs = function sceParseAs(type, expr) {
var parsed = $parse(expr);
@@ -29057,18 +23929,18 @@ function $SceProvider() {
* @name $sce#trustAs
*
* @description
- * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such,
- * returns an object that is trusted by angular for use in specified strict contextual
- * escaping contexts (such as ng-bind-html, ng-include, any src attribute
- * interpolation, any dom event binding attribute interpolation such as for onclick, etc.)
- * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual
- * escaping.
+ * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a
+ * wrapped object that represents your value, and the trust you have in its safety for the given
+ * context. AngularJS can then use that value as-is in bindings of the specified secure context.
+ * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute
+ * interpolations. See {@link ng.$sce $sce} for strict contextual escaping.
*
- * @param {string} type The kind of context in which this value is safe for use. e.g. url,
- * resourceUrl, html, js and css.
- * @param {*} value The value that that should be considered trusted/safe.
- * @returns {*} A value that can be used to stand in for the provided `value` in places
- * where Angular expects a $sce.trustAs() return value.
+ * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`,
+ * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`.
+ *
+ * @param {*} value The value that that should be considered trusted.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in the context you specified.
*/
/**
@@ -29079,11 +23951,23 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsHtml(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml
- * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.HTML` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.HTML` context (like `ng-bind-html`).
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAsCss
+ *
+ * @description
+ * Shorthand method. `$sce.trustAsCss(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`}
+ *
+ * @param {*} value The value to mark as trusted for `$sce.CSS` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant
+ * of your `value` in `$sce.CSS` context. This context is currently unused, so there are
+ * almost no reasons to use this function so far.
*/
/**
@@ -29094,11 +23978,10 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsUrl(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl
- * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.URL` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.URL` context. That context is currently unused, so there are almost no reasons
+ * to use this function so far.
*/
/**
@@ -29109,11 +23992,10 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsResourceUrl(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl
- * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the return
- * value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute
+ * bindings, ...)
*/
/**
@@ -29124,11 +24006,10 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsJs(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs
- * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.JS` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to
+ * use this function so far.
*/
/**
@@ -29137,16 +24018,17 @@ function $SceProvider() {
*
* @description
* Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such,
- * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the
- * originally supplied value if the queried context type is a supertype of the created type.
- * If this condition isn't satisfied, throws an exception.
+ * takes any input, and either returns a value that's safe to use in the specified context,
+ * or throws an exception. This function is aware of trusted values created by the `trustAs`
+ * function and its shorthands, and when contexts are appropriate, returns the unwrapped value
+ * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a
+ * safe value (e.g., no sanitization is available or possible.)
*
- * @param {string} type The kind of context in which this value is to be used.
- * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`}
- * call.
- * @returns {*} The value the was originally provided to
- * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context.
- * Otherwise, throws an exception.
+ * @param {string} type The context in which this value is to be used.
+ * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs
+ * `$sce.trustAs`} call, or anything else (which will not be considered trusted.)
+ * @return {*} A version of the value that's safe to use in the given context, or throws an
+ * exception if this is impossible.
*/
/**
@@ -29158,7 +24040,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)`
*/
/**
@@ -29170,7 +24052,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)`
*/
/**
@@ -29182,7 +24064,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.URL, value)`
*/
/**
@@ -29194,7 +24076,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`}
*
* @param {*} value The value to pass to `$sceDelegate.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)`
*/
/**
@@ -29206,7 +24088,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.JS, value)`
*/
/**
@@ -29218,12 +24100,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -29235,12 +24117,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -29252,12 +24134,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -29269,12 +24151,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -29286,12 +24168,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
// Shorthand delegations.
@@ -29301,13 +24183,13 @@ function $SceProvider() {
forEach(SCE_CONTEXTS, function(enumValue, name) {
var lName = lowercase(name);
- sce[camelCase("parse_as_" + lName)] = function(expr) {
+ sce[snakeToCamel('parse_as_' + lName)] = function(expr) {
return parse(enumValue, expr);
};
- sce[camelCase("get_trusted_" + lName)] = function(value) {
+ sce[snakeToCamel('get_trusted_' + lName)] = function(value) {
return getTrusted(enumValue, value);
};
- sce[camelCase("trust_as_" + lName)] = function(value) {
+ sce[snakeToCamel('trust_as_' + lName)] = function(value) {
return trustAs(enumValue, value);
};
});
@@ -29316,12 +24198,15 @@ function $SceProvider() {
}];
}
+/* exported $SnifferProvider */
+
/**
* !!! This is an undocumented "private" service !!!
*
* @name $sniffer
* @requires $window
* @requires $document
+ * @this
*
* @property {boolean} history Does the browser support html5 history api ?
* @property {boolean} transitions Does the browser support CSS transition events ?
@@ -29333,41 +24218,32 @@ function $SceProvider() {
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
- // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by
- // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index)
- isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime,
+ // Chrome Packaged Apps are not allowed to access `history.pushState`.
+ // If not sandboxed, they can be detected by the presence of `chrome.app.runtime`
+ // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by
+ // the presence of an extension runtime ID and the absence of other Chrome runtime APIs
+ // (see https://developer.chrome.com/apps/manifest/sandbox).
+ // (NW.js apps have access to Chrome APIs, but do support `history`.)
+ isNw = $window.nw && $window.nw.process,
+ isChromePackagedApp =
+ !isNw &&
+ $window.chrome &&
+ ($window.chrome.app && $window.chrome.app.runtime ||
+ !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id),
hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
android =
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
document = $document[0] || {},
- vendorPrefix,
- vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/,
bodyStyle = document.body && document.body.style,
transitions = false,
- animations = false,
- match;
+ animations = false;
if (bodyStyle) {
- for (var prop in bodyStyle) {
- if (match = vendorRegex.exec(prop)) {
- vendorPrefix = match[0];
- vendorPrefix = vendorPrefix[0].toUpperCase() + vendorPrefix.substr(1);
- break;
- }
- }
-
- if (!vendorPrefix) {
- vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit';
- }
-
- transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle));
- animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
-
- if (android && (!transitions || !animations)) {
- transitions = isString(bodyStyle.webkitTransition);
- animations = isString(bodyStyle.webkitAnimation);
- }
+ // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x
+ // Mentioned browsers need a -webkit- prefix for transitions & animations.
+ transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle);
+ animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle);
}
@@ -29380,16 +24256,15 @@ function $SnifferProvider() {
// older webkit browser (533.9) on Boxee box has exactly the same problem as Android has
// 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: !!(hasHistoryPushState && !(android < 4) && !boxee),
- // jshint +W018
hasEvent: function(event) {
+ // Support: IE 9-11 only
// 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.
// IE10+ implements 'input' event but it erroneously fires under various situations,
// e.g. when placeholder changes, or a form is focused.
- if (event === 'input' && msie <= 11) return false;
+ if (event === 'input' && msie) return false;
if (isUndefined(eventSupport[event])) {
var divElm = document.createElement('div');
@@ -29399,7 +24274,6 @@ function $SnifferProvider() {
return eventSupport[event];
},
csp: csp(),
- vendorPrefix: vendorPrefix,
transitions: transitions,
animations: animations,
android: android
@@ -29407,11 +24281,13 @@ function $SnifferProvider() {
}];
}
-var $templateRequestMinErr = minErr('$compile');
+var $templateRequestMinErr = minErr('$templateRequest');
/**
* @ngdoc provider
* @name $templateRequestProvider
+ * @this
+ *
* @description
* Used to configure the options passed to the {@link $http} service when making a template request.
*
@@ -29458,6 +24334,12 @@ function $TemplateRequestProvider() {
* If you want to pass custom options to the `$http` service, such as setting the Accept header you
* can configure this via {@link $templateRequestProvider#httpOptions}.
*
+ * `$templateRequest` is used internally by {@link $compile}, {@link ngRoute.$route}, and directives such
+ * as {@link ngInclude} to download and cache templates.
+ *
+ * 3rd party modules should use `$templateRequest` if their services or directives are loading
+ * templates.
+ *
* @param {string|TrustedResourceUrl} tpl The HTTP request template URL
* @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
*
@@ -29465,57 +24347,63 @@ function $TemplateRequestProvider() {
*
* @property {number} totalPendingRequests total amount of pending template requests being downloaded.
*/
- this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) {
+ this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce',
+ function($exceptionHandler, $templateCache, $http, $q, $sce) {
- function handleRequestFn(tpl, ignoreRequestError) {
- handleRequestFn.totalPendingRequests++;
+ function handleRequestFn(tpl, ignoreRequestError) {
+ handleRequestFn.totalPendingRequests++;
- // We consider the template cache holds only trusted templates, so
- // there's no need to go through whitelisting again for keys that already
- // are included in there. This also makes Angular accept any script
- // directive, no matter its name. However, we still need to unwrap trusted
- // types.
- if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
- tpl = $sce.getTrustedResourceUrl(tpl);
- }
+ // We consider the template cache holds only trusted templates, so
+ // there's no need to go through adding the template again to the trusted
+ // resources for keys that already are included in there. This also makes
+ // AngularJS accept any script directive, no matter its name. However, we
+ // still need to unwrap trusted types.
+ if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
+ tpl = $sce.getTrustedResourceUrl(tpl);
+ }
- var transformResponse = $http.defaults && $http.defaults.transformResponse;
+ var transformResponse = $http.defaults && $http.defaults.transformResponse;
- if (isArray(transformResponse)) {
- transformResponse = transformResponse.filter(function(transformer) {
- return transformer !== defaultHttpResponseTransform;
- });
- } else if (transformResponse === defaultHttpResponseTransform) {
- transformResponse = null;
- }
+ if (isArray(transformResponse)) {
+ transformResponse = transformResponse.filter(function(transformer) {
+ return transformer !== defaultHttpResponseTransform;
+ });
+ } else if (transformResponse === defaultHttpResponseTransform) {
+ transformResponse = null;
+ }
+
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
+ .finally(function() {
+ handleRequestFn.totalPendingRequests--;
+ })
+ .then(function(response) {
+ return $templateCache.put(tpl, response.data);
+ }, handleError);
+
+ function handleError(resp) {
+ if (!ignoreRequestError) {
+ resp = $templateRequestMinErr('tpload',
+ 'Failed to load template: {0} (HTTP status: {1} {2})',
+ tpl, resp.status, resp.statusText);
+
+ $exceptionHandler(resp);
+ }
- return $http.get(tpl, extend({
- cache: $templateCache,
- transformResponse: transformResponse
- }, httpOptions))
- ['finally'](function() {
- handleRequestFn.totalPendingRequests--;
- })
- .then(function(response) {
- $templateCache.put(tpl, response.data);
- return response.data;
- }, handleError);
-
- function handleError(resp) {
- if (!ignoreRequestError) {
- throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
- tpl, resp.status, resp.statusText);
+ return $q.reject(resp);
}
- return $q.reject(resp);
}
- }
- handleRequestFn.totalPendingRequests = 0;
+ handleRequestFn.totalPendingRequests = 0;
- return handleRequestFn;
- }];
+ return handleRequestFn;
+ }
+ ];
}
+/** @this */
function $$TestabilityProvider() {
this.$get = ['$rootScope', '$browser', '$location',
function($rootScope, $browser, $location) {
@@ -29554,7 +24442,7 @@ function $$TestabilityProvider() {
matches.push(binding);
}
} else {
- if (bindingName.indexOf(expression) != -1) {
+ if (bindingName.indexOf(expression) !== -1) {
matches.push(binding);
}
}
@@ -29619,7 +24507,15 @@ function $$TestabilityProvider() {
* @name $$testability#whenStable
*
* @description
- * Calls the callback when $timeout and $http requests are completed.
+ * Calls the callback when all pending tasks are completed.
+ *
+ * Types of tasks waited for include:
+ * - Pending timeouts (via {@link $timeout}).
+ * - Pending HTTP requests (via {@link $http}).
+ * - In-progress route transitions (via {@link $route}).
+ * - Pending tasks scheduled via {@link $rootScope#$applyAsync}.
+ * - Pending tasks scheduled via {@link $rootScope#$evalAsync}.
+ * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
*
* @param {function} callback
*/
@@ -29631,6 +24527,9 @@ function $$TestabilityProvider() {
}];
}
+var $timeoutMinErr = minErr('$timeout');
+
+/** @this */
function $TimeoutProvider() {
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
function($rootScope, $browser, $q, $$q, $exceptionHandler) {
@@ -29638,35 +24537,35 @@ function $TimeoutProvider() {
var deferreds = {};
- /**
- * @ngdoc service
- * @name $timeout
- *
- * @description
- * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
- * block and delegates any exceptions to
- * {@link ng.$exceptionHandler $exceptionHandler} service.
- *
- * The return value of calling `$timeout` is a promise, which will be resolved when
- * the delay has passed and the timeout function, if provided, is executed.
- *
- * To cancel a timeout request, call `$timeout.cancel(promise)`.
- *
- * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
- * synchronously flush the queue of deferred functions.
- *
- * If you only want a promise that will be resolved after some specified delay
- * then you can call `$timeout` without the `fn` function.
- *
- * @param {function()=} fn A function, whose execution should be delayed.
- * @param {number=} [delay=0] Delay in milliseconds.
- * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
- * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @param {...*=} Pass additional parameters to the executed function.
- * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
- * will be resolved with the return value of the `fn` function.
- *
- */
+ /**
+ * @ngdoc service
+ * @name $timeout
+ *
+ * @description
+ * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
+ * block and delegates any exceptions to
+ * {@link ng.$exceptionHandler $exceptionHandler} service.
+ *
+ * The return value of calling `$timeout` is a promise, which will be resolved when
+ * the delay has passed and the timeout function, if provided, is executed.
+ *
+ * To cancel a timeout request, call `$timeout.cancel(promise)`.
+ *
+ * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
+ * synchronously flush the queue of deferred functions.
+ *
+ * If you only want a promise that will be resolved after some specified delay
+ * then you can call `$timeout` without the `fn` function.
+ *
+ * @param {function()=} fn A function, whose execution should be delayed.
+ * @param {number=} [delay=0] Delay in milliseconds.
+ * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
+ * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @param {...*=} Pass additional parameters to the executed function.
+ * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
+ * will be resolved with the return value of the `fn` function.
+ *
+ */
function timeout(fn, delay, invokeApply) {
if (!isFunction(fn)) {
invokeApply = delay;
@@ -29686,13 +24585,12 @@ function $TimeoutProvider() {
} catch (e) {
deferred.reject(e);
$exceptionHandler(e);
- }
- finally {
+ } finally {
delete deferreds[promise.$$timeoutId];
}
if (!skipApply) $rootScope.$apply();
- }, delay);
+ }, delay, '$timeout');
promise.$$timeoutId = timeoutId;
deferreds[timeoutId] = deferred;
@@ -29701,25 +24599,37 @@ function $TimeoutProvider() {
}
- /**
- * @ngdoc method
- * @name $timeout#cancel
- *
- * @description
- * Cancels a task associated with the `promise`. As a result of this, the promise will be
- * resolved with a rejection.
- *
- * @param {Promise=} promise Promise returned by the `$timeout` function.
- * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
- * canceled.
- */
+ /**
+ * @ngdoc method
+ * @name $timeout#cancel
+ *
+ * @description
+ * Cancels a task associated with the `promise`. As a result of this, the promise will be
+ * resolved with a rejection.
+ *
+ * @param {Promise=} promise Promise returned by the `$timeout` function.
+ * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
+ * canceled.
+ */
timeout.cancel = function(promise) {
- if (promise && promise.$$timeoutId in deferreds) {
- deferreds[promise.$$timeoutId].reject('canceled');
- delete deferreds[promise.$$timeoutId];
- return $browser.defer.cancel(promise.$$timeoutId);
+ if (!promise) return false;
+
+ if (!promise.hasOwnProperty('$$timeoutId')) {
+ throw $timeoutMinErr('badprom',
+ '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.');
}
- return false;
+
+ if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false;
+
+ var id = promise.$$timeoutId;
+ var deferred = deferreds[id];
+
+ // Timeout cancels should not report an unhandled promise.
+ markQExceptionHandled(deferred.promise);
+ deferred.reject('canceled');
+ delete deferreds[id];
+
+ return $browser.defer.cancel(id);
};
return timeout;
@@ -29733,9 +24643,16 @@ function $TimeoutProvider() {
// doesn't know about mocked locations and resolves URLs to the real document - which is
// exactly the behavior needed here. There is little value is mocking these out for this
// service.
-var urlParsingNode = window.document.createElement("a");
+var urlParsingNode = window.document.createElement('a');
var originUrl = urlResolve(window.location.href);
+var baseUrlParsingNode;
+urlParsingNode.href = 'http://[::1]';
+
+// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview)
+// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets
+// when parsed out of an anchor element.
+var ipv6InBrackets = urlParsingNode.hostname === '[::1]';
/**
*
@@ -29746,7 +24663,7 @@ var originUrl = urlResolve(window.location.href);
* URL will be resolved into an absolute URL in the context of the application document.
* Parsing means that the anchor node's host, hostname, protocol, port, pathname and related
* properties are all populated to reflect the normalized URL. This approach has wide
- * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See
+ * compatibility - Safari 1+, Mozilla 1+ etc. See
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
*
* Implementation Notes for IE
@@ -29766,42 +24683,51 @@ var originUrl = urlResolve(window.location.href);
* http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
*
* @kind function
- * @param {string} url The URL to be parsed.
+ * @param {string|object} url The URL to be parsed. If `url` is not a string, it will be returned
+ * unchanged.
* @description Normalizes and parses a URL.
* @returns {object} Returns the normalized URL as a dictionary.
*
- * | member name | Description |
- * |---------------|----------------|
+ * | member name | Description |
+ * |---------------|------------------------------------------------------------------------|
* | href | A normalized version of the provided URL if it was not an absolute URL |
- * | protocol | The protocol including the trailing colon |
+ * | protocol | The protocol without the trailing colon |
* | host | The host and port (if the port is non-default) of the normalizedUrl |
* | search | The search params, minus the question mark |
- * | hash | The hash string, minus the hash symbol
- * | hostname | The hostname
- * | port | The port, without ":"
- * | pathname | The pathname, beginning with "/"
+ * | hash | The hash string, minus the hash symbol |
+ * | hostname | The hostname |
+ * | port | The port, without ":" |
+ * | pathname | The pathname, beginning with "/" |
*
*/
function urlResolve(url) {
+ if (!isString(url)) return url;
+
var href = url;
+ // Support: IE 9-11 only
if (msie) {
// Normalize before parse. Refer Implementation Notes on why this is
// done in two steps on IE.
- urlParsingNode.setAttribute("href", href);
+ urlParsingNode.setAttribute('href', href);
href = urlParsingNode.href;
}
urlParsingNode.setAttribute('href', href);
- // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
+ var hostname = urlParsingNode.hostname;
+
+ if (!ipv6InBrackets && hostname.indexOf(':') > -1) {
+ hostname = '[' + hostname + ']';
+ }
+
return {
href: urlParsingNode.href,
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
host: urlParsingNode.host,
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
- hostname: urlParsingNode.hostname,
+ hostname: hostname,
port: urlParsingNode.port,
pathname: (urlParsingNode.pathname.charAt(0) === '/')
? urlParsingNode.pathname
@@ -29810,26 +24736,107 @@ function urlResolve(url) {
}
/**
- * Parse a request URL and determine whether this is a same-origin request as the application document.
+ * Parse a request URL and determine whether this is a same-origin request as the application
+ * document.
*
* @param {string|object} requestUrl The url of the request as a string that will be resolved
* or a parsed URL object.
* @returns {boolean} Whether the request is for the same origin as the application document.
*/
function urlIsSameOrigin(requestUrl) {
- var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
- return (parsed.protocol === originUrl.protocol &&
- parsed.host === originUrl.host);
+ return urlsAreSameOrigin(requestUrl, originUrl);
+}
+
+/**
+ * Parse a request URL and determine whether it is same-origin as the current document base URL.
+ *
+ * Note: The base URL is usually the same as the document location (`location.href`) but can
+ * be overriden by using the `<base>` tag.
+ *
+ * @param {string|object} requestUrl The url of the request as a string that will be resolved
+ * or a parsed URL object.
+ * @returns {boolean} Whether the URL is same-origin as the document base URL.
+ */
+function urlIsSameOriginAsBaseUrl(requestUrl) {
+ return urlsAreSameOrigin(requestUrl, getBaseUrl());
+}
+
+/**
+ * Create a function that can check a URL's origin against a list of allowed/trusted origins.
+ * The current location's origin is implicitly trusted.
+ *
+ * @param {string[]} trustedOriginUrls - A list of URLs (strings), whose origins are trusted.
+ *
+ * @returns {Function} - A function that receives a URL (string or parsed URL object) and returns
+ * whether it is of an allowed origin.
+ */
+function urlIsAllowedOriginFactory(trustedOriginUrls) {
+ var parsedAllowedOriginUrls = [originUrl].concat(trustedOriginUrls.map(urlResolve));
+
+ /**
+ * Check whether the specified URL (string or parsed URL object) has an origin that is allowed
+ * based on a list of trusted-origin URLs. The current location's origin is implicitly
+ * trusted.
+ *
+ * @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be
+ * resolved or a parsed URL object).
+ *
+ * @returns {boolean} - Whether the specified URL is of an allowed origin.
+ */
+ return function urlIsAllowedOrigin(requestUrl) {
+ var parsedUrl = urlResolve(requestUrl);
+ return parsedAllowedOriginUrls.some(urlsAreSameOrigin.bind(null, parsedUrl));
+ };
+}
+
+/**
+ * Determine if two URLs share the same origin.
+ *
+ * @param {string|Object} url1 - First URL to compare as a string or a normalized URL in the form of
+ * a dictionary object returned by `urlResolve()`.
+ * @param {string|object} url2 - Second URL to compare as a string or a normalized URL in the form
+ * of a dictionary object returned by `urlResolve()`.
+ *
+ * @returns {boolean} - True if both URLs have the same origin, and false otherwise.
+ */
+function urlsAreSameOrigin(url1, url2) {
+ url1 = urlResolve(url1);
+ url2 = urlResolve(url2);
+
+ return (url1.protocol === url2.protocol &&
+ url1.host === url2.host);
+}
+
+/**
+ * Returns the current document base URL.
+ * @returns {string}
+ */
+function getBaseUrl() {
+ if (window.document.baseURI) {
+ return window.document.baseURI;
+ }
+
+ // `document.baseURI` is available everywhere except IE
+ if (!baseUrlParsingNode) {
+ baseUrlParsingNode = window.document.createElement('a');
+ baseUrlParsingNode.href = '.';
+
+ // Work-around for IE bug described in Implementation Notes. The fix in `urlResolve()` is not
+ // suitable here because we need to track changes to the base URL.
+ baseUrlParsingNode = baseUrlParsingNode.cloneNode(false);
+ }
+ return baseUrlParsingNode.href;
}
/**
* @ngdoc service
* @name $window
+ * @this
*
* @description
* A reference to the browser's `window` object. While `window`
* is globally available in JavaScript, it causes testability problems, because
- * it is a global variable. In angular we always refer to it through the
+ * it is a global variable. In AngularJS we always refer to it through the
* `$window` service, so it may be overridden, removed or mocked for testing.
*
* Expressions, like the one defined for the `ngClick` directive in the example
@@ -29838,7 +24845,7 @@ function urlIsSameOrigin(requestUrl) {
* expression.
*
* @example
- <example module="windowExample">
+ <example module="windowExample" name="window-service">
<file name="index.html">
<script>
angular.module('windowExample', [])
@@ -29881,6 +24888,14 @@ function $$CookieReader($document) {
var lastCookies = {};
var lastCookieString = '';
+ function safeGetCookie(rawDocument) {
+ try {
+ return rawDocument.cookie || '';
+ } catch (e) {
+ return '';
+ }
+ }
+
function safeDecodeURIComponent(str) {
try {
return decodeURIComponent(str);
@@ -29891,7 +24906,7 @@ function $$CookieReader($document) {
return function() {
var cookieArray, cookie, i, index, name;
- var currentCookieString = rawDocument.cookie || '';
+ var currentCookieString = safeGetCookie(rawDocument);
if (currentCookieString !== lastCookieString) {
lastCookieString = currentCookieString;
@@ -29918,6 +24933,7 @@ function $$CookieReader($document) {
$$CookieReader.$inject = ['$document'];
+/** @this */
function $$CookieReaderProvider() {
this.$get = $$CookieReader;
}
@@ -29943,7 +24959,7 @@ function $$CookieReaderProvider() {
* annotated with dependencies and is responsible for creating a filter function.
*
* <div class="alert alert-warning">
- * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
* Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
@@ -29986,8 +25002,8 @@ function $$CookieReaderProvider() {
* ```
*
*
- * For more information about how angular filters work, and how to create your own filters, see
- * {@link guide/filter Filters} in the Angular Developer Guide.
+ * For more information about how AngularJS filters work, and how to create your own filters, see
+ * {@link guide/filter Filters} in the AngularJS Developer Guide.
*/
/**
@@ -29997,9 +25013,15 @@ function $$CookieReaderProvider() {
* @description
* Filters are used for formatting data displayed to the user.
*
+ * They can be used in view templates, controllers or services. AngularJS comes
+ * with a collection of [built-in filters](api/ng/filter), but it is easy to
+ * define your own as well.
+ *
* The general syntax in templates is as follows:
*
- * {{ expression [| filter_name[:parameter_value] ... ] }}
+ * ```html
+ * {{ expression [| filter_name[:parameter_value] ... ] }}
+ * ```
*
* @param {String} name Name of the filter function to retrieve
* @return {Function} the filter function
@@ -30022,6 +25044,7 @@ function $$CookieReaderProvider() {
</example>
*/
$FilterProvider.$inject = ['$provide'];
+/** @this */
function $FilterProvider($provide) {
var suffix = 'Filter';
@@ -30032,7 +25055,7 @@ function $FilterProvider($provide) {
* the keys are the filter names and the values are the filter factories.
*
* <div class="alert alert-warning">
- * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
* Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
@@ -30071,7 +25094,7 @@ function $FilterProvider($provide) {
lowercaseFilter: false,
numberFilter: false,
orderByFilter: false,
- uppercaseFilter: false,
+ uppercaseFilter: false
*/
register('currency', currencyFilter);
@@ -30094,6 +25117,9 @@ function $FilterProvider($provide) {
* Selects a subset of items from `array` and returns it as a new array.
*
* @param {Array} array The source array.
+ * <div class="alert alert-info">
+ * **Note**: If the array contains objects that reference themselves, filtering is not possible.
+ * </div>
* @param {string|Object|function()} expression The predicate to be used for selecting items from
* `array`.
*
@@ -30126,9 +25152,10 @@ function $FilterProvider($provide) {
*
* The final result is an array of those elements that the predicate returned true for.
*
- * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in
- * determining if the expected value (from the filter expression) and actual value (from
- * the object in the array) should be considered a match.
+ * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in
+ * determining if values retrieved using `expression` (when it is not a function) should be
+ * considered a match based on the expected value (from the filter expression) and actual
+ * value (from the object in the array).
*
* Can be one of:
*
@@ -30139,17 +25166,18 @@ function $FilterProvider($provide) {
* - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`.
* This is essentially strict comparison of expected and actual.
*
- * - `false|undefined`: A short hand for a function which will look for a substring match in case
- * insensitive way.
+ * - `false`: A short hand for a function which will look for a substring match in a case
+ * insensitive way. Primitive values are converted to strings. Objects are not compared against
+ * primitives, unless they have a custom `toString` method (e.g. `Date` objects).
+ *
*
- * Primitive values are converted to strings. Objects are not compared against primitives,
- * unless they have a custom `toString` method (e.g. `Date` objects).
+ * Defaults to `false`.
*
- * @param {string=} anyPropertyKey The special property name that matches against any property.
+ * @param {string} [anyPropertyKey] The special property name that matches against any property.
* By default `$`.
*
* @example
- <example>
+ <example name="filter-filter">
<file name="index.html">
<div ng-init="friends = [{name:'John', phone:'555-1276'},
{name:'Mary', phone:'800-BIG-MARY'},
@@ -30241,9 +25269,8 @@ function filterFilter() {
case 'number':
case 'string':
matchAgainstAnyProp = true;
- //jshint -W086
+ // falls through
case 'object':
- //jshint +W086
predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp);
break;
default:
@@ -30311,7 +25338,10 @@ function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstA
var key;
if (matchAgainstAnyProp) {
for (key in actual) {
- if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
+ // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined
+ // See: https://github.com/angular/angular.js/issues/15644
+ if (key.charAt && (key.charAt(0) !== '$') &&
+ deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
return true;
}
}
@@ -30333,7 +25363,6 @@ function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstA
} else {
return comparator(actual, expected);
}
- break;
case 'function':
return false;
default:
@@ -30366,7 +25395,7 @@ var ZERO_CHAR = '0';
*
*
* @example
- <example module="currencyExample">
+ <example module="currencyExample" name="currency-filter">
<file name="index.html">
<script>
angular.module('currencyExample', [])
@@ -30377,7 +25406,7 @@ var ZERO_CHAR = '0';
<div ng-controller="ExampleController">
<input type="number" ng-model="amount" aria-label="amount"> <br>
default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br>
- custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span>
+ custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span><br>
no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span>
</div>
</file>
@@ -30388,7 +25417,7 @@ var ZERO_CHAR = '0';
expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235');
});
it('should update', function() {
- if (browser.params.browser == 'safari') {
+ if (browser.params.browser === 'safari') {
// Safari does not understand the minus key. See
// https://github.com/angular/protractor/issues/481
return;
@@ -30414,11 +25443,14 @@ function currencyFilter($locale) {
fractionSize = formats.PATTERNS[1].maxFrac;
}
+ // If the currency symbol is empty, trim whitespace around the symbol
+ var currencySymbolRe = !currencySymbol ? /\s*\u00A4\s*/g : /\u00A4/g;
+
// if null or undefined pass it through
return (amount == null)
? amount
: formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize).
- replace(/\u00A4/g, currencySymbol);
+ replace(currencySymbolRe, currencySymbol);
};
}
@@ -30444,7 +25476,7 @@ function currencyFilter($locale) {
* include "," group separators after each third digit).
*
* @example
- <example module="numberFilterExample">
+ <example module="numberFilterExample" name="number-filter">
<file name="index.html">
<script>
angular.module('numberFilterExample', [])
@@ -30523,16 +25555,16 @@ function parse(numStr) {
}
// Count the number of leading zeros.
- for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */}
+ for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ }
- if (i == (zeros = numStr.length)) {
+ if (i === (zeros = numStr.length)) {
// The digits are all zero.
digits = [0];
numberOfIntegerDigits = 1;
} else {
// Count the number of trailing zeros
zeros--;
- while (numStr.charAt(zeros) == ZERO_CHAR) zeros--;
+ while (numStr.charAt(zeros) === ZERO_CHAR) zeros--;
// Trailing zeros are insignificant so ignore them
numberOfIntegerDigits -= i;
@@ -30724,7 +25756,7 @@ function dateGetter(name, size, offset, trim, negWrap) {
if (offset > 0 || value > -offset) {
value += offset;
}
- if (value === 0 && offset == -12) value = 12;
+ if (value === 0 && offset === -12) value = 12;
return padNumber(value, size, trim, negWrap);
};
}
@@ -30741,7 +25773,7 @@ function dateStrGetter(name, shortForm, standAlone) {
function timeZoneGetter(date, formats, offset) {
var zone = -1 * offset;
- var paddedZone = (zone >= 0) ? "+" : "";
+ var paddedZone = (zone >= 0) ? '+' : '';
paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
padNumber(Math.abs(zone % 60), 2);
@@ -30821,8 +25853,8 @@ var DATE_FORMATS = {
GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
- NUMBER_STRING = /^\-?\d+$/;
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,
+ NUMBER_STRING = /^-?\d+$/;
/**
* @ngdoc filter
@@ -30880,6 +25912,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+
* `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence
* (e.g. `"h 'o''clock'"`).
*
+ * Any other characters in the `format` string will be output as-is.
+ *
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
* number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its
* shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is
@@ -30893,7 +25927,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
- <example>
+ <example name="filter-date">
<file name="index.html">
<span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>:
<span>{{1288323623006 | date:'medium'}}</span><br>
@@ -30909,7 +25943,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+
expect(element(by.binding("1288323623006 | date:'medium'")).getText()).
toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()).
- toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/);
+ toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/);
expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
@@ -30926,7 +25960,7 @@ function dateFilter($locale) {
// 1 2 3 4 5 6 7 8 9 10 11
function jsonStringToDate(string) {
var match;
- if (match = string.match(R_ISO8601_STR)) {
+ if ((match = string.match(R_ISO8601_STR))) {
var date = new Date(0),
tzHour = 0,
tzMin = 0,
@@ -30987,7 +26021,7 @@ function dateFilter($locale) {
forEach(parts, function(value) {
fn = DATE_FORMATS[value];
text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
- : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
+ : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
});
return text;
@@ -31012,15 +26046,15 @@ function dateFilter($locale) {
*
*
* @example
- <example>
+ <example name="filter-json">
<file name="index.html">
<pre id="default-spacing">{{ {'name':'value'} | json }}</pre>
<pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre>
</file>
<file name="protractor.js" type="protractor">
it('should jsonify filtered objects', function() {
- expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
- expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
+ expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/);
+ expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/);
});
</file>
</example>
@@ -31042,6 +26076,9 @@ function jsonFilter() {
* @kind function
* @description
* Converts string to lowercase.
+ *
+ * See the {@link ng.uppercase uppercase filter documentation} for a functionally identical example.
+ *
* @see angular.lowercase
*/
var lowercaseFilter = valueFn(lowercase);
@@ -31053,7 +26090,23 @@ var lowercaseFilter = valueFn(lowercase);
* @kind function
* @description
* Converts string to uppercase.
- * @see angular.uppercase
+ * @example
+ <example module="uppercaseFilterExample" name="filter-uppercase">
+ <file name="index.html">
+ <script>
+ angular.module('uppercaseFilterExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.title = 'This is a title';
+ }]);
+ </script>
+ <div ng-controller="ExampleController">
+ <!-- This title should be formatted normally -->
+ <h1>{{title}}</h1>
+ <!-- This title should be capitalized -->
+ <h1>{{title | uppercase}}</h1>
+ </div>
+ </file>
+ </example>
*/
var uppercaseFilter = valueFn(uppercase);
@@ -31081,7 +26134,7 @@ var uppercaseFilter = valueFn(uppercase);
* less than `limit` elements.
*
* @example
- <example module="limitToExample">
+ <example module="limitToExample" name="limit-to-filter">
<file name="index.html">
<script>
angular.module('limitToExample', [])
@@ -31163,7 +26216,7 @@ function limitToFilter() {
} else {
limit = toInt(limit);
}
- if (isNaN(limit)) return input;
+ if (isNumberNaN(limit)) return input;
if (isNumber(input)) input = input.toString();
if (!isArrayLike(input)) return input;
@@ -31205,7 +26258,7 @@ function sliceFn(input, begin, end) {
* String, etc).
*
* The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker
- * for the preceeding one. The `expression` is evaluated against each item and the output is used
+ * for the preceding one. The `expression` is evaluated against each item and the output is used
* for comparing with other items.
*
* You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in
@@ -31229,6 +26282,7 @@ function sliceFn(input, begin, end) {
* index: ...
* }
* ```
+ * **Note:** `null` values use `'null'` as their type.
* 2. The comparator function is used to sort the items, based on the derived values, types and
* indices.
*
@@ -31242,6 +26296,9 @@ function sliceFn(input, begin, end) {
* dummy predicate that returns the item's index as `value`.
* (If you are using a custom comparator, make sure it can handle this predicate as well.)
*
+ * If a custom comparator still can't distinguish between two items, then they will be sorted based
+ * on their index using the built-in comparator.
+ *
* Finally, in an attempt to simplify things, if a predicate returns an object as the extracted
* value for an item, `orderBy` will try to convert that object to a primitive value, before passing
* it to the comparator. The following rules govern the conversion:
@@ -31260,11 +26317,15 @@ function sliceFn(input, begin, end) {
*
* The default, built-in comparator should be sufficient for most usecases. In short, it compares
* numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
- * using their index in the original collection, and sorts values of different types by type.
+ * using their index in the original collection, sorts values of different types by type and puts
+ * `undefined` and `null` values at the end of the sorted list.
*
* More specifically, it follows these steps to determine the relative order of items:
*
- * 1. If the compared values are of different types, compare the types themselves alphabetically.
+ * 1. If the compared values are of different types:
+ * - If one of the values is undefined, consider it "greater than" the other.
+ * - Else if one of the values is null, consider it "greater than" the other.
+ * - Else compare the types themselves alphabetically.
* 2. If both values are of type `string`, compare them alphabetically in a case- and
* locale-insensitive way.
* 3. If both values are objects, compare their indices instead.
@@ -31275,6 +26336,10 @@ function sliceFn(input, begin, end) {
*
* **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
* saved as numbers and not strings.
+ * **Note:** For the purpose of sorting, `null` and `undefined` are considered "greater than"
+ * any other value (with undefined "greater than" null). This effectively means that `null`
+ * and `undefined` values end up at the end of a list sorted in ascending order.
+ * **Note:** `null` values use `'null'` as their type to be able to distinguish them from objects.
*
* @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
* @param {(Function|string|Array.<Function|string>)=} expression - A predicate (or list of
@@ -31284,7 +26349,7 @@ function sliceFn(input, begin, end) {
*
* - `Function`: A getter function. This function will be called with each item as argument and
* the return value will be used for sorting.
- * - `string`: An Angular expression. This expression will be evaluated against each item and the
+ * - `string`: An AngularJS expression. This expression will be evaluated against each item and the
* result will be used for sorting. For example, use `'label'` to sort by a property called
* `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label`
* property.<br />
@@ -31785,7 +26850,7 @@ function orderByFilter($parse) {
}
}
- return compare(v1.tieBreaker, v2.tieBreaker) * descending;
+ return (compare(v1.tieBreaker, v2.tieBreaker) || defaultCompare(v1.tieBreaker, v2.tieBreaker)) * descending;
}
};
@@ -31796,8 +26861,8 @@ function orderByFilter($parse) {
if (isFunction(predicate)) {
get = predicate;
} else if (isString(predicate)) {
- if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
- descending = predicate.charAt(0) == '-' ? -1 : 1;
+ if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) {
+ descending = predicate.charAt(0) === '-' ? -1 : 1;
predicate = predicate.substring(1);
}
if (predicate !== '') {
@@ -31841,8 +26906,7 @@ function orderByFilter($parse) {
function getPredicateValue(value, index) {
var type = typeof value;
if (value === null) {
- type = 'string';
- value = 'null';
+ type = 'null';
} else if (type === 'object') {
value = objectValue(value);
}
@@ -31873,7 +26937,11 @@ function orderByFilter($parse) {
result = value1 < value2 ? -1 : 1;
}
} else {
- result = type1 < type2 ? -1 : 1;
+ result = (type1 === 'undefined') ? 1 :
+ (type2 === 'undefined') ? -1 :
+ (type1 === 'null') ? 1 :
+ (type2 === 'null') ? -1 :
+ (type1 < type2) ? -1 : 1;
}
return result;
@@ -31896,12 +26964,10 @@ function ngDirective(directive) {
* @restrict E
*
* @description
- * Modifies the default behavior of the html A tag so that the default action is prevented when
+ * Modifies the default behavior of the html a tag so that the default action is prevented when
* the href attribute is empty.
*
- * This change permits the easy creation of action links with the `ngClick` directive
- * without changing the location or causing page reloads, e.g.:
- * `<a href="" ng-click="list.addItem()">Add Item</a>`
+ * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive.
*/
var htmlAnchorDirective = valueFn({
restrict: 'E',
@@ -31932,10 +26998,10 @@ var htmlAnchorDirective = valueFn({
* @priority 99
*
* @description
- * Using Angular markup like `{{hash}}` in an href attribute will
+ * Using AngularJS markup like `{{hash}}` in an href attribute will
* make the link go to the wrong URL if the user clicks it before
- * Angular has a chance to replace the `{{hash}}` markup with its
- * value. Until Angular replaces the markup the link will be broken
+ * AngularJS has a chance to replace the `{{hash}}` markup with its
+ * value. Until AngularJS replaces the markup the link will be broken
* and will most likely return a 404 error. The `ngHref` directive
* solves this problem.
*
@@ -31955,7 +27021,7 @@ var htmlAnchorDirective = valueFn({
* @example
* This example shows various combinations of `href`, `ng-href` and `ng-click` attributes
* in links and their different behaviors:
- <example>
+ <example name="ng-href">
<file name="index.html">
<input ng-model="value" /><br />
<a id="link-1" href ng-click="value = 1">link 1</a> (link, don't reload)<br />
@@ -31983,7 +27049,7 @@ var htmlAnchorDirective = valueFn({
element(by.id('link-3')).click();
- // At this point, we navigate away from an Angular page, so we need
+ // At this point, we navigate away from an AngularJS page, so we need
// to use browser.driver to get the base webdriver.
browser.wait(function() {
@@ -32012,7 +27078,7 @@ var htmlAnchorDirective = valueFn({
element(by.id('link-6')).click();
- // At this point, we navigate away from an Angular page, so we need
+ // At this point, we navigate away from an AngularJS page, so we need
// to use browser.driver to get the base webdriver.
browser.wait(function() {
return browser.driver.getCurrentUrl().then(function(url) {
@@ -32031,9 +27097,9 @@ var htmlAnchorDirective = valueFn({
* @priority 99
*
* @description
- * Using Angular markup like `{{hash}}` in a `src` attribute doesn't
+ * Using AngularJS markup like `{{hash}}` in a `src` attribute doesn't
* work right: The browser will fetch from the URL with the literal
- * text `{{hash}}` until Angular replaces the expression inside
+ * text `{{hash}}` until AngularJS replaces the expression inside
* `{{hash}}`. The `ngSrc` directive solves this problem.
*
* The buggy way to write it:
@@ -32057,9 +27123,9 @@ var htmlAnchorDirective = valueFn({
* @priority 99
*
* @description
- * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't
+ * Using AngularJS markup like `{{hash}}` in a `srcset` attribute doesn't
* work right: The browser will fetch from the URL with the literal
- * text `{{hash}}` until Angular replaces the expression inside
+ * text `{{hash}}` until AngularJS replaces the expression inside
* `{{hash}}`. The `ngSrcset` directive solves this problem.
*
* The buggy way to write it:
@@ -32084,14 +27150,15 @@ var htmlAnchorDirective = valueFn({
*
* @description
*
- * This directive sets the `disabled` attribute on the element if the
+ * This directive sets the `disabled` attribute on the element (typically a form control,
+ * e.g. `input`, `button`, `select` etc.) if the
* {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
*
* A special directive is necessary because we cannot use interpolation inside the `disabled`
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
- <example>
+ <example name="ng-disabled">
<file name="index.html">
<label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/>
<button ng-model="button" ng-disabled="checked">Button</button>
@@ -32105,7 +27172,6 @@ var htmlAnchorDirective = valueFn({
</file>
</example>
*
- * @element INPUT
* @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
* then the `disabled` attribute will be set on the element
*/
@@ -32127,16 +27193,16 @@ var htmlAnchorDirective = valueFn({
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
- <example>
+ <example name="ng-checked">
<file name="index.html">
- <label>Check me to check both: <input type="checkbox" ng-model="master"></label><br/>
- <input id="checkSlave" type="checkbox" ng-checked="master" aria-label="Slave input">
+ <label>Check me to check both: <input type="checkbox" ng-model="leader"></label><br/>
+ <input id="checkFollower" type="checkbox" ng-checked="leader" aria-label="Follower input">
</file>
<file name="protractor.js" type="protractor">
it('should check both checkBoxes', function() {
- expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy();
- element(by.model('master')).click();
- expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy();
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeFalsy();
+ element(by.model('leader')).click();
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeTruthy();
});
</file>
</example>
@@ -32163,10 +27229,10 @@ var htmlAnchorDirective = valueFn({
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
- <example>
+ <example name="ng-readonly">
<file name="index.html">
<label>Check me to make text readonly: <input type="checkbox" ng-model="checked"></label><br/>
- <input type="text" ng-readonly="checked" value="I'm Angular" aria-label="Readonly field" />
+ <input type="text" ng-readonly="checked" value="I'm AngularJS" aria-label="Readonly field" />
</file>
<file name="protractor.js" type="protractor">
it('should toggle readonly attr', function() {
@@ -32204,7 +27270,7 @@ var htmlAnchorDirective = valueFn({
* </div>
*
* @example
- <example>
+ <example name="ng-selected">
<file name="index.html">
<label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/>
<select aria-label="ngSelected demo">
@@ -32241,15 +27307,20 @@ var htmlAnchorDirective = valueFn({
*
* ## A note about browser compatibility
*
- * Edge, Firefox, and Internet Explorer do not support the `details` element, it is
+ * Internet Explorer and Edge do not support the `details` element, it is
* recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead.
*
* @example
- <example>
+ <example name="ng-open">
<file name="index.html">
- <label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/>
+ <label>Toggle details: <input type="checkbox" ng-model="open"></label><br/>
<details id="details" ng-open="open">
- <summary>Show/Hide me</summary>
+ <summary>List</summary>
+ <ul>
+ <li>Apple</li>
+ <li>Orange</li>
+ <li>Durian</li>
+ </ul>
</details>
</file>
<file name="protractor.js" type="protractor">
@@ -32271,7 +27342,7 @@ var ngAttributeAliasDirectives = {};
// boolean attrs are evaluated
forEach(BOOLEAN_ATTR, function(propName, attrName) {
// binding to multiple is not supported
- if (propName == "multiple") return;
+ if (propName === 'multiple') return;
function defaultLinkFn(scope, element, attr) {
scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
@@ -32308,10 +27379,10 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
link: function(scope, element, attr) {
//special case ngPattern when a literal regular expression value
//is used as the expression (this way we don't have to watch anything).
- if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") {
+ if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') {
var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
if (match) {
- attr.$set("ngPattern", new RegExp(match[1], match[2]));
+ attr.$set('ngPattern', new RegExp(match[1], match[2]));
return;
}
}
@@ -32327,7 +27398,7 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
// ng-src, ng-srcset, ng-href are interpolated
forEach(['src', 'srcset', 'href'], function(attrName) {
var normalized = directiveNormalize('ng-' + attrName);
- ngAttributeAliasDirectives[normalized] = function() {
+ ngAttributeAliasDirectives[normalized] = ['$sce', function($sce) {
return {
priority: 99, // it needs to run after the attributes are interpolated
link: function(scope, element, attr) {
@@ -32341,6 +27412,10 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
propName = null;
}
+ // We need to sanitize the url at least once, in case it is a constant
+ // non-interpolated attribute.
+ attr.$set(normalized, $sce.getTrustedMediaUrl(attr[normalized]));
+
attr.$observe(normalized, function(value) {
if (!value) {
if (attrName === 'href') {
@@ -32351,28 +27426,32 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
attr.$set(name, value);
- // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
+ // Support: IE 9-11 only
+ // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
- // we use attr[attrName] value since $set can sanitize the url.
+ // We use attr[attrName] value since $set might have sanitized the url.
if (msie && propName) element.prop(propName, attr[name]);
});
}
};
- };
+ }];
});
-/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
+/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS
*/
var nullFormCtrl = {
$addControl: noop,
+ $getControls: valueFn([]),
$$renameControl: nullFormRenameControl,
$removeControl: noop,
$setValidity: noop,
$setDirty: noop,
$setPristine: noop,
- $setSubmitted: noop
+ $setSubmitted: noop,
+ $$setSubmitted: noop
},
+PENDING_CLASS = 'ng-pending',
SUBMITTED_CLASS = 'ng-submitted';
function nullFormRenameControl(control, name) {
@@ -32387,17 +27466,23 @@ function nullFormRenameControl(control, name) {
* @property {boolean} $dirty True if user has already interacted with the form.
* @property {boolean} $valid True if all of the containing forms and controls are valid.
* @property {boolean} $invalid True if at least one containing control or form is invalid.
- * @property {boolean} $pending True if at least one containing control or form is pending.
* @property {boolean} $submitted True if user has submitted the form even if its invalid.
*
- * @property {Object} $error Is an object hash, containing references to controls or
- * forms with failing validators, where:
+ * @property {Object} $pending An object hash, containing references to controls or forms with
+ * pending validators, where:
+ *
+ * - keys are validations tokens (error names).
+ * - values are arrays of controls or forms that have a pending validator for the given error name.
+ *
+ * See {@link form.FormController#$error $error} for a list of built-in validation tokens.
+ *
+ * @property {Object} $error An object hash, containing references to controls or forms with failing
+ * validators, where:
*
* - keys are validation tokens (error names),
- * - values are arrays of controls or forms that have a failing validator for given error name.
+ * - values are arrays of controls or forms that have a failing validator for the given error name.
*
* Built-in validation tokens:
- *
* - `email`
* - `max`
* - `maxlength`
@@ -32423,22 +27508,28 @@ function nullFormRenameControl(control, name) {
*/
//asks for $scope to fool the BC controller module
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
-function FormController(element, attrs, $scope, $animate, $interpolate) {
- var form = this,
- controls = [];
+function FormController($element, $attrs, $scope, $animate, $interpolate) {
+ this.$$controls = [];
// init state
- form.$error = {};
- form.$$success = {};
- form.$pending = undefined;
- form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
- form.$dirty = false;
- form.$pristine = true;
- form.$valid = true;
- form.$invalid = false;
- form.$submitted = false;
- form.$$parentForm = nullFormCtrl;
+ this.$error = {};
+ this.$$success = {};
+ this.$pending = undefined;
+ this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$submitted = false;
+ this.$$parentForm = nullFormCtrl;
+
+ this.$$element = $element;
+ this.$$animate = $animate;
+ setupValidity(this);
+}
+
+FormController.prototype = {
/**
* @ngdoc method
* @name form.FormController#$rollbackViewValue
@@ -32450,11 +27541,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is typically needed by the reset button of
* a form that uses `ng-model-options` to pend updates.
*/
- form.$rollbackViewValue = function() {
- forEach(controls, function(control) {
+ $rollbackViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$rollbackViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -32467,11 +27558,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
- form.$commitViewValue = function() {
- forEach(controls, function(control) {
+ $commitViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$commitViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -32494,29 +27585,53 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* For example, if an input control is added that is already `$dirty` and has `$error` properties,
* calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
- form.$addControl = function(control) {
+ $addControl: function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
// and not added to the scope. Now we throw an error.
assertNotHasOwnProperty(control.$name, 'input');
- controls.push(control);
+ this.$$controls.push(control);
if (control.$name) {
- form[control.$name] = control;
+ this[control.$name] = control;
}
- control.$$parentForm = form;
- };
+ control.$$parentForm = this;
+ },
+
+ /**
+ * @ngdoc method
+ * @name form.FormController#$getControls
+ * @returns {Array} the controls that are currently part of this form
+ *
+ * @description
+ * This method returns a **shallow copy** of the controls that are currently part of this form.
+ * The controls can be instances of {@link form.FormController `FormController`}
+ * ({@link ngForm "child-forms"}) and of {@link ngModel.NgModelController `NgModelController`}.
+ * If you need access to the controls of child-forms, you have to call `$getControls()`
+ * recursively on them.
+ * This can be used for example to iterate over all controls to validate them.
+ *
+ * The controls can be accessed normally, but adding to, or removing controls from the array has
+ * no effect on the form. Instead, use {@link form.FormController#$addControl `$addControl()`} and
+ * {@link form.FormController#$removeControl `$removeControl()`} for this use-case.
+ * Likewise, adding a control to, or removing a control from the form is not reflected
+ * in the shallow copy. That means you should get a fresh copy from `$getControls()` every time
+ * you need access to the controls.
+ */
+ $getControls: function() {
+ return shallowCopy(this.$$controls);
+ },
// Private API: rename a form control
- form.$$renameControl = function(control, newName) {
+ $$renameControl: function(control, newName) {
var oldName = control.$name;
- if (form[oldName] === control) {
- delete form[oldName];
+ if (this[oldName] === control) {
+ delete this[oldName];
}
- form[newName] = control;
+ this[newName] = control;
control.$name = newName;
- };
+ },
/**
* @ngdoc method
@@ -32534,60 +27649,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* different from case to case. For example, removing the only `$dirty` control from a form may or
* may not mean that the form is still `$dirty`.
*/
- form.$removeControl = function(control) {
- if (control.$name && form[control.$name] === control) {
- delete form[control.$name];
- }
- forEach(form.$pending, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$error, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$$success, function(value, name) {
- form.$setValidity(name, null, control);
- });
-
- arrayRemove(controls, control);
+ $removeControl: function(control) {
+ if (control.$name && this[control.$name] === control) {
+ delete this[control.$name];
+ }
+ forEach(this.$pending, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$error, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$$success, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+
+ arrayRemove(this.$$controls, control);
control.$$parentForm = nullFormCtrl;
- };
-
-
- /**
- * @ngdoc method
- * @name form.FormController#$setValidity
- *
- * @description
- * Sets the validity of a form control.
- *
- * This method will also propagate to parent forms.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: element,
- set: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- object[property] = [controller];
- } else {
- var index = list.indexOf(controller);
- if (index === -1) {
- list.push(controller);
- }
- }
- },
- unset: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- return;
- }
- arrayRemove(list, controller);
- if (list.length === 0) {
- delete object[property];
- }
- },
- $animate: $animate
- });
+ },
/**
* @ngdoc method
@@ -32599,13 +27680,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* This method can be called to add the 'ng-dirty' class and set the form to a dirty
* state (ng-dirty class). This method will also propagate to parent forms.
*/
- form.$setDirty = function() {
- $animate.removeClass(element, PRISTINE_CLASS);
- $animate.addClass(element, DIRTY_CLASS);
- form.$dirty = true;
- form.$pristine = false;
- form.$$parentForm.$setDirty();
- };
+ $setDirty: function() {
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$parentForm.$setDirty();
+ },
/**
* @ngdoc method
@@ -32614,22 +27695,24 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* @description
* Sets the form to its pristine state.
*
- * This method can be called to remove the 'ng-dirty' class and set the form to its pristine
- * state (ng-pristine class). This method will also propagate to all the controls contained
- * in this form.
+ * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes
+ * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted`
+ * state to false.
+ *
+ * This method will also propagate to all the controls contained in this form.
*
* Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
* saving or resetting it.
*/
- form.$setPristine = function() {
- $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
- form.$dirty = false;
- form.$pristine = true;
- form.$submitted = false;
- forEach(controls, function(control) {
+ $setPristine: function() {
+ this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$submitted = false;
+ forEach(this.$$controls, function(control) {
control.$setPristine();
});
- };
+ },
/**
* @ngdoc method
@@ -32644,25 +27727,87 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* Setting a form controls back to their untouched state is often useful when setting the form
* back to its pristine state.
*/
- form.$setUntouched = function() {
- forEach(controls, function(control) {
+ $setUntouched: function() {
+ forEach(this.$$controls, function(control) {
control.$setUntouched();
});
- };
+ },
/**
* @ngdoc method
* @name form.FormController#$setSubmitted
*
* @description
- * Sets the form to its submitted state.
+ * Sets the form to its `$submitted` state. This will also set `$submitted` on all child and
+ * parent forms of the form.
*/
- form.$setSubmitted = function() {
- $animate.addClass(element, SUBMITTED_CLASS);
- form.$submitted = true;
- form.$$parentForm.$setSubmitted();
- };
-}
+ $setSubmitted: function() {
+ var rootForm = this;
+ while (rootForm.$$parentForm && (rootForm.$$parentForm !== nullFormCtrl)) {
+ rootForm = rootForm.$$parentForm;
+ }
+ rootForm.$$setSubmitted();
+ },
+
+ $$setSubmitted: function() {
+ this.$$animate.addClass(this.$$element, SUBMITTED_CLASS);
+ this.$submitted = true;
+ forEach(this.$$controls, function(control) {
+ if (control.$$setSubmitted) {
+ control.$$setSubmitted();
+ }
+ });
+ }
+};
+
+/**
+ * @ngdoc method
+ * @name form.FormController#$setValidity
+ *
+ * @description
+ * Change the validity state of the form, and notify the parent form (if any).
+ *
+ * Application developers will rarely need to call this method directly. It is used internally, by
+ * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a
+ * control's validity state to the parent `FormController`.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be
+ * assigned to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` (for
+ * unfulfilled `$asyncValidators`), so that it is available for data-binding. The
+ * `validationErrorKey` should be in camelCase and will get converted into dash-case for
+ * class name. Example: `myError` will result in `ng-valid-my-error` and
+ * `ng-invalid-my-error` classes and can be bound to as `{{ someForm.$error.myError }}`.
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending
+ * (undefined), or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by AngularJS when validators do not run because of parse errors and when
+ * `$asyncValidators` do not run because any of the `$validators` failed.
+ * @param {NgModelController | FormController} controller - The controller whose validity state is
+ * triggering the change.
+ */
+addSetValidityMethod({
+ clazz: FormController,
+ set: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ object[property] = [controller];
+ } else {
+ var index = list.indexOf(controller);
+ if (index === -1) {
+ list.push(controller);
+ }
+ }
+ },
+ unset: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ return;
+ }
+ arrayRemove(list, controller);
+ if (list.length === 0) {
+ delete object[property];
+ }
+ }
+});
/**
* @ngdoc directive
@@ -32670,16 +27815,21 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* @restrict EAC
*
* @description
- * Nestable alias of {@link ng.directive:form `form`} directive. HTML
- * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a
- * sub-group of controls needs to be determined.
+ * Helper directive that makes it possible to create control groups inside a
+ * {@link ng.directive:form `form`} directive.
+ * These "child forms" can be used, for example, to determine the validity of a sub-group of
+ * controls.
*
- * Note: the purpose of `ngForm` is to group controls,
- * but not to be a replacement for the `<form>` tag with all of its capabilities
- * (e.g. posting to the server, ...).
+ * <div class="alert alert-danger">
+ * **Note**: `ngForm` cannot be used as a replacement for `<form>`, because it lacks its
+ * [built-in HTML functionality](https://html.spec.whatwg.org/#the-form-element).
+ * Specifically, you cannot submit `ngForm` like a `<form>` tag. That means,
+ * you cannot send data to the server with `ngForm`, or integrate it with
+ * {@link ng.directive:ngSubmit `ngSubmit`}.
+ * </div>
*
- * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into
- * related scope, under this name.
+ * @param {string=} ngForm|name Name of the form. If specified, the form controller will
+ * be published into the related scope, under this name.
*
*/
@@ -32695,15 +27845,15 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* If the `name` attribute is specified, the form controller is published onto the current scope under
* this name.
*
- * # Alias: {@link ng.directive:ngForm `ngForm`}
+ * ## Alias: {@link ng.directive:ngForm `ngForm`}
*
- * In Angular, forms can be nested. This means that the outer form is valid when all of the child
+ * In AngularJS, forms can be nested. This means that the outer form is valid when all of the child
* forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so
- * Angular provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to
+ * AngularJS provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to
* `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group
* of controls needs to be determined.
*
- * # CSS classes
+ * ## CSS classes
* - `ng-valid` is set if the form is valid.
* - `ng-invalid` is set if the form is invalid.
* - `ng-pending` is set if the form is pending.
@@ -32714,14 +27864,14 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
*
- * # Submitting a form and preventing the default action
+ * ## Submitting a form and preventing the default action
*
- * Since the role of forms in client-side Angular applications is different than in classical
+ * Since the role of forms in client-side AngularJS applications is different than in classical
* roundtrip apps, it is desirable for the browser not to translate the form submission into a full
* page reload that sends the data to the server. Instead some javascript logic should be triggered
* to handle the form submission in an application-specific way.
*
- * For this reason, Angular prevents the default action (form submission to the server) unless the
+ * For this reason, AngularJS prevents the default action (form submission to the server) unless the
* `<form>` element has an `action` attribute specified.
*
* You can use one of the following two ways to specify what javascript method should be called when
@@ -32747,8 +27897,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
* to have access to the updated model.
*
- * ## Animation Hooks
- *
+ * @animations
* Animations in ngForm are triggered when any of the associated CSS classes are added and removed.
* These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any
* other validations that are performed within the form. Animations in ngForm are similar to how
@@ -32772,7 +27921,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* </pre>
*
* @example
- <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample">
+ <example name="ng-form" deps="angular-animate.js" animations="true" fixBase="true" module="formExample">
<file name="index.html">
<script>
angular.module('formExample', [])
@@ -32859,13 +28008,13 @@ var formDirectiveFactory = function(isNgForm) {
event.preventDefault();
};
- addEventListenerFn(formElement[0], 'submit', handleFormSubmission);
+ formElement[0].addEventListener('submit', handleFormSubmission);
// unregister the preventDefault listener so that we don't not leak memory but in a
// way that will achieve the prevention of the default action.
formElement.on('$destroy', function() {
$timeout(function() {
- removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
+ formElement[0].removeEventListener('submit', handleFormSubmission);
}, 0, false);
});
}
@@ -32910,13 +28059,12 @@ var formDirectiveFactory = function(isNgForm) {
var formDirective = formDirectiveFactory();
var ngFormDirective = formDirectiveFactory(true);
-/* global VALID_CLASS: false,
+/* global
+ VALID_CLASS: false,
INVALID_CLASS: false,
PRISTINE_CLASS: false,
DIRTY_CLASS: false,
- UNTOUCHED_CLASS: false,
- TOUCHED_CLASS: false,
- ngModelMinErr: false,
+ ngModelMinErr: false
*/
// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
@@ -32932,12 +28080,11 @@ var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-
// 7. Path
// 8. Query
// 9. Fragment
-// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999
-var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
-/* jshint maxlen:220 */
-var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
-/* jshint maxlen:200 */
-var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
+// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999
+var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
+// eslint-disable-next-line max-len
+var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
+var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/;
var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/;
@@ -32957,10 +28104,10 @@ var inputType = {
* @name input[text]
*
* @description
- * Standard HTML text input with angular data binding, inherited by most of the `input` elements.
+ * Standard HTML text input with AngularJS data binding, inherited by most of the `input` elements.
*
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Adds `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -32975,7 +28122,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -32983,9 +28130,9 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
* This parameter is ignored for input[type=password] controls, which will never trim the
* input.
*
@@ -33059,13 +28206,13 @@ var inputType = {
* modern browsers do not yet support this input type, it is important to provide cues to users on the
* expected input format via a placeholder or label.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
* valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute
@@ -33083,7 +28230,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33117,7 +28264,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM-dd"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -33162,13 +28308,17 @@ var inputType = {
* the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * The format of the displayed time can be adjusted with the
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
+ * and `timeStripZeroSeconds`.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
@@ -33186,7 +28336,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33220,7 +28370,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -33266,13 +28415,18 @@ var inputType = {
* local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
* Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
- * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions}. By default,
+ * this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * The format of the displayed time can be adjusted with the
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
+ * and `timeStripZeroSeconds`.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
@@ -33290,7 +28444,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33324,7 +28478,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "HH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -33369,13 +28522,17 @@ var inputType = {
* the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* week format (yyyy-W##), for example: `2013-W02`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
+ * The value of the resulting Date object will be set to Thursday at 00:00:00 of the requested week,
+ * due to ISO-8601 week numbering standards. Information on ISO's system for numbering the weeks of the
+ * year can be found at: https://en.wikipedia.org/wiki/ISO_8601#Week_dates
+ *
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
@@ -33393,7 +28550,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33429,7 +28586,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-Www"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -33472,7 +28628,7 @@ var inputType = {
* the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* month format (yyyy-MM), for example: `2009-01`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
* If the model is not set to the first of the month, the next view to model update will set it
* to the first of the month.
@@ -33480,7 +28636,7 @@ var inputType = {
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
@@ -33499,7 +28655,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33533,7 +28689,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -33578,12 +28733,16 @@ var inputType = {
* error if not a valid number.
*
* <div class="alert alert-warning">
- * The model must always be of type `number` otherwise Angular will throw an error.
+ * The model must always be of type `number` otherwise AngularJS will throw an error.
* Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt}
* error docs for more information and an example of how to convert your model if necessary.
* </div>
*
- * ## Issues with HTML5 constraint validation
+ *
+ *
+ * @knownIssue
+ *
+ * ### HTML5 constraint validation and `allowInvalid`
*
* In browsers that follow the
* [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29),
@@ -33592,11 +28751,32 @@ var inputType = {
* which means the view / model values in `ngModel` and subsequently the scope value
* will also be an empty string.
*
+ * @knownIssue
+ *
+ * ### Large numbers and `step` validation
+ *
+ * The `step` validation will not work correctly for very large numbers (e.g. 9999999999) due to
+ * Javascript's arithmetic limitations. If you need to handle large numbers, purpose-built
+ * libraries (e.g. https://github.com/MikeMcl/big.js/), can be included into AngularJS by
+ * {@link guide/forms#modifying-built-in-validators overwriting the validators}
+ * for `number` and / or `step`, or by {@link guide/forms#custom-validation applying custom validators}
+ * to an `input[text]` element. The source for `input[number]` type can be used as a starting
+ * point for both implementations.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * Can be interpolated.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * Can be interpolated.
+ * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint.
+ * Can be interpolated.
+ * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint,
+ * but does not trigger HTML5 native validation. Takes an expression.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -33610,7 +28790,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -33618,7 +28798,7 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33693,7 +28873,7 @@ var inputType = {
* the built-in validators (see the {@link guide/forms Forms guide})
* </div>
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -33708,7 +28888,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -33716,7 +28896,7 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33788,11 +28968,13 @@ var inputType = {
*
* <div class="alert alert-warning">
* **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex
- * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can
- * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide})
+ * used in Chromium, which may not fulfill your app's requirements.
+ * If you need stricter (e.g. requiring a top-level domain), or more relaxed validation
+ * (e.g. allowing IPv6 address literals) you can use `ng-pattern` or
+ * modify the built-in validators (see the {@link guide/forms Forms guide}).
* </div>
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -33807,7 +28989,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -33815,7 +28997,7 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -33883,14 +29065,41 @@ var inputType = {
* @description
* HTML radio button.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * **Note:**<br>
+ * All inputs controlled by {@link ngModel ngModel} (including those of type `radio`) will use the
+ * value of their `name` attribute to determine the property under which their
+ * {@link ngModel.NgModelController NgModelController} will be published on the parent
+ * {@link form.FormController FormController}. Thus, if you use the same `name` for multiple
+ * inputs of a form (e.g. a group of radio inputs), only _one_ `NgModelController` will be
+ * published on the parent `FormController` under that name. The rest of the controllers will
+ * continue to work as expected, but you won't be able to access them as properties on the parent
+ * `FormController`.
+ *
+ * <div class="alert alert-info">
+ * <p>
+ * In plain HTML forms, the `name` attribute is used to identify groups of radio inputs, so
+ * that the browser can manage their state (checked/unchecked) based on the state of other
+ * inputs in the same group.
+ * </p>
+ * <p>
+ * In AngularJS forms, this is not necessary. The input's state will be updated based on the
+ * value of the underlying model data.
+ * </p>
+ * </div>
+ *
+ * <div class="alert alert-success">
+ * If you omit the `name` attribute on a radio input, `ngModel` will automatically assign it a
+ * unique name.
+ * </div>
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string} value The value to which the `ngModel` expression should be set when selected.
* Note that `value` only supports `string` values, i.e. the scope model needs to be a string,
* too. Use `ngValue` if you need complex models (`number`, `object`, ...).
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio
+ * @param {string} ngValue AngularJS expression to which `ngModel` will be be set when the radio
* is selected. Should be used instead of the `value` attribute if you need
* a non-string `ngModel` (`boolean`, `array`, ...).
*
@@ -33928,19 +29137,140 @@ var inputType = {
</file>
<file name="protractor.js" type="protractor">
it('should change state', function() {
+ var inputs = element.all(by.model('color.name'));
var color = element(by.binding('color.name'));
expect(color.getText()).toContain('blue');
- element.all(by.model('color.name')).get(0).click();
-
+ inputs.get(0).click();
expect(color.getText()).toContain('red');
+
+ inputs.get(1).click();
+ expect(color.getText()).toContain('green');
});
</file>
</example>
*/
'radio': radioInputType,
+ /**
+ * @ngdoc input
+ * @name input[range]
+ *
+ * @description
+ * Native range input with validation and transformation.
+ *
+ * The model for the range input must always be a `Number`.
+ *
+ * IE9 and other browsers that do not support the `range` type fall back
+ * to a text input without any default values for `min`, `max` and `step`. Model binding,
+ * validation and number parsing are nevertheless supported.
+ *
+ * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]`
+ * in a way that never allows the input to hold an invalid value. That means:
+ * - any non-numerical value is set to `(max + min) / 2`.
+ * - any numerical value that is less than the current min val, or greater than the current max val
+ * is set to the min / max val respectively.
+ * - additionally, the current `step` is respected, so the nearest value that satisfies a step
+ * is used.
+ *
+ * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range))
+ * for more info.
+ *
+ * This has the following consequences for AngularJS:
+ *
+ * Since the element value should always reflect the current model value, a range input
+ * will set the bound ngModel expression to the value that the browser has set for the
+ * input element. For example, in the following input `<input type="range" ng-model="model.value">`,
+ * if the application sets `model.value = null`, the browser will set the input to `'50'`.
+ * AngularJS will then set the model to `50`, to prevent input and model value being out of sync.
+ *
+ * That means the model for range will immediately be set to `50` after `ngModel` has been
+ * initialized. It also means a range input can never have the required error.
+ *
+ * This does not only affect changes to the model value, but also to the values of the `min`,
+ * `max`, and `step` attributes. When these change in a way that will cause the browser to modify
+ * the input value, AngularJS will also update the model value.
+ *
+ * Automatic value adjustment also means that a range input element can never have the `required`,
+ * `min`, or `max` errors.
+ *
+ * However, `step` is currently only fully implemented by Firefox. Other browsers have problems
+ * when the step value changes dynamically - they do not adjust the element value correctly, but
+ * instead may set the `stepMismatch` error. If that's the case, the AngularJS will set the `step`
+ * error on the input, and set the model to `undefined`.
+ *
+ * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do
+ * not set the `min` and `max` attributes, which means that the browser won't automatically adjust
+ * the input value based on their values, and will always assume min = 0, max = 100, and step = 1.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation to ensure that the value entered is greater
+ * than `min`. Can be interpolated.
+ * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`.
+ * Can be interpolated.
+ * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
+ * Can be interpolated.
+ * @param {expression=} ngChange AngularJS expression to be executed when the ngModel value changes due
+ * to user interaction with the input element.
+ * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the
+ * element. **Note** : `ngChecked` should not be used alongside `ngModel`.
+ * Checkout {@link ng.directive:ngChecked ngChecked} for usage.
+ *
+ * @example
+ <example name="range-input-directive" module="rangeExample">
+ <file name="index.html">
+ <script>
+ angular.module('rangeExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.value = 75;
+ $scope.min = 10;
+ $scope.max = 90;
+ }]);
+ </script>
+ <form name="myForm" ng-controller="ExampleController">
+
+ Model as range: <input type="range" name="range" ng-model="value" min="{{min}}" max="{{max}}">
+ <hr>
+ Model as number: <input type="number" ng-model="value"><br>
+ Min: <input type="number" ng-model="min"><br>
+ Max: <input type="number" ng-model="max"><br>
+ value = <code>{{value}}</code><br/>
+ myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
+ myForm.range.$error = <code>{{myForm.range.$error}}</code>
+ </form>
+ </file>
+ </example>
+
+ * ## Range Input with ngMin & ngMax attributes
+
+ * @example
+ <example name="range-input-directive-ng" module="rangeExample">
+ <file name="index.html">
+ <script>
+ angular.module('rangeExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.value = 75;
+ $scope.min = 10;
+ $scope.max = 90;
+ }]);
+ </script>
+ <form name="myForm" ng-controller="ExampleController">
+ Model as range: <input type="range" name="range" ng-model="value" ng-min="min" ng-max="max">
+ <hr>
+ Model as number: <input type="number" ng-model="value"><br>
+ Min: <input type="number" ng-model="min"><br>
+ Max: <input type="number" ng-model="max"><br>
+ value = <code>{{value}}</code><br/>
+ myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
+ myForm.range.$error = <code>{{myForm.range.$error}}</code>
+ </form>
+ </file>
+ </example>
+
+ */
+ 'range': rangeInputType,
/**
* @ngdoc input
@@ -33949,11 +29279,11 @@ var inputType = {
* @description
* HTML checkbox.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {expression=} ngTrueValue The value to which the expression should be set when selected.
* @param {expression=} ngFalseValue The value to which the expression should be set when not selected.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -34020,7 +29350,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var type = lowercase(element[0].type);
- // In composition mode, users are still inputing intermediate text buffer,
+ // In composition mode, users are still inputting intermediate text buffer,
// hold the listener until composition is done.
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
if (!$sniffer.android) {
@@ -34030,6 +29360,16 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
composing = true;
});
+ // Support: IE9+
+ element.on('compositionupdate', function(ev) {
+ // End composition when ev.data is empty string on 'compositionupdate' event.
+ // When the input de-focusses (e.g. by clicking away), IE triggers 'compositionupdate'
+ // instead of 'compositionend'.
+ if (isUndefined(ev.data) || ev.data === '') {
+ composing = false;
+ }
+ });
+
element.on('compositionend', function() {
composing = false;
listener();
@@ -34078,7 +29418,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
};
- element.on('keydown', function(event) {
+ element.on('keydown', /** @this */ function(event) {
var key = event.keyCode;
// ignore
@@ -34088,9 +29428,9 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
deferListener(event, this, this.value);
});
- // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
+ // if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it
if ($sniffer.hasEvent('paste')) {
- element.on('paste cut', deferListener);
+ element.on('paste cut drop', deferListener);
}
}
@@ -34103,7 +29443,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// For these event types, when native validators are present and the browser supports the type,
// check for validity changes on various DOM events.
if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
- element.on(PARTIAL_VALIDATION_EVENTS, function(ev) {
+ element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) {
if (!timeout) {
var validity = this[VALIDITY_STATE_PROPERTY];
var origBadInput = validity.badInput;
@@ -34160,7 +29500,7 @@ function weekParser(isoWeek, existingDate) {
}
function createDateParser(regexp, mapping) {
- return function(iso, date) {
+ return function(iso, previousDate) {
var parts, map;
if (isDate(iso)) {
@@ -34171,7 +29511,7 @@ function createDateParser(regexp, mapping) {
// When a date is JSON'ified to wraps itself inside of an extra
// set of double quotes. This makes the date parsing code unable
// to match the date string and parse it as a date.
- if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') {
+ if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') {
iso = iso.substring(1, iso.length - 1);
}
if (ISO_DATE_REGEXP.test(iso)) {
@@ -34182,15 +29522,15 @@ function createDateParser(regexp, mapping) {
if (parts) {
parts.shift();
- if (date) {
+ if (previousDate) {
map = {
- yyyy: date.getFullYear(),
- MM: date.getMonth() + 1,
- dd: date.getDate(),
- HH: date.getHours(),
- mm: date.getMinutes(),
- ss: date.getSeconds(),
- sss: date.getMilliseconds() / 1000
+ yyyy: previousDate.getFullYear(),
+ MM: previousDate.getMonth() + 1,
+ dd: previousDate.getDate(),
+ HH: previousDate.getHours(),
+ mm: previousDate.getMinutes(),
+ ss: previousDate.getSeconds(),
+ sss: previousDate.getMilliseconds() / 1000
};
} else {
map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 };
@@ -34201,7 +29541,15 @@ function createDateParser(regexp, mapping) {
map[mapping[index]] = +part;
}
});
- return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
+
+ var date = new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
+ if (map.yyyy < 100) {
+ // In the constructor, 2-digit years map to 1900-1999.
+ // Use `setFullYear()` to set the correct year.
+ date.setFullYear(map.yyyy);
+ }
+
+ return date;
}
}
@@ -34210,25 +29558,24 @@ function createDateParser(regexp, mapping) {
}
function createDateInputType(type, regexp, parseDate, format) {
- return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
- badInputChecker(scope, element, attr, ctrl);
+ return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
+ badInputChecker(scope, element, attr, ctrl, type);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
- var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
+
+ var isTimeType = type === 'time' || type === 'datetimelocal';
var previousDate;
+ var previousTimezone;
- ctrl.$$parserName = type;
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
+
if (regexp.test(value)) {
// 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) {
- parsedDate = convertTimezoneToLocal(parsedDate, timezone);
- }
- return parsedDate;
+ return parseDateAndConvertTimeZoneToLocal(value, previousDate);
}
+ ctrl.$$parserName = type;
return undefined;
});
@@ -34238,35 +29585,50 @@ function createDateInputType(type, regexp, parseDate, format) {
}
if (isValidDate(value)) {
previousDate = value;
- if (previousDate && timezone) {
+ var timezone = ctrl.$options.getOption('timezone');
+
+ if (timezone) {
+ previousTimezone = timezone;
previousDate = convertTimezoneToLocal(previousDate, timezone, true);
}
- return $filter('date')(value, format, timezone);
+
+ return formatter(value, timezone);
} else {
previousDate = null;
+ previousTimezone = null;
return '';
}
});
if (isDefined(attr.min) || attr.ngMin) {
- var minVal;
+ var minVal = attr.min || $parse(attr.ngMin)(scope);
+ var parsedMinVal = parseObservedDateValue(minVal);
+
ctrl.$validators.min = function(value) {
- return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal;
+ return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
};
attr.$observe('min', function(val) {
- minVal = parseObservedDateValue(val);
- ctrl.$validate();
+ if (val !== minVal) {
+ parsedMinVal = parseObservedDateValue(val);
+ minVal = val;
+ ctrl.$validate();
+ }
});
}
if (isDefined(attr.max) || attr.ngMax) {
- var maxVal;
+ var maxVal = attr.max || $parse(attr.ngMax)(scope);
+ var parsedMaxVal = parseObservedDateValue(maxVal);
+
ctrl.$validators.max = function(value) {
- return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
+ return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
};
attr.$observe('max', function(val) {
- maxVal = parseObservedDateValue(val);
- ctrl.$validate();
+ if (val !== maxVal) {
+ parsedMaxVal = parseObservedDateValue(val);
+ maxVal = val;
+ ctrl.$validate();
+ }
});
}
@@ -34276,30 +29638,68 @@ function createDateInputType(type, regexp, parseDate, format) {
}
function parseObservedDateValue(val) {
- return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val;
+ return isDefined(val) && !isDate(val) ? parseDateAndConvertTimeZoneToLocal(val) || undefined : val;
+ }
+
+ function parseDateAndConvertTimeZoneToLocal(value, previousDate) {
+ var timezone = ctrl.$options.getOption('timezone');
+
+ if (previousTimezone && previousTimezone !== timezone) {
+ // If the timezone has changed, adjust the previousDate to the default timezone
+ // so that the new date is converted with the correct timezone offset
+ previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone));
+ }
+
+ var parsedDate = parseDate(value, previousDate);
+
+ if (!isNaN(parsedDate) && timezone) {
+ parsedDate = convertTimezoneToLocal(parsedDate, timezone);
+ }
+ return parsedDate;
+ }
+
+ function formatter(value, timezone) {
+ var targetFormat = format;
+
+ if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) {
+ targetFormat = format
+ .replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat'))
+ .replace(/:$/, '');
+ }
+
+ var formatted = $filter('date')(value, targetFormat, timezone);
+
+ if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) {
+ formatted = formatted.replace(/(?::00)?(?:\.000)?$/, '');
+ }
+
+ return formatted;
}
};
}
-function badInputChecker(scope, element, attr, ctrl) {
+function badInputChecker(scope, element, attr, ctrl, parserName) {
var node = element[0];
var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
if (nativeValidation) {
ctrl.$parsers.push(function(value) {
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
- return validity.badInput || validity.typeMismatch ? undefined : value;
+ if (validity.badInput || validity.typeMismatch) {
+ ctrl.$$parserName = parserName;
+ return undefined;
+ }
+
+ return value;
});
}
}
-function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- badInputChecker(scope, element, attr, ctrl);
- baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
-
- ctrl.$$parserName = 'number';
+function numberFormatterParser(ctrl) {
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
if (NUMBER_REGEXP.test(value)) return parseFloat(value);
+
+ ctrl.$$parserName = 'number';
return undefined;
});
@@ -34312,37 +29712,282 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
return value;
});
+}
+
+function parseNumberAttrVal(val) {
+ if (isDefined(val) && !isNumber(val)) {
+ val = parseFloat(val);
+ }
+ return !isNumberNaN(val) ? val : undefined;
+}
+
+function isNumberInteger(num) {
+ // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066
+ // (minus the assumption that `num` is a number)
+
+ // eslint-disable-next-line no-bitwise
+ return (num | 0) === num;
+}
+
+function countDecimals(num) {
+ var numString = num.toString();
+ var decimalSymbolIndex = numString.indexOf('.');
+
+ if (decimalSymbolIndex === -1) {
+ if (-1 < num && num < 1) {
+ // It may be in the exponential notation format (`1e-X`)
+ var match = /e-(\d+)$/.exec(numString);
+
+ if (match) {
+ return Number(match[1]);
+ }
+ }
+
+ return 0;
+ }
+
+ return numString.length - decimalSymbolIndex - 1;
+}
+
+function isValidForStep(viewValue, stepBase, step) {
+ // At this point `stepBase` and `step` are expected to be non-NaN values
+ // and `viewValue` is expected to be a valid stringified number.
+ var value = Number(viewValue);
+
+ var isNonIntegerValue = !isNumberInteger(value);
+ var isNonIntegerStepBase = !isNumberInteger(stepBase);
+ var isNonIntegerStep = !isNumberInteger(step);
+
+ // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or
+ // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers.
+ if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) {
+ var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0;
+ var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0;
+ var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0;
+
+ var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals);
+ var multiplier = Math.pow(10, decimalCount);
+
+ value = value * multiplier;
+ stepBase = stepBase * multiplier;
+ step = step * multiplier;
+
+ if (isNonIntegerValue) value = Math.round(value);
+ if (isNonIntegerStepBase) stepBase = Math.round(stepBase);
+ if (isNonIntegerStep) step = Math.round(step);
+ }
+
+ return (value - stepBase) % step === 0;
+}
+
+function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
+ badInputChecker(scope, element, attr, ctrl, 'number');
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var parsedMinVal;
if (isDefined(attr.min) || attr.ngMin) {
- var minVal;
- ctrl.$validators.min = function(value) {
- return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
+ var minVal = attr.min || $parse(attr.ngMin)(scope);
+ parsedMinVal = parseNumberAttrVal(minVal);
+
+ ctrl.$validators.min = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
};
attr.$observe('min', function(val) {
- if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val);
+ if (val !== minVal) {
+ parsedMinVal = parseNumberAttrVal(val);
+ minVal = val;
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
}
- minVal = isNumber(val) && !isNaN(val) ? val : undefined;
- // TODO(matsko): implement validateLater to reduce number of validations
- ctrl.$validate();
});
}
if (isDefined(attr.max) || attr.ngMax) {
- var maxVal;
- ctrl.$validators.max = function(value) {
- return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
+ var maxVal = attr.max || $parse(attr.ngMax)(scope);
+ var parsedMaxVal = parseNumberAttrVal(maxVal);
+
+ ctrl.$validators.max = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
};
attr.$observe('max', function(val) {
- if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val);
+ if (val !== maxVal) {
+ parsedMaxVal = parseNumberAttrVal(val);
+ maxVal = val;
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
}
- maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
+ });
+ }
+
+ if (isDefined(attr.step) || attr.ngStep) {
+ var stepVal = attr.step || $parse(attr.ngStep)(scope);
+ var parsedStepVal = parseNumberAttrVal(stepVal);
+
+ ctrl.$validators.step = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
+ isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
+ };
+
+ attr.$observe('step', function(val) {
// TODO(matsko): implement validateLater to reduce number of validations
- ctrl.$validate();
+ if (val !== stepVal) {
+ parsedStepVal = parseNumberAttrVal(val);
+ stepVal = val;
+ ctrl.$validate();
+ }
+
});
+
+ }
+}
+
+function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ badInputChecker(scope, element, attr, ctrl, 'range');
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
+ minVal = supportsRange ? 0 : undefined,
+ maxVal = supportsRange ? 100 : undefined,
+ stepVal = supportsRange ? 1 : undefined,
+ validity = element[0].validity,
+ hasMinAttr = isDefined(attr.min),
+ hasMaxAttr = isDefined(attr.max),
+ hasStepAttr = isDefined(attr.step);
+
+ var originalRender = ctrl.$render;
+
+ ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
+ //Browsers that implement range will set these values automatically, but reading the adjusted values after
+ //$render would cause the min / max validators to be applied with the wrong value
+ function rangeRender() {
+ originalRender();
+ ctrl.$setViewValue(element.val());
+ } :
+ originalRender;
+
+ if (hasMinAttr) {
+ minVal = parseNumberAttrVal(attr.min);
+
+ ctrl.$validators.min = supportsRange ?
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ function noopMinValidator() { return true; } :
+ // non-support browsers validate the min val
+ function minValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
+ };
+
+ setInitialValueAndObserver('min', minChange);
+ }
+
+ if (hasMaxAttr) {
+ maxVal = parseNumberAttrVal(attr.max);
+
+ ctrl.$validators.max = supportsRange ?
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ function noopMaxValidator() { return true; } :
+ // non-support browsers validate the max val
+ function maxValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
+ };
+
+ setInitialValueAndObserver('max', maxChange);
+ }
+
+ if (hasStepAttr) {
+ stepVal = parseNumberAttrVal(attr.step);
+
+ ctrl.$validators.step = supportsRange ?
+ function nativeStepValidator() {
+ // Currently, only FF implements the spec on step change correctly (i.e. adjusting the
+ // input element value to a valid value). It's possible that other browsers set the stepMismatch
+ // validity error instead, so we can at least report an error in that case.
+ return !validity.stepMismatch;
+ } :
+ // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
+ function stepValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
+ isValidForStep(viewValue, minVal || 0, stepVal);
+ };
+
+ setInitialValueAndObserver('step', stepChange);
+ }
+
+ function setInitialValueAndObserver(htmlAttrName, changeFn) {
+ // interpolated attributes set the attribute value only after a digest, but we need the
+ // attribute value when the input is first rendered, so that the browser can adjust the
+ // input value based on the min/max value
+ element.attr(htmlAttrName, attr[htmlAttrName]);
+ var oldVal = attr[htmlAttrName];
+ attr.$observe(htmlAttrName, function wrappedObserver(val) {
+ if (val !== oldVal) {
+ oldVal = val;
+ changeFn(val);
+ }
+ });
+ }
+
+ function minChange(val) {
+ minVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the minVal is greater than the element value
+ if (minVal > elVal) {
+ elVal = minVal;
+ element.val(elVal);
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function maxChange(val) {
+ maxVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the maxVal is less than the element value
+ if (maxVal < elVal) {
+ element.val(maxVal);
+ // IE11 and Chrome don't set the value to the minVal when max < min
+ elVal = maxVal < minVal ? minVal : maxVal;
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function stepChange(val) {
+ stepVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ // Some browsers don't adjust the input value correctly, but set the stepMismatch error
+ if (!supportsRange) {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ } else if (ctrl.$viewValue !== element.val()) {
+ ctrl.$setViewValue(element.val());
+ }
}
}
@@ -34352,7 +29997,6 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
stringBasedInputType(ctrl);
- ctrl.$$parserName = 'url';
ctrl.$validators.url = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
@@ -34365,7 +30009,6 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
stringBasedInputType(ctrl);
- ctrl.$$parserName = 'email';
ctrl.$validators.email = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
@@ -34373,22 +30016,31 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
function radioInputType(scope, element, attr, ctrl) {
+ var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false';
// make the name unique, if not defined
if (isUndefined(attr.name)) {
element.attr('name', nextUid());
}
var listener = function(ev) {
+ var value;
if (element[0].checked) {
- ctrl.$setViewValue(attr.value, ev && ev.type);
+ value = attr.value;
+ if (doTrim) {
+ value = trim(value);
+ }
+ ctrl.$setViewValue(value, ev && ev.type);
}
};
- element.on('click', listener);
+ element.on('change', listener);
ctrl.$render = function() {
var value = attr.value;
- element[0].checked = (value == ctrl.$viewValue);
+ if (doTrim) {
+ value = trim(value);
+ }
+ element[0].checked = (value === ctrl.$viewValue);
};
attr.$observe('value', ctrl.$render);
@@ -34415,7 +30067,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
ctrl.$setViewValue(element[0].checked, ev && ev.type);
};
- element.on('click', listener);
+ element.on('change', listener);
ctrl.$render = function() {
element[0].checked = ctrl.$viewValue;
@@ -34444,11 +30096,11 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @restrict E
*
* @description
- * HTML textarea element control with angular data-binding. The data-binding and validation
+ * HTML textarea element control with AngularJS data-binding. The data-binding and validation
* properties of this element are exactly the same as those of the
* {@link ng.directive:input input element}.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -34460,7 +30112,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -34468,9 +30120,23 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
+ *
+ * @knownIssue
+ *
+ * When specifying the `placeholder` attribute of `<textarea>`, Internet Explorer will temporarily
+ * insert the placeholder value as the textarea's content. If the placeholder value contains
+ * interpolation (`{{ ... }}`), an error will be logged in the console when AngularJS tries to update
+ * the value of the by-then-removed text node. This doesn't affect the functionality of the
+ * textarea, but can be undesirable.
+ *
+ * You can work around this Internet Explorer issue by using `ng-attr-placeholder` instead of
+ * `placeholder` on textareas, whenever you need interpolation in the placeholder value. You can
+ * find more details on `ngAttr` in the
+ * [Interpolation](guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes) section of the
+ * Developer Guide.
*/
@@ -34489,7 +30155,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`.
* </div>
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {boolean=} ngRequired Sets `required` attribute if set to true
@@ -34499,7 +30165,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * value does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * value does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -34507,9 +30173,9 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
* This parameter is ignored for input[type=password] controls, which will never trim the
* input.
*
@@ -34628,28 +30294,70 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
}];
+var hiddenInputBrowserCacheDirective = function() {
+ var valueProperty = {
+ configurable: true,
+ enumerable: false,
+ get: function() {
+ return this.getAttribute('value') || '';
+ },
+ set: function(val) {
+ this.setAttribute('value', val);
+ }
+ };
+
+ return {
+ restrict: 'E',
+ priority: 200,
+ compile: function(_, attr) {
+ if (lowercase(attr.type) !== 'hidden') {
+ return;
+ }
+
+ return {
+ pre: function(scope, element, attr, ctrls) {
+ var node = element[0];
+
+ // Support: Edge
+ // Moving the DOM around prevents autofillling
+ if (node.parentNode) {
+ node.parentNode.insertBefore(node, node.nextSibling);
+ }
+
+ // Support: FF, IE
+ // Avoiding direct assignment to .value prevents autofillling
+ if (Object.defineProperty) {
+ Object.defineProperty(node, 'value', valueProperty);
+ }
+ }
+ };
+ }
+ };
+};
+
+
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
/**
* @ngdoc directive
* @name ngValue
+ * @restrict A
+ * @priority 100
*
* @description
- * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`},
- * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to
- * the bound value.
+ * Binds the given expression to the value of the element.
*
- * `ngValue` is useful when dynamically generating lists of radio buttons using
- * {@link ngRepeat `ngRepeat`}, as shown below.
+ * It is mainly used on {@link input[radio] `input[radio]`} and option elements,
+ * so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its
+ * {@link select `select`} parent element) is set to the bound value. It is especially useful
+ * for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below.
*
- * Likewise, `ngValue` can be used to generate `<option>` elements for
- * the {@link select `select`} element. In that case however, only strings are supported
- * for the `value `attribute, so the resulting `ngModel` will always be a string.
- * Support for `select` models with non-string values is available via `ngOptions`.
+ * It can also be used to achieve one-way binding of a given expression to an input element
+ * such as an `input[text]` or a `textarea`, when that element does not use ngModel.
*
- * @element input
- * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
- * of the `input` element
+ * @element ANY
+ * @param {string=} ngValue AngularJS expression, whose value will be bound to the `value` attribute
+ * and `value` property of the element.
*
* @example
<example name="ngValue-directive" module="valueExample">
@@ -34688,3712 +30396,101 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
</example>
*/
var ngValueDirective = function() {
+ /**
+ * inputs use the value attribute as their default value if the value property is not set.
+ * Once the value property has been set (by adding input), it will not react to changes to
+ * the value attribute anymore. Setting both attribute and property fixes this behavior, and
+ * makes it possible to use ngValue as a sort of one-way bind.
+ */
+ function updateElementValue(element, attr, value) {
+ // Support: IE9 only
+ // In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`).
+ var propValue = isDefined(value) ? value : (msie === 9) ? '' : null;
+ element.prop('value', propValue);
+ attr.$set('value', value);
+ }
+
return {
restrict: 'A',
priority: 100,
compile: function(tpl, tplAttr) {
if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
return function ngValueConstantLink(scope, elm, attr) {
- attr.$set('value', scope.$eval(attr.ngValue));
+ var value = scope.$eval(attr.ngValue);
+ updateElementValue(elm, attr, value);
};
} else {
return function ngValueLink(scope, elm, attr) {
scope.$watch(attr.ngValue, function valueWatchAction(value) {
- attr.$set('value', value);
- });
- };
- }
- }
- };
-};
-
-/**
- * @ngdoc directive
- * @name ngBind
- * @restrict AC
- *
- * @description
- * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element
- * with the value of a given expression, and to update the text content when the value of that
- * expression changes.
- *
- * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like
- * `{{ expression }}` which is similar but less verbose.
- *
- * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily
- * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an
- * element attribute, it makes the bindings invisible to the user while the page is loading.
- *
- * An alternative solution to this problem would be using the
- * {@link ng.directive:ngCloak ngCloak} directive.
- *
- *
- * @element ANY
- * @param {expression} ngBind {@link guide/expression Expression} to evaluate.
- *
- * @example
- * Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
- <example module="bindExample">
- <file name="index.html">
- <script>
- angular.module('bindExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.name = 'Whirled';
- }]);
- </script>
- <div ng-controller="ExampleController">
- <label>Enter name: <input type="text" ng-model="name"></label><br>
- Hello <span ng-bind="name"></span>!
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-bind', function() {
- var nameInput = element(by.model('name'));
-
- expect(element(by.binding('name')).getText()).toBe('Whirled');
- nameInput.clear();
- nameInput.sendKeys('world');
- expect(element(by.binding('name')).getText()).toBe('world');
- });
- </file>
- </example>
- */
-var ngBindDirective = ['$compile', function($compile) {
- return {
- 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) {
- element.textContent = isUndefined(value) ? '' : value;
- });
- };
- }
- };
-}];
-
-
-/**
- * @ngdoc directive
- * @name ngBindTemplate
- *
- * @description
- * The `ngBindTemplate` directive specifies that the element
- * text content should be replaced with the interpolation of the template
- * in the `ngBindTemplate` attribute.
- * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}`
- * expressions. This directive is needed since some HTML elements
- * (such as TITLE and OPTION) cannot contain SPAN elements.
- *
- * @element ANY
- * @param {string} ngBindTemplate template of form
- * <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval.
- *
- * @example
- * Try it here: enter text in text box and watch the greeting change.
- <example module="bindExample">
- <file name="index.html">
- <script>
- angular.module('bindExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.salutation = 'Hello';
- $scope.name = 'World';
- }]);
- </script>
- <div ng-controller="ExampleController">
- <label>Salutation: <input type="text" ng-model="salutation"></label><br>
- <label>Name: <input type="text" ng-model="name"></label><br>
- <pre ng-bind-template="{{salutation}} {{name}}!"></pre>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-bind', function() {
- var salutationElem = element(by.binding('salutation'));
- var salutationInput = element(by.model('salutation'));
- var nameInput = element(by.model('name'));
-
- expect(salutationElem.getText()).toBe('Hello World!');
-
- salutationInput.clear();
- salutationInput.sendKeys('Greetings');
- nameInput.clear();
- nameInput.sendKeys('user');
-
- expect(salutationElem.getText()).toBe('Greetings user!');
- });
- </file>
- </example>
- */
-var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) {
- return {
- 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.textContent = isUndefined(value) ? '' : value;
- });
- };
- }
- };
-}];
-
-
-/**
- * @ngdoc directive
- * @name ngBindHtml
- *
- * @description
- * Evaluates the expression and inserts the resulting HTML into the element in a secure way. By default,
- * the resulting HTML 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). 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#show-me-an-example-using-sce- 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.)
- *
- * @element ANY
- * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate.
- *
- * @example
-
- <example module="bindHtmlExample" deps="angular-sanitize.js">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <p ng-bind-html="myHTML"></p>
- </div>
- </file>
-
- <file name="script.js">
- angular.module('bindHtmlExample', ['ngSanitize'])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.myHTML =
- 'I am an <code>HTML</code>string with ' +
- '<a href="#">links!</a> and other <em>stuff</em>';
- }]);
- </file>
-
- <file name="protractor.js" type="protractor">
- it('should check ng-bind-html', function() {
- expect(element(by.binding('myHTML')).getText()).toBe(
- 'I am an HTMLstring with links! and other stuff');
- });
- </file>
- </example>
- */
-var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) {
- return {
- restrict: 'A',
- compile: function ngBindHtmlCompile(tElement, tAttrs) {
- var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
- var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) {
- // Unwrap the value to compare the actual inner safe value, not the wrapper object.
- return $sce.valueOf(val);
- });
- $compile.$$addBindingClass(tElement);
-
- return function ngBindHtmlLink(scope, element, attr) {
- $compile.$$addBindingInfo(element, attr.ngBindHtml);
-
- scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
- // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter.
- var value = ngBindHtmlGetter(scope);
- element.html($sce.getTrustedHtml(value) || '');
- });
- };
- }
- };
-}];
-
-/**
- * @ngdoc directive
- * @name ngChange
- *
- * @description
- * Evaluate the given expression when the user changes the input.
- * The expression is evaluated immediately, unlike the JavaScript onchange event
- * which only triggers at the end of a change (usually, when the user leaves the
- * form element or presses the return key).
- *
- * The `ngChange` expression is only evaluated when a change in the input value causes
- * a new value to be committed to the model.
- *
- * It will not be evaluated:
- * * if the value returned from the `$parsers` transformation pipeline has not changed
- * * if the input has continued to be invalid since the model will stay `null`
- * * if the model is changed programmatically and not by a change to the input value
- *
- *
- * Note, this directive requires `ngModel` to be present.
- *
- * @element input
- * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
- * in input value.
- *
- * @example
- * <example name="ngChange-directive" module="changeExample">
- * <file name="index.html">
- * <script>
- * angular.module('changeExample', [])
- * .controller('ExampleController', ['$scope', function($scope) {
- * $scope.counter = 0;
- * $scope.change = function() {
- * $scope.counter++;
- * };
- * }]);
- * </script>
- * <div ng-controller="ExampleController">
- * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" />
- * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" />
- * <label for="ng-change-example2">Confirmed</label><br />
- * <tt>debug = {{confirmed}}</tt><br/>
- * <tt>counter = {{counter}}</tt><br/>
- * </div>
- * </file>
- * <file name="protractor.js" type="protractor">
- * var counter = element(by.binding('counter'));
- * var debug = element(by.binding('confirmed'));
- *
- * it('should evaluate the expression if changing from view', function() {
- * expect(counter.getText()).toContain('0');
- *
- * element(by.id('ng-change-example1')).click();
- *
- * expect(counter.getText()).toContain('1');
- * expect(debug.getText()).toContain('true');
- * });
- *
- * it('should not evaluate the expression if changing from model', function() {
- * element(by.id('ng-change-example2')).click();
-
- * expect(counter.getText()).toContain('0');
- * expect(debug.getText()).toContain('true');
- * });
- * </file>
- * </example>
- */
-var ngChangeDirective = valueFn({
- restrict: 'A',
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- ctrl.$viewChangeListeners.push(function() {
- scope.$eval(attr.ngChange);
- });
- }
-});
-
-function classDirective(name, selector) {
- name = 'ngClass' + name;
- return ['$animate', function($animate) {
- return {
- restrict: 'AC',
- link: function(scope, element, attr) {
- var oldVal;
-
- scope.$watch(attr[name], ngClassWatchAction, true);
-
- attr.$observe('class', function(value) {
- ngClassWatchAction(scope.$eval(attr[name]));
- });
-
-
- if (name !== 'ngClass') {
- scope.$watch('$index', function($index, old$index) {
- // jshint bitwise: false
- var mod = $index & 1;
- if (mod !== (old$index & 1)) {
- var classes = arrayClasses(scope.$eval(attr[name]));
- mod === selector ?
- addClasses(classes) :
- removeClasses(classes);
- }
- });
- }
-
- function addClasses(classes) {
- var newClasses = digestClassCounts(classes, 1);
- attr.$addClass(newClasses);
- }
-
- function removeClasses(classes) {
- var newClasses = digestClassCounts(classes, -1);
- attr.$removeClass(newClasses);
- }
-
- function digestClassCounts(classes, count) {
- // Use createMap() to prevent class assumptions involving property
- // names in Object.prototype
- var classCounts = element.data('$classCounts') || createMap();
- var classesToUpdate = [];
- forEach(classes, function(className) {
- if (count > 0 || classCounts[className]) {
- classCounts[className] = (classCounts[className] || 0) + count;
- if (classCounts[className] === +(count > 0)) {
- classesToUpdate.push(className);
- }
- }
- });
- element.data('$classCounts', classCounts);
- return classesToUpdate.join(' ');
- }
-
- function updateClasses(oldClasses, newClasses) {
- var toAdd = arrayDifference(newClasses, oldClasses);
- var toRemove = arrayDifference(oldClasses, newClasses);
- toAdd = digestClassCounts(toAdd, 1);
- toRemove = digestClassCounts(toRemove, -1);
- if (toAdd && toAdd.length) {
- $animate.addClass(element, toAdd);
- }
- if (toRemove && toRemove.length) {
- $animate.removeClass(element, toRemove);
- }
- }
-
- function ngClassWatchAction(newVal) {
- // jshint bitwise: false
- if (selector === true || (scope.$index & 1) === selector) {
- // jshint bitwise: true
- var newClasses = arrayClasses(newVal || []);
- if (!oldVal) {
- addClasses(newClasses);
- } else if (!equals(newVal,oldVal)) {
- var oldClasses = arrayClasses(oldVal);
- updateClasses(oldClasses, newClasses);
- }
- }
- if (isArray(newVal)) {
- oldVal = newVal.map(function(v) { return shallowCopy(v); });
- } else {
- oldVal = shallowCopy(newVal);
- }
- }
- }
- };
-
- function arrayDifference(tokens1, tokens2) {
- var values = [];
-
- outer:
- for (var i = 0; i < tokens1.length; i++) {
- var token = tokens1[i];
- for (var j = 0; j < tokens2.length; j++) {
- if (token == tokens2[j]) continue outer;
- }
- values.push(token);
- }
- return values;
- }
-
- function arrayClasses(classVal) {
- var classes = [];
- if (isArray(classVal)) {
- forEach(classVal, function(v) {
- classes = classes.concat(arrayClasses(v));
- });
- return classes;
- } else if (isString(classVal)) {
- return classVal.split(' ');
- } else if (isObject(classVal)) {
- forEach(classVal, function(v, k) {
- if (v) {
- classes = classes.concat(k.split(' '));
- }
- });
- return classes;
- }
- return classVal;
- }
- }];
-}
-
-/**
- * @ngdoc directive
- * @name ngClass
- * @restrict AC
- *
- * @description
- * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding
- * an expression that represents all classes to be added.
- *
- * The directive operates in three different ways, depending on which of three types the expression
- * evaluates to:
- *
- * 1. If the expression evaluates to a string, the string should be one or more space-delimited class
- * names.
- *
- * 2. If the expression evaluates to an object, then for each key-value pair of the
- * object with a truthy value the corresponding key is used as a class name.
- *
- * 3. If the expression evaluates to an array, each element of the array should either be a string as in
- * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array
- * to give you more control over what CSS classes appear. See the code below for an example of this.
- *
- *
- * The directive won't add duplicate classes if a particular class was already set.
- *
- * When the expression changes, the previously added classes are removed and only then are the
- * new classes added.
- *
- * @knownIssue
- * You should not use {@link guide/interpolation interpolation} in the value of the `class`
- * attribute, when using the `ngClass` directive on the same element.
- * See {@link guide/interpolation#known-issues here} for more info.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
- * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
- *
- * @element ANY
- * @param {expression} ngClass {@link guide/expression Expression} to eval. The result
- * of the evaluation can be a string representing space delimited class
- * names, an array, or a map of class names to boolean values. In the case of a map, the
- * names of the properties whose values are truthy will be added as css classes to the
- * element.
- *
- * @example Example that demonstrates basic bindings via ngClass directive.
- <example>
- <file name="index.html">
- <p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p>
- <label>
- <input type="checkbox" ng-model="deleted">
- deleted (apply "strike" class)
- </label><br>
- <label>
- <input type="checkbox" ng-model="important">
- important (apply "bold" class)
- </label><br>
- <label>
- <input type="checkbox" ng-model="error">
- error (apply "has-error" class)
- </label>
- <hr>
- <p ng-class="style">Using String Syntax</p>
- <input type="text" ng-model="style"
- placeholder="Type: bold strike red" aria-label="Type: bold strike red">
- <hr>
- <p ng-class="[style1, style2, style3]">Using Array Syntax</p>
- <input ng-model="style1"
- placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red"><br>
- <input ng-model="style2"
- placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 2"><br>
- <input ng-model="style3"
- placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 3"><br>
- <hr>
- <p ng-class="[style4, {orange: warning}]">Using Array and Map Syntax</p>
- <input ng-model="style4" placeholder="Type: bold, strike" aria-label="Type: bold, strike"><br>
- <label><input type="checkbox" ng-model="warning"> warning (apply "orange" class)</label>
- </file>
- <file name="style.css">
- .strike {
- text-decoration: line-through;
- }
- .bold {
- font-weight: bold;
- }
- .red {
- color: red;
- }
- .has-error {
- color: red;
- background-color: yellow;
- }
- .orange {
- color: orange;
- }
- </file>
- <file name="protractor.js" type="protractor">
- var ps = element.all(by.css('p'));
-
- it('should let you toggle the class', function() {
-
- expect(ps.first().getAttribute('class')).not.toMatch(/bold/);
- expect(ps.first().getAttribute('class')).not.toMatch(/has-error/);
-
- element(by.model('important')).click();
- expect(ps.first().getAttribute('class')).toMatch(/bold/);
-
- element(by.model('error')).click();
- expect(ps.first().getAttribute('class')).toMatch(/has-error/);
- });
-
- it('should let you toggle string example', function() {
- expect(ps.get(1).getAttribute('class')).toBe('');
- element(by.model('style')).clear();
- element(by.model('style')).sendKeys('red');
- expect(ps.get(1).getAttribute('class')).toBe('red');
- });
-
- it('array example should have 3 classes', function() {
- expect(ps.get(2).getAttribute('class')).toBe('');
- element(by.model('style1')).sendKeys('bold');
- element(by.model('style2')).sendKeys('strike');
- element(by.model('style3')).sendKeys('red');
- expect(ps.get(2).getAttribute('class')).toBe('bold strike red');
- });
-
- it('array with map example should have 2 classes', function() {
- expect(ps.last().getAttribute('class')).toBe('');
- element(by.model('style4')).sendKeys('bold');
- element(by.model('warning')).click();
- expect(ps.last().getAttribute('class')).toBe('bold orange');
- });
- </file>
- </example>
-
- ## Animations
-
- The example below demonstrates how to perform animations using ngClass.
-
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
- <file name="index.html">
- <input id="setbtn" type="button" value="set" ng-click="myVar='my-class'">
- <input id="clearbtn" type="button" value="clear" ng-click="myVar=''">
- <br>
- <span class="base-class" ng-class="myVar">Sample Text</span>
- </file>
- <file name="style.css">
- .base-class {
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- }
-
- .base-class.my-class {
- color: red;
- font-size:3em;
- }
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-class', function() {
- expect(element(by.css('.base-class')).getAttribute('class')).not.
- toMatch(/my-class/);
-
- element(by.id('setbtn')).click();
-
- expect(element(by.css('.base-class')).getAttribute('class')).
- toMatch(/my-class/);
-
- element(by.id('clearbtn')).click();
-
- expect(element(by.css('.base-class')).getAttribute('class')).not.
- toMatch(/my-class/);
- });
- </file>
- </example>
-
-
- ## ngClass and pre-existing CSS3 Transitions/Animations
- The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
- Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder
- any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure
- to view the step by step details of {@link $animate#addClass $animate.addClass} and
- {@link $animate#removeClass $animate.removeClass}.
- */
-var ngClassDirective = classDirective('', true);
-
-/**
- * @ngdoc directive
- * @name ngClassOdd
- * @restrict AC
- *
- * @description
- * The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except they work in
- * conjunction with `ngRepeat` and take effect only on odd (even) rows.
- *
- * This directive can be applied only within the scope of an
- * {@link ng.directive:ngRepeat ngRepeat}.
- *
- * @element ANY
- * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result
- * of the evaluation can be a string representing space delimited class names or an array.
- *
- * @example
- <example>
- <file name="index.html">
- <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']">
- <li ng-repeat="name in names">
- <span ng-class-odd="'odd'" ng-class-even="'even'">
- {{name}}
- </span>
- </li>
- </ol>
- </file>
- <file name="style.css">
- .odd {
- color: red;
- }
- .even {
- color: blue;
- }
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-class-odd and ng-class-even', function() {
- expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
- toMatch(/odd/);
- expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
- toMatch(/even/);
- });
- </file>
- </example>
- */
-var ngClassOddDirective = classDirective('Odd', 0);
-
-/**
- * @ngdoc directive
- * @name ngClassEven
- * @restrict AC
- *
- * @description
- * The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except they work in
- * conjunction with `ngRepeat` and take effect only on odd (even) rows.
- *
- * This directive can be applied only within the scope of an
- * {@link ng.directive:ngRepeat ngRepeat}.
- *
- * @element ANY
- * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The
- * result of the evaluation can be a string representing space delimited class names or an array.
- *
- * @example
- <example>
- <file name="index.html">
- <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']">
- <li ng-repeat="name in names">
- <span ng-class-odd="'odd'" ng-class-even="'even'">
- {{name}} &nbsp; &nbsp; &nbsp;
- </span>
- </li>
- </ol>
- </file>
- <file name="style.css">
- .odd {
- color: red;
- }
- .even {
- color: blue;
- }
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-class-odd and ng-class-even', function() {
- expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
- toMatch(/odd/);
- expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
- toMatch(/even/);
- });
- </file>
- </example>
- */
-var ngClassEvenDirective = classDirective('Even', 1);
-
-/**
- * @ngdoc directive
- * @name ngCloak
- * @restrict AC
- *
- * @description
- * The `ngCloak` directive is used to prevent the Angular html template from being briefly
- * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this
- * directive to avoid the undesirable flicker effect caused by the html template display.
- *
- * The directive can be applied to the `<body>` element, but the preferred usage is to apply
- * multiple `ngCloak` directives to small portions of the page to permit progressive rendering
- * of the browser view.
- *
- * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and
- * `angular.min.js`.
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
- *
- * ```css
- * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
- * display: none !important;
- * }
- * ```
- *
- * When this css rule is loaded by the browser, all html elements (including their children) that
- * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive
- * during the compilation of the template it deletes the `ngCloak` element attribute, making
- * the compiled element visible.
- *
- * For the best result, the `angular.js` script must be loaded in the head section of the html
- * document; alternatively, the css rule above must be included in the external stylesheet of the
- * application.
- *
- * @element ANY
- *
- * @example
- <example>
- <file name="index.html">
- <div id="template1" ng-cloak>{{ 'hello' }}</div>
- <div id="template2" class="ng-cloak">{{ 'world' }}</div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should remove the template directive and css class', function() {
- expect($('#template1').getAttribute('ng-cloak')).
- toBeNull();
- expect($('#template2').getAttribute('ng-cloak')).
- toBeNull();
- });
- </file>
- </example>
- *
- */
-var ngCloakDirective = ngDirective({
- compile: function(element, attr) {
- attr.$set('ngCloak', undefined);
- element.removeClass('ng-cloak');
- }
-});
-
-/**
- * @ngdoc directive
- * @name ngController
- *
- * @description
- * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular
- * supports the principles behind the Model-View-Controller design pattern.
- *
- * MVC components in angular:
- *
- * * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties
- * are accessed through bindings.
- * * View — The template (HTML with data bindings) that is rendered into the View.
- * * Controller — The `ngController` directive specifies a Controller class; the class contains business
- * logic behind the application to decorate the scope with functions and values
- *
- * Note that you can also attach controllers to the DOM by declaring it in a route definition
- * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller
- * again using `ng-controller` in the template itself. This will cause the controller to be attached
- * and executed twice.
- *
- * @element ANY
- * @scope
- * @priority 500
- * @param {expression} ngController Name of a constructor function registered with the current
- * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression}
- * that on the current scope evaluates to a constructor function.
- *
- * The controller instance can be published into a scope property by specifying
- * `ng-controller="as propertyName"`.
- *
- * If the current `$controllerProvider` is configured to use globals (via
- * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may
- * also be the name of a globally accessible constructor function (not recommended).
- *
- * @example
- * Here is a simple form for editing user contact information. Adding, removing, clearing, and
- * greeting are methods declared on the controller (see source tab). These methods can
- * easily be called from the angular markup. Any changes to the data are automatically reflected
- * in the View without the need for a manual update.
- *
- * Two different declaration styles are included below:
- *
- * * one binds methods and properties directly onto the controller using `this`:
- * `ng-controller="SettingsController1 as settings"`
- * * one injects `$scope` into the controller:
- * `ng-controller="SettingsController2"`
- *
- * The second option is more common in the Angular community, and is generally used in boilerplates
- * and in this guide. However, there are advantages to binding properties directly to the controller
- * and avoiding scope.
- *
- * * Using `controller as` makes it obvious which controller you are accessing in the template when
- * multiple controllers apply to an element.
- * * If you are writing your controllers as classes you have easier access to the properties and
- * methods, which will appear on the scope, from inside the controller code.
- * * Since there is always a `.` in the bindings, you don't have to worry about prototypal
- * inheritance masking primitives.
- *
- * This example demonstrates the `controller as` syntax.
- *
- * <example name="ngControllerAs" module="controllerAsExample">
- * <file name="index.html">
- * <div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings">
- * <label>Name: <input type="text" ng-model="settings.name"/></label>
- * <button ng-click="settings.greet()">greet</button><br/>
- * Contact:
- * <ul>
- * <li ng-repeat="contact in settings.contacts">
- * <select ng-model="contact.type" aria-label="Contact method" id="select_{{$index}}">
- * <option>phone</option>
- * <option>email</option>
- * </select>
- * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
- * <button ng-click="settings.clearContact(contact)">clear</button>
- * <button ng-click="settings.removeContact(contact)" aria-label="Remove">X</button>
- * </li>
- * <li><button ng-click="settings.addContact()">add</button></li>
- * </ul>
- * </div>
- * </file>
- * <file name="app.js">
- * angular.module('controllerAsExample', [])
- * .controller('SettingsController1', SettingsController1);
- *
- * function SettingsController1() {
- * this.name = "John Smith";
- * this.contacts = [
- * {type: 'phone', value: '408 555 1212'},
- * {type: 'email', value: 'john.smith@example.org'} ];
- * }
- *
- * SettingsController1.prototype.greet = function() {
- * alert(this.name);
- * };
- *
- * SettingsController1.prototype.addContact = function() {
- * this.contacts.push({type: 'email', value: 'yourname@example.org'});
- * };
- *
- * SettingsController1.prototype.removeContact = function(contactToRemove) {
- * var index = this.contacts.indexOf(contactToRemove);
- * this.contacts.splice(index, 1);
- * };
- *
- * SettingsController1.prototype.clearContact = function(contact) {
- * contact.type = 'phone';
- * contact.value = '';
- * };
- * </file>
- * <file name="protractor.js" type="protractor">
- * it('should check controller as', function() {
- * var container = element(by.id('ctrl-as-exmpl'));
- * expect(container.element(by.model('settings.name'))
- * .getAttribute('value')).toBe('John Smith');
- *
- * var firstRepeat =
- * container.element(by.repeater('contact in settings.contacts').row(0));
- * var secondRepeat =
- * container.element(by.repeater('contact in settings.contacts').row(1));
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('408 555 1212');
- *
- * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('john.smith@example.org');
- *
- * firstRepeat.element(by.buttonText('clear')).click();
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('');
- *
- * container.element(by.buttonText('add')).click();
- *
- * expect(container.element(by.repeater('contact in settings.contacts').row(2))
- * .element(by.model('contact.value'))
- * .getAttribute('value'))
- * .toBe('yourname@example.org');
- * });
- * </file>
- * </example>
- *
- * This example demonstrates the "attach to `$scope`" style of controller.
- *
- * <example name="ngController" module="controllerExample">
- * <file name="index.html">
- * <div id="ctrl-exmpl" ng-controller="SettingsController2">
- * <label>Name: <input type="text" ng-model="name"/></label>
- * <button ng-click="greet()">greet</button><br/>
- * Contact:
- * <ul>
- * <li ng-repeat="contact in contacts">
- * <select ng-model="contact.type" id="select_{{$index}}">
- * <option>phone</option>
- * <option>email</option>
- * </select>
- * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
- * <button ng-click="clearContact(contact)">clear</button>
- * <button ng-click="removeContact(contact)">X</button>
- * </li>
- * <li>[ <button ng-click="addContact()">add</button> ]</li>
- * </ul>
- * </div>
- * </file>
- * <file name="app.js">
- * angular.module('controllerExample', [])
- * .controller('SettingsController2', ['$scope', SettingsController2]);
- *
- * function SettingsController2($scope) {
- * $scope.name = "John Smith";
- * $scope.contacts = [
- * {type:'phone', value:'408 555 1212'},
- * {type:'email', value:'john.smith@example.org'} ];
- *
- * $scope.greet = function() {
- * alert($scope.name);
- * };
- *
- * $scope.addContact = function() {
- * $scope.contacts.push({type:'email', value:'yourname@example.org'});
- * };
- *
- * $scope.removeContact = function(contactToRemove) {
- * var index = $scope.contacts.indexOf(contactToRemove);
- * $scope.contacts.splice(index, 1);
- * };
- *
- * $scope.clearContact = function(contact) {
- * contact.type = 'phone';
- * contact.value = '';
- * };
- * }
- * </file>
- * <file name="protractor.js" type="protractor">
- * it('should check controller', function() {
- * var container = element(by.id('ctrl-exmpl'));
- *
- * expect(container.element(by.model('name'))
- * .getAttribute('value')).toBe('John Smith');
- *
- * var firstRepeat =
- * container.element(by.repeater('contact in contacts').row(0));
- * var secondRepeat =
- * container.element(by.repeater('contact in contacts').row(1));
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('408 555 1212');
- * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('john.smith@example.org');
- *
- * firstRepeat.element(by.buttonText('clear')).click();
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('');
- *
- * container.element(by.buttonText('add')).click();
- *
- * expect(container.element(by.repeater('contact in contacts').row(2))
- * .element(by.model('contact.value'))
- * .getAttribute('value'))
- * .toBe('yourname@example.org');
- * });
- * </file>
- *</example>
-
- */
-var ngControllerDirective = [function() {
- return {
- restrict: 'A',
- scope: true,
- controller: '@',
- priority: 500
- };
-}];
-
-/**
- * @ngdoc directive
- * @name ngCsp
- *
- * @element html
- * @description
- *
- * Angular has some features that can break certain
- * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
- *
- * If you intend to implement these rules then you must tell Angular not to use these features.
- *
- * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
- *
- *
- * The following rules affect Angular:
- *
- * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions
- * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30%
- * increase in the speed of evaluating Angular expressions.
- *
- * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular
- * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}).
- * To make these directives work when a CSP rule is blocking inline styles, you must link to the
- * `angular-csp.css` in your HTML manually.
- *
- * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval
- * and automatically deactivates this feature in the {@link $parse} service. This autodetection,
- * however, triggers a CSP error to be logged in the console:
- *
- * ```
- * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of
- * script in the following Content Security Policy directive: "default-src 'self'". Note that
- * 'script-src' was not explicitly set, so 'default-src' is used as a fallback.
- * ```
- *
- * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp`
- * directive on an element of the HTML document that appears before the `<script>` tag that loads
- * the `angular.js` file.
- *
- * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.*
- *
- * You can specify which of the CSP related Angular features should be deactivated by providing
- * a value for the `ng-csp` attribute. The options are as follows:
- *
- * * no-inline-style: this stops Angular from injecting CSS styles into the DOM
- *
- * * no-unsafe-eval: this stops Angular from optimizing $parse with unsafe eval of strings
- *
- * You can use these values in the following combinations:
- *
- *
- * * No declaration means that Angular will assume that you can do inline styles, but it will do
- * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous versions
- * of Angular.
- *
- * * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline
- * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions
- * of Angular.
- *
- * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject
- * inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
- *
- * * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
- * run eval - no automatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
- *
- * * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
- * styles nor use eval, which is the same as an empty: ng-csp.
- * E.g.`<body ng-csp="no-inline-style;no-unsafe-eval">`
- *
- * @example
- * This example shows how to apply the `ngCsp` directive to the `html` tag.
- ```html
- <!doctype html>
- <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/);
+ updateElementValue(elm, attr, value);
});
- </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() fn that looks for the `ng-csp` attribute anywhere in the current doc
-
-/**
- * @ngdoc directive
- * @name ngClick
- *
- * @description
- * The ngClick directive allows you to specify custom behavior when
- * an element is clicked.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon
- * click. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-click="count = count + 1" ng-init="count=0">
- Increment
- </button>
- <span>
- count: {{count}}
- </span>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-click', function() {
- expect(element(by.binding('count')).getText()).toMatch('0');
- element(by.css('button')).click();
- expect(element(by.binding('count')).getText()).toMatch('1');
- });
- </file>
- </example>
- */
-/*
- * A collection of directives that allows creation of custom event handlers that are defined as
- * angular expressions and are compiled and executed within the current scope.
- */
-var ngEventDirectives = {};
-
-// For events that might fire synchronously during DOM manipulation
-// we need to execute their event handlers asynchronously using $evalAsync,
-// so that they are not executed in an inconsistent state.
-var forceAsyncEvents = {
- 'blur': true,
- 'focus': true
-};
-forEach(
- 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
- function(eventName) {
- var directiveName = directiveNormalize('ng-' + eventName);
- ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
- return {
- restrict: 'A',
- compile: function($element, attr) {
- // We expose the powerful $event object on the scope that provides access to the Window,
- // etc. that isn't protected by the fast paths in $parse. We explicitly request better
- // checks at the cost of speed since event handler expressions are not executed as
- // frequently as regular change detection.
- var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
- return function ngEventHandler(scope, element) {
- element.on(eventName, function(event) {
- var callback = function() {
- fn(scope, {$event:event});
- };
- if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
- scope.$evalAsync(callback);
- } else {
- scope.$apply(callback);
- }
- });
- };
- }
- };
- }];
- }
-);
-
-/**
- * @ngdoc directive
- * @name ngDblclick
- *
- * @description
- * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon
- * a dblclick. (The Event object is available as `$event`)
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-dblclick="count = count + 1" ng-init="count=0">
- Increment (on double click)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMousedown
- *
- * @description
- * The ngMousedown directive allows you to specify custom behavior on mousedown event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon
- * mousedown. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mousedown="count = count + 1" ng-init="count=0">
- Increment (on mouse down)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMouseup
- *
- * @description
- * Specify custom behavior on mouseup event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon
- * mouseup. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseup="count = count + 1" ng-init="count=0">
- Increment (on mouse up)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngMouseover
- *
- * @description
- * Specify custom behavior on mouseover event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon
- * mouseover. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseover="count = count + 1" ng-init="count=0">
- Increment (when mouse is over)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMouseenter
- *
- * @description
- * Specify custom behavior on mouseenter event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon
- * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseenter="count = count + 1" ng-init="count=0">
- Increment (when mouse enters)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMouseleave
- *
- * @description
- * Specify custom behavior on mouseleave event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon
- * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseleave="count = count + 1" ng-init="count=0">
- Increment (when mouse leaves)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMousemove
- *
- * @description
- * Specify custom behavior on mousemove event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon
- * mousemove. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mousemove="count = count + 1" ng-init="count=0">
- Increment (when mouse moves)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngKeydown
- *
- * @description
- * Specify custom behavior on keydown event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon
- * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-keydown="count = count + 1" ng-init="count=0">
- key down count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngKeyup
- *
- * @description
- * Specify custom behavior on keyup event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon
- * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
- *
- * @example
- <example>
- <file name="index.html">
- <p>Typing in the input box below updates the key count</p>
- <input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}}
-
- <p>Typing in the input box below updates the keycode</p>
- <input ng-keyup="event=$event">
- <p>event keyCode: {{ event.keyCode }}</p>
- <p>event altKey: {{ event.altKey }}</p>
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngKeypress
- *
- * @description
- * Specify custom behavior on keypress event.
- *
- * @element ANY
- * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon
- * keypress. ({@link guide/expression#-event- Event object is available as `$event`}
- * and can be interrogated for keyCode, altKey, etc.)
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-keypress="count = count + 1" ng-init="count=0">
- key press count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngSubmit
- *
- * @description
- * Enables binding angular expressions to onsubmit events.
- *
- * Additionally it prevents the default action (which for form means sending the request to the
- * server and reloading the current page), but only if the form does not contain `action`,
- * `data-action`, or `x-action` attributes.
- *
- * <div class="alert alert-warning">
- * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and
- * `ngSubmit` handlers together. See the
- * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation}
- * for a detailed discussion of when `ngSubmit` may be triggered.
- * </div>
- *
- * @element form
- * @priority 0
- * @param {expression} ngSubmit {@link guide/expression Expression} to eval.
- * ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example module="submitExample">
- <file name="index.html">
- <script>
- angular.module('submitExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.list = [];
- $scope.text = 'hello';
- $scope.submit = function() {
- if ($scope.text) {
- $scope.list.push(this.text);
- $scope.text = '';
- }
- };
- }]);
- </script>
- <form ng-submit="submit()" ng-controller="ExampleController">
- Enter text and hit enter:
- <input type="text" ng-model="text" name="text" />
- <input type="submit" id="submit" value="Submit" />
- <pre>list={{list}}</pre>
- </form>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-submit', function() {
- expect(element(by.binding('list')).getText()).toBe('list=[]');
- element(by.css('#submit')).click();
- expect(element(by.binding('list')).getText()).toContain('hello');
- expect(element(by.model('text')).getAttribute('value')).toBe('');
- });
- it('should ignore empty strings', function() {
- expect(element(by.binding('list')).getText()).toBe('list=[]');
- element(by.css('#submit')).click();
- element(by.css('#submit')).click();
- expect(element(by.binding('list')).getText()).toContain('hello');
- });
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngFocus
- *
- * @description
- * Specify custom behavior on focus event.
- *
- * Note: As the `focus` event is executed synchronously when calling `input.focus()`
- * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
- * during an `$apply` to ensure a consistent state.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
- * focus. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-/**
- * @ngdoc directive
- * @name ngBlur
- *
- * @description
- * Specify custom behavior on blur event.
- *
- * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when
- * an element has lost focus.
- *
- * Note: As the `blur` event is executed synchronously also during DOM manipulations
- * (e.g. removing a focussed input),
- * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
- * during an `$apply` to ensure a consistent state.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon
- * blur. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-/**
- * @ngdoc directive
- * @name ngCopy
- *
- * @description
- * Specify custom behavior on copy event.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon
- * copy. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-copy="copied=true" ng-init="copied=false; value='copy me'" ng-model="value">
- copied: {{copied}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngCut
- *
- * @description
- * Specify custom behavior on cut event.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon
- * cut. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-cut="cut=true" ng-init="cut=false; value='cut me'" ng-model="value">
- cut: {{cut}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngPaste
- *
- * @description
- * Specify custom behavior on paste event.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon
- * paste. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-paste="paste=true" ng-init="paste=false" placeholder='paste here'>
- pasted: {{paste}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngIf
- * @restrict A
- * @multiElement
- *
- * @description
- * The `ngIf` directive removes or recreates a portion of the DOM tree based on an
- * {expression}. If the expression assigned to `ngIf` evaluates to a false
- * value then the element is removed from the DOM, otherwise a clone of the
- * element is reinserted into the DOM.
- *
- * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the
- * element in the DOM rather than changing its visibility via the `display` css property. A common
- * case when this difference is significant is when using css selectors that rely on an element's
- * 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/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
- * is if an element's class attribute is directly modified after it's compiled, using something like
- * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element
- * the added class will be lost because the original compiled state is used to regenerate the element.
- *
- * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter`
- * and `leave` effects.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container |
- * | {@link ng.$animate#leave leave} | 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
- * the element is removed from the DOM tree. If it is truthy a copy of the compiled
- * element is added to the DOM tree.
- *
- * @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
- <file name="index.html">
- <label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/>
- Show when checked:
- <span ng-if="checked" class="animate-if">
- This is removed when the checkbox is unchecked.
- </span>
- </file>
- <file name="animations.css">
- .animate-if {
- background:white;
- border:1px solid black;
- padding:10px;
- }
-
- .animate-if.ng-enter, .animate-if.ng-leave {
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- }
-
- .animate-if.ng-enter,
- .animate-if.ng-leave.ng-leave-active {
- opacity:0;
- }
-
- .animate-if.ng-leave,
- .animate-if.ng-enter.ng-enter-active {
- opacity:1;
- }
- </file>
- </example>
- */
-var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
- return {
- multiElement: true,
- transclude: 'element',
- priority: 600,
- terminal: true,
- restrict: 'A',
- $$tlb: true,
- link: function($scope, $element, $attr, ctrl, $transclude) {
- var block, childScope, previousElements;
- $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
-
- if (value) {
- if (!childScope) {
- $transclude(function(clone, newScope) {
- childScope = newScope;
- clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
- // Note: We only need the first/last node of the cloned nodes.
- // However, we need to keep the reference to the jqlite wrapper as it might be changed later
- // by a directive with templateUrl when its template arrives.
- block = {
- clone: clone
- };
- $animate.enter(clone, $element.parent(), $element);
- });
- }
- } else {
- if (previousElements) {
- previousElements.remove();
- previousElements = null;
- }
- if (childScope) {
- childScope.$destroy();
- childScope = null;
- }
- if (block) {
- previousElements = getBlockNodes(block.clone);
- $animate.leave(previousElements).then(function() {
- previousElements = null;
- });
- block = null;
- }
- }
- });
- }
- };
-}];
-
-/**
- * @ngdoc directive
- * @name ngInclude
- * @restrict ECA
- *
- * @description
- * Fetches, compiles and includes an external HTML fragment.
- *
- * By default, the template URL is restricted to the same domain and protocol as the
- * application document. This is done by calling {@link $sce#getTrustedResourceUrl
- * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols
- * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or
- * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link
- * ng.$sce Strict Contextual Escaping}.
- *
- * In addition, the browser's
- * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
- * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
- * policy may further restrict whether the template is successfully loaded.
- * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://`
- * access on some browsers.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link ng.$animate#enter enter} | when the expression changes, on the new include |
- * | {@link ng.$animate#leave leave} | when the expression changes, on the old include |
- *
- * The enter and leave animation occur concurrently.
- *
- * @scope
- * @priority 400
- *
- * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
- * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
- * @param {string=} onload Expression to evaluate when a new partial is loaded.
- * <div class="alert alert-warning">
- * **Note:** When using onload on SVG elements in IE11, the browser will try to call
- * a function with the name on the window element, which will usually throw a
- * "function is undefined" error. To fix this, you can instead use `data-onload` or a
- * different form that {@link guide/directive#normalization matches} `onload`.
- * </div>
- *
- * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
- * $anchorScroll} to scroll the viewport after the content is loaded.
- *
- * - If the attribute is not set, disable scrolling.
- * - If the attribute is set without value, enable scrolling.
- * - Otherwise enable scrolling only if the expression evaluates to truthy value.
- *
- * @example
- <example module="includeExample" deps="angular-animate.js" animations="true">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <select ng-model="template" ng-options="t.name for t in templates">
- <option value="">(blank)</option>
- </select>
- url of the template: <code>{{template.url}}</code>
- <hr/>
- <div class="slide-animate-container">
- <div class="slide-animate" ng-include="template.url"></div>
- </div>
- </div>
- </file>
- <file name="script.js">
- angular.module('includeExample', ['ngAnimate'])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.templates =
- [ { name: 'template1.html', url: 'template1.html'},
- { name: 'template2.html', url: 'template2.html'} ];
- $scope.template = $scope.templates[0];
- }]);
- </file>
- <file name="template1.html">
- Content of template1.html
- </file>
- <file name="template2.html">
- Content of template2.html
- </file>
- <file name="animations.css">
- .slide-animate-container {
- position:relative;
- background:white;
- border:1px solid black;
- height:40px;
- overflow:hidden;
- }
-
- .slide-animate {
- padding:10px;
- }
-
- .slide-animate.ng-enter, .slide-animate.ng-leave {
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-
- position:absolute;
- top:0;
- left:0;
- right:0;
- bottom:0;
- display:block;
- padding:10px;
- }
-
- .slide-animate.ng-enter {
- top:-50px;
- }
- .slide-animate.ng-enter.ng-enter-active {
- top:0;
- }
-
- .slide-animate.ng-leave {
- top:0;
- }
- .slide-animate.ng-leave.ng-leave-active {
- top:50px;
- }
- </file>
- <file name="protractor.js" type="protractor">
- var templateSelect = element(by.model('template'));
- var includeElem = element(by.css('[ng-include]'));
-
- it('should load template1.html', function() {
- expect(includeElem.getText()).toMatch(/Content of template1.html/);
- });
-
- it('should load template2.html', function() {
- if (browser.params.browser == 'firefox') {
- // Firefox can't handle using selects
- // See https://github.com/angular/protractor/issues/480
- return;
- }
- templateSelect.click();
- templateSelect.all(by.css('option')).get(2).click();
- expect(includeElem.getText()).toMatch(/Content of template2.html/);
- });
-
- it('should change to blank', function() {
- if (browser.params.browser == 'firefox') {
- // Firefox can't handle using selects
- return;
- }
- templateSelect.click();
- templateSelect.all(by.css('option')).get(0).click();
- expect(includeElem.isPresent()).toBe(false);
- });
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc event
- * @name ngInclude#$includeContentRequested
- * @eventType emit on the scope ngInclude was declared in
- * @description
- * Emitted every time the ngInclude content is requested.
- *
- * @param {Object} angularEvent Synthetic event object.
- * @param {String} src URL of content to load.
- */
-
-
-/**
- * @ngdoc event
- * @name ngInclude#$includeContentLoaded
- * @eventType emit on the current ngInclude scope
- * @description
- * Emitted every time the ngInclude content is reloaded.
- *
- * @param {Object} angularEvent Synthetic event object.
- * @param {String} src URL of content to load.
- */
-
-
-/**
- * @ngdoc event
- * @name ngInclude#$includeContentError
- * @eventType emit on the scope ngInclude was declared in
- * @description
- * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299)
- *
- * @param {Object} angularEvent Synthetic event object.
- * @param {String} src URL of content to load.
- */
-var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
- function($templateRequest, $anchorScroll, $animate) {
- return {
- restrict: 'ECA',
- priority: 400,
- terminal: true,
- transclude: 'element',
- controller: angular.noop,
- compile: function(element, attr) {
- var srcExp = attr.ngInclude || attr.src,
- onloadExp = attr.onload || '',
- autoScrollExp = attr.autoscroll;
-
- return function(scope, $element, $attr, ctrl, $transclude) {
- var changeCounter = 0,
- currentScope,
- previousElement,
- currentElement;
-
- var cleanupLastIncludeContent = function() {
- if (previousElement) {
- previousElement.remove();
- previousElement = null;
- }
- if (currentScope) {
- currentScope.$destroy();
- currentScope = null;
- }
- if (currentElement) {
- $animate.leave(currentElement).then(function() {
- previousElement = null;
- });
- previousElement = currentElement;
- currentElement = null;
- }
};
-
- scope.$watch(srcExp, function ngIncludeWatchAction(src) {
- var afterAnimation = function() {
- if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
- $anchorScroll();
- }
- };
- var thisChangeId = ++changeCounter;
-
- if (src) {
- //set the 2nd param to true to ignore the template request error so that the inner
- //contents and scope can be cleaned up.
- $templateRequest(src, true).then(function(response) {
- if (scope.$$destroyed) return;
-
- if (thisChangeId !== changeCounter) return;
- var newScope = scope.$new();
- ctrl.template = response;
-
- // Note: This will also link all children of ng-include that were contained in the original
- // html. If that content contains controllers, ... they could pollute/change the scope.
- // However, using ng-include on an element with additional content does not make sense...
- // Note: We can't remove them in the cloneAttchFn of $transclude as that
- // function is called before linking the content, which would apply child
- // directives to non existing elements.
- var clone = $transclude(newScope, function(clone) {
- cleanupLastIncludeContent();
- $animate.enter(clone, null, $element).then(afterAnimation);
- });
-
- currentScope = newScope;
- currentElement = clone;
-
- currentScope.$emit('$includeContentLoaded', src);
- scope.$eval(onloadExp);
- }, function() {
- if (scope.$$destroyed) return;
-
- if (thisChangeId === changeCounter) {
- cleanupLastIncludeContent();
- scope.$emit('$includeContentError', src);
- }
- });
- scope.$emit('$includeContentRequested', src);
- } else {
- cleanupLastIncludeContent();
- ctrl.template = null;
- }
- });
- };
- }
- };
-}];
-
-// This directive is called during the $transclude call of the first `ngInclude` directive.
-// It will replace and compile the content of the element with the loaded template.
-// We need this directive so that the element content is already filled when
-// the link function of another directive on the same element as ngInclude
-// is called.
-var ngIncludeFillContentDirective = ['$compile',
- function($compile) {
- return {
- restrict: 'ECA',
- priority: -400,
- require: 'ngInclude',
- link: function(scope, $element, $attr, ctrl) {
- if (toString.call($element[0]).match(/SVG/)) {
- // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
- // support innerHTML, so detect this here and try to generate the contents
- // specially.
- $element.empty();
- $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope,
- function namespaceAdaptedClone(clone) {
- $element.append(clone);
- }, {futureParentElement: $element});
- return;
- }
-
- $element.html(ctrl.template);
- $compile($element.contents())(scope);
- }
- };
- }];
-
-/**
- * @ngdoc directive
- * @name ngInit
- * @restrict AC
- *
- * @description
- * The `ngInit` directive allows you to evaluate an expression in the
- * current scope.
- *
- * <div class="alert alert-danger">
- * This directive can be abused to add unnecessary amounts of logic into your templates.
- * There are only a few appropriate uses of `ngInit`, such as for aliasing special properties of
- * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below; and for injecting data via
- * server side scripting. Besides these few cases, you should use {@link guide/controller controllers}
- * rather than `ngInit` to initialize values on a scope.
- * </div>
- *
- * <div class="alert alert-warning">
- * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make
- * sure you have parentheses to ensure correct operator precedence:
- * <pre class="prettyprint">
- * `<div ng-init="test1 = ($index | toString)"></div>`
- * </pre>
- * </div>
- *
- * @priority 450
- *
- * @element ANY
- * @param {expression} ngInit {@link guide/expression Expression} to eval.
- *
- * @example
- <example module="initExample">
- <file name="index.html">
- <script>
- angular.module('initExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.list = [['a', 'b'], ['c', 'd']];
- }]);
- </script>
- <div ng-controller="ExampleController">
- <div ng-repeat="innerList in list" ng-init="outerIndex = $index">
- <div ng-repeat="value in innerList" ng-init="innerIndex = $index">
- <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span>
- </div>
- </div>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should alias index positions', function() {
- var elements = element.all(by.css('.example-init'));
- expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;');
- expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;');
- expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;');
- expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;');
- });
- </file>
- </example>
- */
-var ngInitDirective = ngDirective({
- priority: 450,
- compile: function() {
- return {
- pre: function(scope, element, attrs) {
- scope.$eval(attrs.ngInit);
- }
- };
- }
-});
-
-/**
- * @ngdoc directive
- * @name ngList
- *
- * @description
- * Text input that converts between a delimited string and an array of strings. The default
- * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom
- * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`.
- *
- * The behaviour of the directive is affected by the use of the `ngTrim` attribute.
- * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
- * list item is respected. This implies that the user of the directive is responsible for
- * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
- * tab or newline character.
- * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
- * when joining the list items back together) and whitespace around each list item is stripped
- * before it is added to the model.
- *
- * ### Example with Validation
- *
- * <example name="ngList-directive" module="listExample">
- * <file name="app.js">
- * angular.module('listExample', [])
- * .controller('ExampleController', ['$scope', function($scope) {
- * $scope.names = ['morpheus', 'neo', 'trinity'];
- * }]);
- * </file>
- * <file name="index.html">
- * <form name="myForm" ng-controller="ExampleController">
- * <label>List: <input name="namesInput" ng-model="names" ng-list required></label>
- * <span role="alert">
- * <span class="error" ng-show="myForm.namesInput.$error.required">
- * Required!</span>
- * </span>
- * <br>
- * <tt>names = {{names}}</tt><br/>
- * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
- * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
- * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
- * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
- * </form>
- * </file>
- * <file name="protractor.js" type="protractor">
- * var listInput = element(by.model('names'));
- * var names = element(by.exactBinding('names'));
- * var valid = element(by.binding('myForm.namesInput.$valid'));
- * var error = element(by.css('span.error'));
- *
- * it('should initialize to model', function() {
- * expect(names.getText()).toContain('["morpheus","neo","trinity"]');
- * expect(valid.getText()).toContain('true');
- * expect(error.getCssValue('display')).toBe('none');
- * });
- *
- * it('should be invalid if empty', function() {
- * listInput.clear();
- * listInput.sendKeys('');
- *
- * expect(names.getText()).toContain('');
- * expect(valid.getText()).toContain('false');
- * expect(error.getCssValue('display')).not.toBe('none');
- * });
- * </file>
- * </example>
- *
- * ### Example - splitting on newline
- * <example name="ngList-directive-newlines">
- * <file name="index.html">
- * <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
- * <pre>{{ list | json }}</pre>
- * </file>
- * <file name="protractor.js" type="protractor">
- * it("should split the text by newlines", function() {
- * var listInput = element(by.model('list'));
- * var output = element(by.binding('list | json'));
- * listInput.sendKeys('abc\ndef\nghi');
- * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
- * });
- * </file>
- * </example>
- *
- * @element input
- * @param {string=} ngList optional delimiter that should be used to split the value.
- */
-var ngListDirective = function() {
- return {
- restrict: 'A',
- priority: 100,
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- // We want to control whitespace trimming so we use this convoluted approach
- // to access the ngList attribute, which doesn't pre-trim the attribute
- var ngList = element.attr(attr.$attr.ngList) || ', ';
- var trimValues = attr.ngTrim !== 'false';
- var separator = trimValues ? trim(ngList) : ngList;
-
- var parse = function(viewValue) {
- // If the viewValue is invalid (say required but empty) it will be `undefined`
- if (isUndefined(viewValue)) return;
-
- var list = [];
-
- if (viewValue) {
- forEach(viewValue.split(separator), function(value) {
- if (value) list.push(trimValues ? trim(value) : value);
- });
- }
-
- return list;
- };
-
- ctrl.$parsers.push(parse);
- ctrl.$formatters.push(function(value) {
- if (isArray(value)) {
- return value.join(ngList);
- }
-
- return undefined;
- });
-
- // Override the standard $isEmpty because an empty array means the input is empty.
- ctrl.$isEmpty = function(value) {
- return !value || !value.length;
- };
- }
- };
-};
-
-/* global VALID_CLASS: true,
- INVALID_CLASS: true,
- PRISTINE_CLASS: true,
- DIRTY_CLASS: true,
- UNTOUCHED_CLASS: true,
- TOUCHED_CLASS: true,
-*/
-
-var VALID_CLASS = 'ng-valid',
- INVALID_CLASS = 'ng-invalid',
- PRISTINE_CLASS = 'ng-pristine',
- DIRTY_CLASS = 'ng-dirty',
- UNTOUCHED_CLASS = 'ng-untouched',
- TOUCHED_CLASS = 'ng-touched',
- PENDING_CLASS = 'ng-pending',
- EMPTY_CLASS = 'ng-empty',
- NOT_EMPTY_CLASS = 'ng-not-empty';
-
-var ngModelMinErr = minErr('ngModel');
-
-/**
- * @ngdoc type
- * @name ngModel.NgModelController
- *
- * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a
- * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue
- * is set.
- * @property {*} $modelValue The value in the model that the control is bound to.
- * @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
- the control reads value from the DOM. The functions are called in array order, each passing
- its return value through to the next. The last return value is forwarded to the
- {@link ngModel.NgModelController#$validators `$validators`} collection.
-
-Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
-`$viewValue`}.
-
-Returning `undefined` from a parser means a parse error occurred. In that case,
-no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
-will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
-is set to `true`. The parse error is stored in `ngModel.$error.parse`.
-
- *
- * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever
- the model value changes. The functions are called in reverse array order, each passing the value through to the
- next. The last return value is used as the actual DOM value.
- Used to format / convert values for display in the control.
- * ```js
- * function formatter(value) {
- * if (value) {
- * return value.toUpperCase();
- * }
- * }
- * ngModel.$formatters.push(formatter);
- * ```
- *
- * @property {Object.<string, function>} $validators A collection of validators that are applied
- * whenever the model value changes. The key value within the object refers to the name of the
- * validator while the function refers to the validation operation. The validation operation is
- * provided with the model value as an argument and must return a true or false value depending
- * on the response of that validation.
- *
- * ```js
- * ngModel.$validators.validCharacters = function(modelValue, viewValue) {
- * var value = modelValue || viewValue;
- * return /[0-9]+/.test(value) &&
- * /[a-z]+/.test(value) &&
- * /[A-Z]+/.test(value) &&
- * /\W+/.test(value);
- * };
- * ```
- *
- * @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to
- * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
- * is expected to return a promise when it is run during the model validation process. Once the promise
- * is delivered then the validation status will be set to true when fulfilled and false when rejected.
- * When the asynchronous validators are triggered, each of the validators will run in parallel and the model
- * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator
- * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators
- * will only run once all synchronous validators have passed.
- *
- * Please note that if $http is used then it is important that the server returns a success HTTP response code
- * in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
- *
- * ```js
- * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
- * var value = modelValue || viewValue;
- *
- * // Lookup user by username
- * return $http.get('/api/users/' + value).
- * then(function resolved() {
- * //username exists, this means validation fails
- * return $q.reject('exists');
- * }, function rejected() {
- * //username does not exist, therefore this validation passes
- * return true;
- * });
- * };
- * ```
- *
- * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
- * view value has changed. It is called with no arguments, and its return value is ignored.
- * This can be used in place of additional $watches against the model value.
- *
- * @property {Object} $error An object hash with all failing validator ids as keys.
- * @property {Object} $pending An object hash with all pending validator ids as keys.
- *
- * @property {boolean} $untouched True if control has not lost focus yet.
- * @property {boolean} $touched True if control has lost focus.
- * @property {boolean} $pristine True if user has not interacted with the control yet.
- * @property {boolean} $dirty True if user has already interacted with the control.
- * @property {boolean} $valid True if there is no error.
- * @property {boolean} $invalid True if at least one error on the control.
- * @property {string} $name The name attribute of the control.
- *
- * @description
- *
- * `NgModelController` provides API for the {@link ngModel `ngModel`} directive.
- * The controller contains services for data-binding, validation, CSS updates, and value formatting
- * and parsing. It purposefully does not contain any logic which deals with DOM rendering or
- * listening to DOM events.
- * Such DOM related logic should be provided by other directives which make use of
- * `NgModelController` for data-binding to control elements.
- * Angular provides this DOM logic for most {@link input `input`} elements.
- * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example
- * custom control example} that uses `ngModelController` to bind to `contenteditable` elements.
- *
- * @example
- * ### Custom Control Example
- * This example shows how to use `NgModelController` with a custom control to achieve
- * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
- * collaborate together to achieve the desired result.
- *
- * `contenteditable` is an HTML5 attribute, which tells the browser to let the element
- * contents be edited in place by the user.
- *
- * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize}
- * module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`).
- * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks
- * that content using the `$sce` service.
- *
- * <example name="NgModelController" module="customControl" deps="angular-sanitize.js">
- <file name="style.css">
- [contenteditable] {
- border: 1px solid black;
- background-color: white;
- min-height: 20px;
- }
-
- .ng-invalid {
- border: 1px solid red;
- }
-
- </file>
- <file name="script.js">
- angular.module('customControl', ['ngSanitize']).
- directive('contenteditable', ['$sce', function($sce) {
- return {
- restrict: 'A', // only activate on element attribute
- require: '?ngModel', // get a hold of NgModelController
- link: function(scope, element, attrs, ngModel) {
- if (!ngModel) return; // do nothing if no ng-model
-
- // Specify how UI should be updated
- ngModel.$render = function() {
- element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
- };
-
- // Listen for change events to enable binding
- element.on('blur keyup change', function() {
- scope.$evalAsync(read);
- });
- read(); // initialize
-
- // Write data to the model
- function read() {
- var html = element.html();
- // When we clear the content editable the browser leaves a <br> behind
- // If strip-br attribute is provided then we strip this out
- if ( attrs.stripBr && html == '<br>' ) {
- html = '';
- }
- ngModel.$setViewValue(html);
- }
- }
- };
- }]);
- </file>
- <file name="index.html">
- <form name="myForm">
- <div contenteditable
- name="myWidget" ng-model="userContent"
- strip-br="true"
- required>Change me!</div>
- <span ng-show="myForm.myWidget.$error.required">Required!</span>
- <hr>
- <textarea ng-model="userContent" aria-label="Dynamic textarea"></textarea>
- </form>
- </file>
- <file name="protractor.js" type="protractor">
- it('should data-bind and become invalid', function() {
- if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
- // SafariDriver can't handle contenteditable
- // and Firefox driver can't clear contenteditables very well
- return;
- }
- var contentEditable = element(by.css('[contenteditable]'));
- var content = 'Change me!';
-
- expect(contentEditable.getText()).toEqual(content);
-
- contentEditable.clear();
- contentEditable.sendKeys(protractor.Key.BACK_SPACE);
- expect(contentEditable.getText()).toEqual('');
- expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
- });
- </file>
- * </example>
- *
- *
- */
-var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
- function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
- this.$viewValue = Number.NaN;
- this.$modelValue = Number.NaN;
- this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
- this.$validators = {};
- this.$asyncValidators = {};
- this.$parsers = [];
- this.$formatters = [];
- this.$viewChangeListeners = [];
- this.$untouched = true;
- this.$touched = false;
- this.$pristine = true;
- this.$dirty = false;
- this.$valid = true;
- this.$invalid = false;
- this.$error = {}; // keep invalid keys here
- this.$$success = {}; // keep valid keys here
- this.$pending = undefined; // keep pending keys here
- this.$name = $interpolate($attr.name || '', false)($scope);
- this.$$parentForm = nullFormCtrl;
-
- var parsedNgModel = $parse($attr.ngModel),
- parsedNgModelAssign = parsedNgModel.assign,
- ngModelGet = parsedNgModel,
- ngModelSet = parsedNgModelAssign,
- pendingDebounce = null,
- parserValid,
- ctrl = this;
-
- this.$$setOptions = function(options) {
- ctrl.$options = options;
- if (options && options.getterSetter) {
- var invokeModelGetter = $parse($attr.ngModel + '()'),
- invokeModelSetter = $parse($attr.ngModel + '($$$p)');
-
- ngModelGet = function($scope) {
- var modelValue = parsedNgModel($scope);
- if (isFunction(modelValue)) {
- modelValue = invokeModelGetter($scope);
- }
- return modelValue;
- };
- ngModelSet = function($scope, newValue) {
- if (isFunction(parsedNgModel($scope))) {
- invokeModelSetter($scope, {$$$p: newValue});
- } else {
- parsedNgModelAssign($scope, newValue);
- }
- };
- } else if (!parsedNgModel.assign) {
- throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
- $attr.ngModel, startingTag($element));
- }
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$render
- *
- * @description
- * Called when the view needs to be updated. It is expected that the user of the ng-model
- * directive will implement this method.
- *
- * The `$render()` method is invoked in the following situations:
- *
- * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last
- * committed value then `$render()` is called to update the input control.
- * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and
- * the `$viewValue` are different from last time.
- *
- * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
- * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue`
- * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
- * invoked if you only change a property on the objects.
- */
- this.$render = noop;
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$isEmpty
- *
- * @description
- * This is called when we need to determine if the value of an input is empty.
- *
- * For instance, the required directive does this to work out if the input has data or not.
- *
- * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
- *
- * You can override this for input directives whose concept of being empty is different from the
- * default. The `checkboxInputType` directive does this because in its case a value of `false`
- * implies empty.
- *
- * @param {*} value The value of the input to check for emptiness.
- * @returns {boolean} True if `value` is "empty".
- */
- this.$isEmpty = function(value) {
- return isUndefined(value) || value === '' || value === null || value !== value;
- };
-
- this.$$updateEmptyClasses = function(value) {
- if (ctrl.$isEmpty(value)) {
- $animate.removeClass($element, NOT_EMPTY_CLASS);
- $animate.addClass($element, EMPTY_CLASS);
- } else {
- $animate.removeClass($element, EMPTY_CLASS);
- $animate.addClass($element, NOT_EMPTY_CLASS);
- }
- };
-
-
- var currentValidationRunId = 0;
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setValidity
- *
- * @description
- * Change the validity state, and notify the form.
- *
- * This method can be called within $parsers/$formatters or a custom validation implementation.
- * However, in most cases it should be sufficient to use the `ngModel.$validators` and
- * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
- *
- * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
- * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
- * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
- * The `validationErrorKey` should be in camelCase and will get converted into dash-case
- * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
- * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
- * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
- * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
- * Skipped is used by Angular when validators do not run because of parse errors and
- * when `$asyncValidators` do not run because any of the `$validators` failed.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: $element,
- set: function(object, property) {
- object[property] = true;
- },
- unset: function(object, property) {
- delete object[property];
- },
- $animate: $animate
- });
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setPristine
- *
- * @description
- * Sets the control to its pristine state.
- *
- * This method can be called to remove the `ng-dirty` class and set the control to its pristine
- * state (`ng-pristine` class). A model is considered to be pristine when the control
- * has not been changed from when first compiled.
- */
- this.$setPristine = function() {
- ctrl.$dirty = false;
- ctrl.$pristine = true;
- $animate.removeClass($element, DIRTY_CLASS);
- $animate.addClass($element, PRISTINE_CLASS);
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setDirty
- *
- * @description
- * Sets the control to its dirty state.
- *
- * This method can be called to remove the `ng-pristine` class and set the control to its dirty
- * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
- * from when first compiled.
- */
- this.$setDirty = function() {
- ctrl.$dirty = true;
- ctrl.$pristine = false;
- $animate.removeClass($element, PRISTINE_CLASS);
- $animate.addClass($element, DIRTY_CLASS);
- ctrl.$$parentForm.$setDirty();
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setUntouched
- *
- * @description
- * Sets the control to its untouched state.
- *
- * This method can be called to remove the `ng-touched` class and set the control to its
- * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched
- * by default, however this function can be used to restore that state if the model has
- * already been touched by the user.
- */
- this.$setUntouched = function() {
- ctrl.$touched = false;
- ctrl.$untouched = true;
- $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setTouched
- *
- * @description
- * Sets the control to its touched state.
- *
- * This method can be called to remove the `ng-untouched` class and set the control to its
- * touched state (`ng-touched` class). A model is considered to be touched when the user has
- * first focused the control element and then shifted focus away from the control (blur event).
- */
- this.$setTouched = function() {
- ctrl.$touched = true;
- ctrl.$untouched = false;
- $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$rollbackViewValue
- *
- * @description
- * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
- * which may be caused by a pending debounced event or because the input is waiting for a some
- * future event.
- *
- * If you have an input that uses `ng-model-options` to set up debounced updates or updates that
- * depend on special events such as blur, you can have a situation where there is a period when
- * the `$viewValue` is out of sync with the ngModel's `$modelValue`.
- *
- * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update
- * and reset the input to the last committed view value.
- *
- * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue`
- * programmatically before these debounced/future events have resolved/occurred, because Angular's
- * dirty checking mechanism is not able to tell whether the model has actually changed or not.
- *
- * The `$rollbackViewValue()` method should be called before programmatically changing the model of an
- * input which may have such events pending. This is important in order to make sure that the
- * input field will be updated with the new model value and any pending operations are cancelled.
- *
- * <example name="ng-model-cancel-update" module="cancel-update-example">
- * <file name="app.js">
- * angular.module('cancel-update-example', [])
- *
- * .controller('CancelUpdateController', ['$scope', function($scope) {
- * $scope.model = {};
- *
- * $scope.setEmpty = function(e, value, rollback) {
- * if (e.keyCode == 27) {
- * e.preventDefault();
- * if (rollback) {
- * $scope.myForm[value].$rollbackViewValue();
- * }
- * $scope.model[value] = '';
- * }
- * };
- * }]);
- * </file>
- * <file name="index.html">
- * <div ng-controller="CancelUpdateController">
- * <p>Both of these inputs are only updated if they are blurred. Hitting escape should
- * empty them. Follow these steps and observe the difference:</p>
- * <ol>
- * <li>Type something in the input. You will see that the model is not yet updated</li>
- * <li>Press the Escape key.
- * <ol>
- * <li> In the first example, nothing happens, because the model is already '', and no
- * update is detected. If you blur the input, the model will be set to the current view.
- * </li>
- * <li> In the second example, the pending update is cancelled, and the input is set back
- * to the last committed view value (''). Blurring the input does nothing.
- * </li>
- * </ol>
- * </li>
- * </ol>
- *
- * <form name="myForm" ng-model-options="{ updateOn: 'blur' }">
- * <div>
- * <p id="inputDescription1">Without $rollbackViewValue():</p>
- * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1"
- * ng-keydown="setEmpty($event, 'value1')">
- * value1: "{{ model.value1 }}"
- * </div>
- *
- * <div>
- * <p id="inputDescription2">With $rollbackViewValue():</p>
- * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2"
- * ng-keydown="setEmpty($event, 'value2', true)">
- * value2: "{{ model.value2 }}"
- * </div>
- * </form>
- * </div>
- * </file>
- <file name="style.css">
- div {
- display: table-cell;
- }
- div:nth-child(1) {
- padding-right: 30px;
- }
-
- </file>
- * </example>
- */
- this.$rollbackViewValue = function() {
- $timeout.cancel(pendingDebounce);
- ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
- ctrl.$render();
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$validate
- *
- * @description
- * Runs each of the registered validators (first synchronous validators and then
- * asynchronous validators).
- * If the validity changes to invalid, the model will be set to `undefined`,
- * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`.
- * If the validity changes to valid, it will set the model to the last available valid
- * `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
- */
- this.$validate = function() {
- // ignore $validate before model is initialized
- if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
- return;
- }
-
- var viewValue = ctrl.$$lastCommittedViewValue;
- // Note: we use the $$rawModelValue as $modelValue might have been
- // set to undefined during a view -> model update that found validation
- // errors. We can't parse the view here, since that could change
- // the model although neither viewValue nor the model on the scope changed
- var modelValue = ctrl.$$rawModelValue;
-
- var prevValid = ctrl.$valid;
- var prevModelValue = ctrl.$modelValue;
-
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
-
- ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
- // If there was no change in validity, don't update the model
- // This prevents changing an invalid modelValue to undefined
- if (!allowInvalid && prevValid !== allValid) {
- // Note: Don't check ctrl.$valid here, as we could have
- // external validators (e.g. calculated on the server),
- // that just call $setValidity and need the model value
- // to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
-
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
- }
- }
- });
-
- };
-
- this.$$runValidators = function(modelValue, viewValue, doneCallback) {
- currentValidationRunId++;
- var localValidationRunId = currentValidationRunId;
-
- // check parser error
- if (!processParseErrors()) {
- validationDone(false);
- return;
- }
- if (!processSyncValidators()) {
- validationDone(false);
- return;
- }
- processAsyncValidators();
-
- function processParseErrors() {
- var errorKey = ctrl.$$parserName || 'parse';
- if (isUndefined(parserValid)) {
- setValidity(errorKey, null);
- } else {
- if (!parserValid) {
- forEach(ctrl.$validators, function(v, name) {
- setValidity(name, null);
- });
- forEach(ctrl.$asyncValidators, function(v, name) {
- setValidity(name, null);
- });
- }
- // Set the parse error last, to prevent unsetting it, should a $validators key == parserName
- setValidity(errorKey, parserValid);
- return parserValid;
- }
- return true;
- }
-
- function processSyncValidators() {
- var syncValidatorsValid = true;
- forEach(ctrl.$validators, function(validator, name) {
- var result = validator(modelValue, viewValue);
- syncValidatorsValid = syncValidatorsValid && result;
- setValidity(name, result);
- });
- if (!syncValidatorsValid) {
- forEach(ctrl.$asyncValidators, function(v, name) {
- setValidity(name, null);
- });
- return false;
- }
- return true;
- }
-
- function processAsyncValidators() {
- var validatorPromises = [];
- var allValid = true;
- forEach(ctrl.$asyncValidators, function(validator, name) {
- var promise = validator(modelValue, viewValue);
- if (!isPromiseLike(promise)) {
- throw ngModelMinErr('nopromise',
- "Expected asynchronous validator to return a promise but got '{0}' instead.", promise);
- }
- setValidity(name, undefined);
- validatorPromises.push(promise.then(function() {
- setValidity(name, true);
- }, function() {
- allValid = false;
- setValidity(name, false);
- }));
- });
- if (!validatorPromises.length) {
- validationDone(true);
- } else {
- $q.all(validatorPromises).then(function() {
- validationDone(allValid);
- }, noop);
- }
- }
-
- function setValidity(name, isValid) {
- if (localValidationRunId === currentValidationRunId) {
- ctrl.$setValidity(name, isValid);
- }
- }
-
- function validationDone(allValid) {
- if (localValidationRunId === currentValidationRunId) {
-
- doneCallback(allValid);
- }
- }
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$commitViewValue
- *
- * @description
- * Commit a pending update to the `$modelValue`.
- *
- * Updates may be pending by a debounced event or because the input is waiting for a some future
- * event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
- * usually handles calling this in response to input events.
- */
- this.$commitViewValue = function() {
- var viewValue = ctrl.$viewValue;
-
- $timeout.cancel(pendingDebounce);
-
- // If the view value has not changed then we should just exit, except in the case where there is
- // a native validator on the element. In this case the validation state may have changed even though
- // the viewValue has stayed empty.
- if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
- return;
- }
- ctrl.$$updateEmptyClasses(viewValue);
- ctrl.$$lastCommittedViewValue = viewValue;
-
- // change to dirty
- if (ctrl.$pristine) {
- this.$setDirty();
- }
- this.$$parseAndValidate();
- };
-
- this.$$parseAndValidate = function() {
- var viewValue = ctrl.$$lastCommittedViewValue;
- var modelValue = viewValue;
- 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($scope);
- }
- var prevModelValue = ctrl.$modelValue;
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
- ctrl.$$rawModelValue = modelValue;
-
- if (allowInvalid) {
- ctrl.$modelValue = modelValue;
- writeToModelIfNeeded();
- }
-
- // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
- // This can happen if e.g. $setViewValue is called from inside a parser
- ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
- if (!allowInvalid) {
- // Note: Don't check ctrl.$valid here, as we could have
- // external validators (e.g. calculated on the server),
- // that just call $setValidity and need the model value
- // to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
- writeToModelIfNeeded();
- }
- });
-
- function writeToModelIfNeeded() {
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
- }
- }
- };
-
- this.$$writeModelToScope = function() {
- ngModelSet($scope, ctrl.$modelValue);
- forEach(ctrl.$viewChangeListeners, function(listener) {
- try {
- listener();
- } catch (e) {
- $exceptionHandler(e);
- }
- });
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setViewValue
- *
- * @description
- * Update the view value.
- *
- * This method should be called when a control wants to change the view value; typically,
- * this is done from within a DOM event handler. For example, the {@link ng.directive:input input}
- * directive calls it when the value of the input changes and {@link ng.directive:select select}
- * calls it when an option is selected.
- *
- * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers`
- * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
- * value sent directly for processing, finally to be applied to `$modelValue` and then the
- * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners,
- * in the `$viewChangeListeners` list, are called.
- *
- * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
- * and the `default` trigger is not listed, all those actions will remain pending until one of the
- * `updateOn` events is triggered on the DOM element.
- * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
- * directive is used with a custom debounce for this particular event.
- * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce`
- * is specified, once the timer runs out.
- *
- * When used with standard inputs, the view value will always be a string (which is in some cases
- * parsed into another type, such as a `Date` object for `input[date]`.)
- * However, custom controls might also pass objects to this method. In this case, we should make
- * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not
- * perform a deep watch of objects, it only looks for a change of identity. If you only change
- * the property of the object then ngModel will not realize that the object has changed and
- * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should
- * not change properties of the copy once it has been passed to `$setViewValue`.
- * Otherwise you may cause the model value on the scope to change incorrectly.
- *
- * <div class="alert alert-info">
- * In any case, the value passed to the method should always reflect the current value
- * of the control. For example, if you are calling `$setViewValue` for an input element,
- * you should pass the input DOM value. Otherwise, the control and the scope model become
- * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change
- * the control's DOM value in any way. If we want to change the control's DOM value
- * programmatically, we should update the `ngModel` scope expression. Its new value will be
- * picked up by the model controller, which will run it through the `$formatters`, `$render` it
- * to update the DOM, and finally call `$validate` on it.
- * </div>
- *
- * @param {*} value value from the view.
- * @param {string} trigger Event that triggered the update.
- */
- this.$setViewValue = function(value, trigger) {
- ctrl.$viewValue = value;
- if (!ctrl.$options || ctrl.$options.updateOnDefault) {
- ctrl.$$debounceViewValueCommit(trigger);
- }
- };
-
- this.$$debounceViewValueCommit = function(trigger) {
- var debounceDelay = 0,
- options = ctrl.$options,
- debounce;
-
- if (options && isDefined(options.debounce)) {
- debounce = options.debounce;
- if (isNumber(debounce)) {
- debounceDelay = debounce;
- } else if (isNumber(debounce[trigger])) {
- debounceDelay = debounce[trigger];
- } else if (isNumber(debounce['default'])) {
- debounceDelay = debounce['default'];
- }
- }
-
- $timeout.cancel(pendingDebounce);
- if (debounceDelay) {
- pendingDebounce = $timeout(function() {
- ctrl.$commitViewValue();
- }, debounceDelay);
- } else if ($rootScope.$$phase) {
- ctrl.$commitViewValue();
- } else {
- $scope.$apply(function() {
- ctrl.$commitViewValue();
- });
- }
- };
-
- // model -> value
- // Note: we cannot use a normal scope.$watch as we want to detect the following:
- // 1. scope value is 'a'
- // 2. user enters 'b'
- // 3. ng-change kicks in and reverts scope value to 'a'
- // -> scope value did not change since the last digest as
- // ng-change executes in apply phase
- // 4. view should be changed back to 'a'
- $scope.$watch(function ngModelWatch() {
- var modelValue = ngModelGet($scope);
-
- // if scope model value and ngModel value are out of sync
- // TODO(perf): why not move this to the action fn?
- if (modelValue !== ctrl.$modelValue &&
- // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
- (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
- ) {
- ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
- parserValid = undefined;
-
- var formatters = ctrl.$formatters,
- idx = formatters.length;
-
- var viewValue = modelValue;
- while (idx--) {
- viewValue = formatters[idx](viewValue);
}
- if (ctrl.$viewValue !== viewValue) {
- ctrl.$$updateEmptyClasses(viewValue);
- ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
- ctrl.$render();
-
- ctrl.$$runValidators(modelValue, viewValue, noop);
- }
- }
-
- return modelValue;
- });
-}];
-
-
-/**
- * @ngdoc directive
- * @name ngModel
- *
- * @element input
- * @priority 1
- *
- * @description
- * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
- * property on the scope using {@link ngModel.NgModelController NgModelController},
- * which is created and exposed by this directive.
- *
- * `ngModel` is responsible for:
- *
- * - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
- * require.
- * - Providing validation behavior (i.e. required, number, email, url).
- * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
- * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`,
- * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations.
- * - Registering the control with its parent {@link ng.directive:form form}.
- *
- * Note: `ngModel` will try to bind to the property given by evaluating the expression on the
- * current scope. If the property doesn't already exist on this scope, it will be created
- * implicitly and added to the scope.
- *
- * For best practices on using `ngModel`, see:
- *
- * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes)
- *
- * For basic examples, how to use `ngModel`, see:
- *
- * - {@link ng.directive:input input}
- * - {@link input[text] text}
- * - {@link input[checkbox] checkbox}
- * - {@link input[radio] radio}
- * - {@link input[number] number}
- * - {@link input[email] email}
- * - {@link input[url] url}
- * - {@link input[date] date}
- * - {@link input[datetime-local] datetime-local}
- * - {@link input[time] time}
- * - {@link input[month] month}
- * - {@link input[week] week}
- * - {@link ng.directive:select select}
- * - {@link ng.directive:textarea textarea}
- *
- * # Complex Models (objects or collections)
- *
- * By default, `ngModel` watches the model by reference, not value. This is important to know when
- * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the
- * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered.
- *
- * The model must be assigned an entirely new object or collection before a re-rendering will occur.
- *
- * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression
- * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or
- * if the select is given the `multiple` attribute.
- *
- * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the
- * first level of the object (or only changing the properties of an item in the collection if it's an array) will still
- * not trigger a re-rendering of the model.
- *
- * # CSS classes
- * The following CSS classes are added and removed on the associated input/select/textarea element
- * depending on the validity of the model.
- *
- * - `ng-valid`: the model is valid
- * - `ng-invalid`: the model is invalid
- * - `ng-valid-[key]`: for each valid key added by `$setValidity`
- * - `ng-invalid-[key]`: for each invalid key added by `$setValidity`
- * - `ng-pristine`: the control hasn't been interacted with yet
- * - `ng-dirty`: the control has been interacted with
- * - `ng-touched`: the control has been blurred
- * - `ng-untouched`: the control hasn't been blurred
- * - `ng-pending`: any `$asyncValidators` are unfulfilled
- * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined
- * by the {@link ngModel.NgModelController#$isEmpty} method
- * - `ng-not-empty`: the view contains a non-empty value
- *
- * Keep in mind that ngAnimate can detect each of these classes when added and removed.
- *
- * ## Animation Hooks
- *
- * Animations within models are triggered when any of the associated CSS classes are added and removed
- * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`,
- * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
- * The animations that are triggered within ngModel are similar to how they work in ngClass and
- * animations can be hooked into using CSS transitions, keyframes as well as JS animations.
- *
- * The following example shows a simple way to utilize CSS transitions to style an input element
- * that has been rendered as invalid after it has been validated:
- *
- * <pre>
- * //be sure to include ngAnimate as a module to hook into more
- * //advanced animations
- * .my-input {
- * transition:0.5s linear all;
- * background: white;
- * }
- * .my-input.ng-invalid {
- * background: red;
- * color:white;
- * }
- * </pre>
- *
- * @example
- * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample">
- <file name="index.html">
- <script>
- angular.module('inputExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.val = '1';
- }]);
- </script>
- <style>
- .my-input {
- transition:all linear 0.5s;
- background: transparent;
- }
- .my-input.ng-invalid {
- color:white;
- background: red;
- }
- </style>
- <p id="inputDescription">
- Update input to see transitions when valid/invalid.
- Integer is a valid value.
- </p>
- <form name="testForm" ng-controller="ExampleController">
- <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input"
- aria-describedby="inputDescription" />
- </form>
- </file>
- * </example>
- *
- * ## Binding to a getter/setter
- *
- * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
- * function that returns a representation of the model when called with zero arguments, and sets
- * the internal state of a model when called with an argument. It's sometimes useful to use this
- * for models that have an internal representation that's different from what the model exposes
- * to the view.
- *
- * <div class="alert alert-success">
- * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more
- * frequently than other parts of your code.
- * </div>
- *
- * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that
- * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to
- * a `<form>`, which will enable this behavior for all `<input>`s within it. See
- * {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
- *
- * The following example shows how to use `ngModel` with a getter/setter:
- *
- * @example
- * <example name="ngModel-getter-setter" module="getterSetterExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ getterSetter: true }" />
- </label>
- </form>
- <pre>user.name = <span ng-bind="user.name()"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('getterSetterExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- var _name = 'Brian';
- $scope.user = {
- name: function(newName) {
- // Note that newName can be undefined for two reasons:
- // 1. Because it is called as a getter and thus called with no arguments
- // 2. Because the property should actually be set to undefined. This happens e.g. if the
- // input is invalid
- return arguments.length ? (_name = newName) : _name;
- }
- };
- }]);
- </file>
- * </example>
- */
-var ngModelDirective = ['$rootScope', function($rootScope) {
- return {
- restrict: 'A',
- require: ['ngModel', '^?form', '^?ngModelOptions'],
- controller: NgModelController,
- // 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);
-
- return {
- pre: function ngModelPreLink(scope, element, attr, ctrls) {
- var modelCtrl = ctrls[0],
- formCtrl = ctrls[1] || modelCtrl.$$parentForm;
-
- modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
-
- // notify others, especially parent forms
- formCtrl.$addControl(modelCtrl);
-
- attr.$observe('name', function(newValue) {
- if (modelCtrl.$name !== newValue) {
- modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue);
- }
- });
-
- scope.$on('$destroy', function() {
- modelCtrl.$$parentForm.$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);
- });
- }
-
- element.on('blur', function() {
- if (modelCtrl.$touched) return;
-
- if ($rootScope.$$phase) {
- scope.$evalAsync(modelCtrl.$setTouched);
- } else {
- scope.$apply(modelCtrl.$setTouched);
- }
- });
- }
- };
}
};
-}];
-
-var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
-
-/**
- * @ngdoc directive
- * @name ngModelOptions
- *
- * @description
- * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
- * events that will trigger a model update and/or a debouncing delay so that the actual update only
- * takes place when a timer expires; this timer will be reset after another change takes place.
- *
- * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
- * be different from the value in the actual model. This means that if you update the model you
- * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
- * order to make sure it is synchronized with the model and that any debounced action is canceled.
- *
- * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
- * method is by making sure the input is placed inside a form that has a `name` attribute. This is
- * important because `form` controllers are published to the related scope under the name in their
- * `name` attribute.
- *
- * Any pending changes will take place immediately when an enclosing form is submitted via the
- * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
- * to have access to the updated model.
- *
- * `ngModelOptions` has an effect on the element it's declared on and its descendants.
- *
- * @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
- * - `updateOn`: string specifying which event should the input be bound to. You can set several
- * events using an space delimited list. There is a special event called `default` that
- * matches the default events belonging of the control.
- * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
- * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
- * custom value for each event. For example:
- * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
- * - `allowInvalid`: boolean value which indicates that the model can be set with values that did
- * not validate correctly instead of the default behavior of setting the model to undefined.
- * - `getterSetter`: boolean value which determines whether or not to treat functions bound to
- `ngModel` as getters/setters.
- * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
- * `<input type="date">`, `<input type="time">`, ... . It understands UTC/GMT and the
- * continental US time zone abbreviations, but for general use, use a time zone offset, for
- * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
- * If not specified, the timezone of the browser will be used.
- *
- * @example
-
- The following example shows how to override immediate updates. Changes on the inputs within the
- form will update the model only when the control loses focus (blur event). If `escape` key is
- pressed while the input field is focused, the value is reset to the value in the current model.
-
- <example name="ngModelOptions-directive-blur" module="optionsExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ updateOn: 'blur' }"
- ng-keyup="cancel($event)" />
- </label><br />
- <label>Other data:
- <input type="text" ng-model="user.data" />
- </label><br />
- </form>
- <pre>user.name = <span ng-bind="user.name"></span></pre>
- <pre>user.data = <span ng-bind="user.data"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'John', data: '' };
-
- $scope.cancel = function(e) {
- if (e.keyCode == 27) {
- $scope.userForm.userName.$rollbackViewValue();
- }
- };
- }]);
- </file>
- <file name="protractor.js" type="protractor">
- var model = element(by.binding('user.name'));
- var input = element(by.model('user.name'));
- var other = element(by.model('user.data'));
-
- it('should allow custom events', function() {
- input.sendKeys(' Doe');
- input.click();
- expect(model.getText()).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John Doe');
- });
-
- it('should $rollbackViewValue when model changes', function() {
- input.sendKeys(' Doe');
- expect(input.getAttribute('value')).toEqual('John Doe');
- input.sendKeys(protractor.Key.ESCAPE);
- expect(input.getAttribute('value')).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John');
- });
- </file>
- </example>
-
- This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
- If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
-
- <example name="ngModelOptions-directive-debounce" module="optionsExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ debounce: 1000 }" />
- </label>
- <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button>
- <br />
- </form>
- <pre>user.name = <span ng-bind="user.name"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'Igor' };
- }]);
- </file>
- </example>
-
- This one shows how to bind to getter/setters:
-
- <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ getterSetter: true }" />
- </label>
- </form>
- <pre>user.name = <span ng-bind="user.name()"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('getterSetterExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- var _name = 'Brian';
- $scope.user = {
- name: function(newName) {
- // Note that newName can be undefined for two reasons:
- // 1. Because it is called as a getter and thus called with no arguments
- // 2. Because the property should actually be set to undefined. This happens e.g. if the
- // input is invalid
- return arguments.length ? (_name = newName) : _name;
- }
- };
- }]);
- </file>
- </example>
- */
-var ngModelOptionsDirective = function() {
- return {
- restrict: 'A',
- controller: ['$scope', '$attrs', function($scope, $attrs) {
- var that = this;
- this.$options = copy($scope.$eval($attrs.ngModelOptions));
- // Allow adding/overriding bound events
- if (isDefined(this.$options.updateOn)) {
- this.$options.updateOnDefault = false;
- // extract "default" pseudo-event from list of events that can trigger a model update
- this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
- that.$options.updateOnDefault = true;
- return ' ';
- }));
- } else {
- this.$options.updateOnDefault = true;
- }
- }]
- };
};
-
-
-// helper methods
function addSetValidityMethod(context) {
- var ctrl = context.ctrl,
- $element = context.$element,
- classCache = {},
+ var clazz = context.clazz,
set = context.set,
- unset = context.unset,
- $animate = context.$animate;
+ unset = context.unset;
- classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
-
- ctrl.$setValidity = setValidity;
-
- function setValidity(validationErrorKey, state, controller) {
+ clazz.prototype.$setValidity = function(validationErrorKey, state, controller) {
if (isUndefined(state)) {
- createAndSet('$pending', validationErrorKey, controller);
+ createAndSet(this, '$pending', validationErrorKey, controller);
} else {
- unsetAndCleanup('$pending', validationErrorKey, controller);
+ unsetAndCleanup(this, '$pending', validationErrorKey, controller);
}
if (!isBoolean(state)) {
- unset(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
+ unset(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
} else {
if (state) {
- unset(ctrl.$error, validationErrorKey, controller);
- set(ctrl.$$success, validationErrorKey, controller);
+ unset(this.$error, validationErrorKey, controller);
+ set(this.$$success, validationErrorKey, controller);
} else {
- set(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
+ set(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
}
}
- if (ctrl.$pending) {
- cachedToggleClass(PENDING_CLASS, true);
- ctrl.$valid = ctrl.$invalid = undefined;
- toggleValidationCss('', null);
+ if (this.$pending) {
+ cachedToggleClass(this, PENDING_CLASS, true);
+ this.$valid = this.$invalid = undefined;
+ toggleValidationCss(this, '', null);
} else {
- cachedToggleClass(PENDING_CLASS, false);
- ctrl.$valid = isObjectEmpty(ctrl.$error);
- ctrl.$invalid = !ctrl.$valid;
- toggleValidationCss('', ctrl.$valid);
+ cachedToggleClass(this, PENDING_CLASS, false);
+ this.$valid = isObjectEmpty(this.$error);
+ this.$invalid = !this.$valid;
+ toggleValidationCss(this, '', this.$valid);
}
// re-read the state as the set/unset methods could have
- // combined state in ctrl.$error[validationError] (used for forms),
+ // combined state in this.$error[validationError] (used for forms),
// where setting/unsetting only increments/decrements the value,
// and does not replace it.
var combinedState;
- if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
+ if (this.$pending && this.$pending[validationErrorKey]) {
combinedState = undefined;
- } else if (ctrl.$error[validationErrorKey]) {
+ } else if (this.$error[validationErrorKey]) {
combinedState = false;
- } else if (ctrl.$$success[validationErrorKey]) {
+ } else if (this.$$success[validationErrorKey]) {
combinedState = true;
} else {
combinedState = null;
}
- toggleValidationCss(validationErrorKey, combinedState);
- ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
- }
+ toggleValidationCss(this, validationErrorKey, combinedState);
+ this.$$parentForm.$setValidity(validationErrorKey, combinedState, this);
+ };
- function createAndSet(name, value, controller) {
+ function createAndSet(ctrl, name, value, controller) {
if (!ctrl[name]) {
ctrl[name] = {};
}
set(ctrl[name], value, controller);
}
- function unsetAndCleanup(name, value, controller) {
+ function unsetAndCleanup(ctrl, name, value, controller) {
if (ctrl[name]) {
unset(ctrl[name], value, controller);
}
@@ -38402,21 +30499,21 @@ function addSetValidityMethod(context) {
}
}
- function cachedToggleClass(className, switchValue) {
- if (switchValue && !classCache[className]) {
- $animate.addClass($element, className);
- classCache[className] = true;
- } else if (!switchValue && classCache[className]) {
- $animate.removeClass($element, className);
- classCache[className] = false;
+ function cachedToggleClass(ctrl, className, switchValue) {
+ if (switchValue && !ctrl.$$classCache[className]) {
+ ctrl.$$animate.addClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = true;
+ } else if (!switchValue && ctrl.$$classCache[className]) {
+ ctrl.$$animate.removeClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = false;
}
}
- function toggleValidationCss(validationErrorKey, isValid) {
+ function toggleValidationCss(ctrl, validationErrorKey, isValid) {
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
- cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
- cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
+ cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true);
+ cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false);
}
}
@@ -38436,35 +30533,36 @@ function isObjectEmpty(obj) {
* @name ngNonBindable
* @restrict AC
* @priority 1000
+ * @element ANY
*
* @description
- * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current
- * DOM element. This is useful if the element contains what appears to be Angular directives and
- * bindings but which should be ignored by Angular. This could be the case if you have a site that
- * displays snippets of code, for instance.
- *
- * @element ANY
+ * The `ngNonBindable` directive tells AngularJS not to compile or bind the contents of the current
+ * DOM element, including directives on the element itself that have a lower priority than
+ * `ngNonBindable`. This is useful if the element contains what appears to be AngularJS directives
+ * and bindings but which should be ignored by AngularJS. This could be the case if you have a site
+ * that displays snippets of code, for instance.
*
* @example
* In this example there are two locations where a simple interpolation binding (`{{}}`) is present,
* but the one wrapped in `ngNonBindable` is left alone.
*
- * @example
- <example>
- <file name="index.html">
- <div>Normal: {{1 + 2}}</div>
- <div ng-non-bindable>Ignored: {{1 + 2}}</div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-non-bindable', function() {
- expect(element(by.binding('1 + 2')).getText()).toContain('3');
- expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/);
- });
- </file>
- </example>
+ <example name="ng-non-bindable">
+ <file name="index.html">
+ <div>Normal: {{1 + 2}}</div>
+ <div ng-non-bindable>Ignored: {{1 + 2}}</div>
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ng-non-bindable', function() {
+ expect(element(by.binding('1 + 2')).getText()).toContain('3');
+ expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/);
+ });
+ </file>
+ </example>
*/
var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+/* exported ngOptionsDirective */
+
/* global jqLiteRemove */
var ngOptionsMinErr = minErr('ngOptions');
@@ -38480,13 +30578,12 @@ var ngOptionsMinErr = minErr('ngOptions');
* elements for the `<select>` element using the array or object obtained by evaluating the
* `ngOptions` comprehension expression.
*
- * In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a
- * similar result. However, `ngOptions` provides some benefits such as reducing memory and
- * increasing speed by not creating a new scope for each repeated instance, as well as providing
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
- * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound
- * to a non-string value. This is because an option element can only be bound to string values at
- * present.
+ * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `<option>` elements instead of
+ * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
*
* When an item in the `<select>` menu is selected, the array element or object property
* represented by the selected option will be bound to the model identified by the `ngModel`
@@ -38575,13 +30672,8 @@ var ngOptionsMinErr = minErr('ngOptions');
* is not matched against any `<option>` and the `<select>` appears as having no selected value.
*
*
- * @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
- * `required` when you want to data-bind to the `required` attribute.
- * @param {comprehension_expression=} ngOptions in one of the following forms:
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {comprehension_expression} ngOptions in one of the following forms:
*
* * for array data sources:
* * `label` **`for`** `value` **`in`** `array`
@@ -38620,9 +30712,16 @@ var ngOptionsMinErr = minErr('ngOptions');
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
* even when the options are recreated (e.g. reloaded from the server).
+ * @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
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
* @example
- <example module="selectExample">
+ <example module="selectExample" name="select">
<file name="index.html">
<script>
angular.module('selectExample', [])
@@ -38695,9 +30794,9 @@ var ngOptionsMinErr = minErr('ngOptions');
</example>
*/
-// jshint maxlen: false
-// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
-var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
+/* eslint-disable max-len */
+// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555000000000666666666666600000007777777777777000000000000000888888888800000000000000000009999999999
+var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
// 1: value expression (valueFn)
// 2: label expression (displayFn)
// 3: group by expression (groupByFn)
@@ -38707,7 +30806,7 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s
// 7: object item value variable name
// 8: collection expression
// 9: track by expression
-// jshint maxlen: 100
+/* eslint-enable */
var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
@@ -38717,9 +30816,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var match = optionsExp.match(NG_OPTIONS_REGEXP);
if (!(match)) {
throw ngOptionsMinErr('iexp',
- "Expected expression in form of " +
- "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '{0}'. Element: {1}",
+ 'Expected expression in form of ' +
+ '\'_select_ (as _label_)? for (_key_,)?_value_ in _collection_\'' +
+ ' but got \'{0}\'. Element: {1}',
optionsExp, startingTag(selectElement));
}
@@ -38861,7 +30960,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
getViewValueFromOption: function(option) {
// If the viewValue could be an object that may be mutated by the application,
// we need to make a copy and not return the reference to the value on the option.
- return trackBy ? angular.copy(option.viewValue) : option.viewValue;
+ return trackBy ? copy(option.viewValue) : option.viewValue;
}
};
}
@@ -38869,7 +30968,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
}
- // we can't just jqLite('<option>') since jqLite is not smart enough
+ // Support: IE 9 only
+ // We can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
var optionTemplate = window.document.createElement('option'),
optGroupTemplate = window.document.createElement('optgroup');
@@ -38882,15 +30982,18 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// The emptyOption allows the application developer to provide their own custom "empty"
// option when the viewValue does not match any of the option values.
- var emptyOption;
for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
if (children[i].value === '') {
- emptyOption = children.eq(i);
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = children.eq(i);
break;
}
}
- var providedEmptyOption = !!emptyOption;
+ // The empty option will be compiled and rendered before we first generate the options
+ selectElement.empty();
+
+ var providedEmptyOption = !!selectCtrl.emptyOption;
var unknownOption = jqLite(optionTemplate.cloneNode(false));
unknownOption.val('?');
@@ -38902,39 +31005,25 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// we only need to create it once.
var listFragment = $document[0].createDocumentFragment();
- var renderEmptyOption = function() {
- if (!providedEmptyOption) {
- selectElement.prepend(emptyOption);
- }
- selectElement.val('');
- emptyOption.prop('selected', true); // needed for IE
- emptyOption.attr('selected', true);
- };
-
- var removeEmptyOption = function() {
- if (!providedEmptyOption) {
- emptyOption.remove();
- }
- };
-
-
- var renderUnknownOption = function() {
- selectElement.prepend(unknownOption);
- selectElement.val('?');
- unknownOption.prop('selected', true); // needed for IE
- unknownOption.attr('selected', true);
- };
-
- var removeUnknownOption = function() {
- unknownOption.remove();
+ // Overwrite the implementation. ngOptions doesn't use hashes
+ selectCtrl.generateUnknownOptionValue = function(val) {
+ return '?';
};
// Update the controller methods for multiple selectable options
if (!multiple) {
selectCtrl.writeValue = function writeNgOptionsValue(value) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+
+ var selectedOption = selectElement[0].options[selectElement[0].selectedIndex];
var option = options.getOptionFromViewValue(value);
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ if (selectedOption) selectedOption.removeAttribute('selected');
+
if (option) {
// Don't update the option when it is already selected.
// For example, the browser will select the first option by default. In that case,
@@ -38942,8 +31031,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// set always
if (selectElement[0].value !== option.selectValue) {
- removeUnknownOption();
- removeEmptyOption();
+ selectCtrl.removeUnknownOption();
selectElement[0].value = option.selectValue;
option.element.selected = true;
@@ -38951,13 +31039,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
option.element.setAttribute('selected', 'selected');
} else {
- if (value === null || providedEmptyOption) {
- removeUnknownOption();
- renderEmptyOption();
- } else {
- removeEmptyOption();
- renderUnknownOption();
- }
+ selectCtrl.selectUnknownOrEmptyOption(value);
}
};
@@ -38966,8 +31048,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var selectedOption = options.selectValueMap[selectElement.val()];
if (selectedOption && !selectedOption.disabled) {
- removeEmptyOption();
- removeUnknownOption();
+ selectCtrl.unselectEmptyOption();
+ selectCtrl.removeUnknownOption();
return options.getViewValueFromOption(selectedOption);
}
return null;
@@ -38975,6 +31057,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// If we are using `track by` then we must watch the tracked value on the model
// since ngModel only watches for object identity change
+ // FIXME: When a user selects an option, this watch will fire needlessly
if (ngOptions.trackBy) {
scope.$watch(
function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
@@ -38984,22 +31067,19 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
} else {
- ngModelCtrl.$isEmpty = function(value) {
- return !value || value.length === 0;
- };
+ selectCtrl.writeValue = function writeNgOptionsMultiple(values) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+ // Only set `<option>.selected` if necessary, in order to prevent some browsers from
+ // scrolling to `<option>` elements that are outside the `<select>` element's viewport.
+ var selectedOptions = values && values.map(getAndUpdateSelectedOption) || [];
- selectCtrl.writeValue = function writeNgOptionsMultiple(value) {
options.items.forEach(function(option) {
- option.element.selected = false;
+ if (option.element.selected && !includes(selectedOptions, option)) {
+ option.element.selected = false;
+ }
});
-
- if (value) {
- value.forEach(function(item) {
- var option = options.getOptionFromViewValue(item);
- if (option) option.element.selected = true;
- });
- }
};
@@ -39032,28 +31112,47 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
}
}
-
if (providedEmptyOption) {
- // we need to remove it before calling selectElement.empty() because otherwise IE will
- // remove the label from the element. wtf?
- emptyOption.remove();
-
// compile the element since there might be bindings in it
- $compile(emptyOption)(scope);
+ $compile(selectCtrl.emptyOption)(scope);
- // remove the class, which is added automatically because we recompile the element and it
- // becomes the compilation root
- emptyOption.removeClass('ng-scope');
- } else {
- emptyOption = jqLite(optionTemplate.cloneNode(false));
- }
+ selectElement.prepend(selectCtrl.emptyOption);
- selectElement.empty();
+ if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
+ // This means the empty option has currently no actual DOM node, probably because
+ // it has been modified by a transclusion directive.
+ selectCtrl.hasEmptyOption = false;
+
+ // Redefine the registerOption function, which will catch
+ // options that are added by ngIf etc. (rendering of the node is async because of
+ // lazy transclusion)
+ selectCtrl.registerOption = function(optionScope, optionEl) {
+ if (optionEl.val() === '') {
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = optionEl;
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ // This ensures the new empty option is selected if previously no option was selected
+ ngModelCtrl.$render();
+
+ optionEl.on('$destroy', function() {
+ var needsRerender = selectCtrl.$isEmptyOptionSelected();
+
+ selectCtrl.hasEmptyOption = false;
+ selectCtrl.emptyOption = undefined;
+
+ if (needsRerender) ngModelCtrl.$render();
+ });
+ }
+ };
- // We need to do this here to ensure that the options object is defined
- // when we first hit it in writeNgOptionsValue
- updateOptions();
+ } else {
+ // remove the class, which is added automatically because we recompile the element and it
+ // becomes the compilation root
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ }
+
+ }
// We will re-render the option elements if the option values or labels change
scope.$watchCollection(ngOptions.getWatchables, updateOptions);
@@ -39066,11 +31165,20 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
updateOptionElement(option, optionElement);
}
+ function getAndUpdateSelectedOption(viewValue) {
+ var option = options.getOptionFromViewValue(viewValue);
+ var element = option && option.element;
+
+ if (element && !element.selected) element.selected = true;
+
+ return option;
+ }
function updateOptionElement(option, element) {
option.element = element;
element.disabled = option.disabled;
- // NOTE: The label must be set before the value, otherwise IE10/11/EDGE create unresponsive
+ // Support: IE 11 only, Edge 12-13 only
+ // NOTE: The label must be set before the value, otherwise IE 11 & Edge create unresponsive
// selects in certain circumstances when multiple selects are next to each other and display
// the option list in listbox style, i.e. the select is [multiple], or specifies a [size].
// See https://github.com/angular/angular.js/issues/11314 for more info.
@@ -39079,7 +31187,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
element.label = option.label;
element.textContent = option.label;
}
- if (option.value !== element.value) element.value = option.selectValue;
+ element.value = option.selectValue;
}
function updateOptions() {
@@ -39106,11 +31214,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var groupElementMap = {};
- // Ensure that the empty option is always there if it was explicitly provided
- if (providedEmptyOption) {
- selectElement.prepend(emptyOption);
- }
-
options.items.forEach(function addOption(option) {
var groupElement;
@@ -39155,7 +31258,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
ngModelCtrl.$render();
}
}
-
}
}
@@ -39183,27 +31285,27 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* @description
* `ngPluralize` is a directive that displays messages according to en-US localization rules.
* These rules are bundled with angular.js, but can be overridden
- * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive
+ * (see {@link guide/i18n AngularJS i18n} dev guide). You configure ngPluralize directive
* by specifying the mappings between
* [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
* and the strings to be displayed.
*
- * # Plural categories and explicit number rules
+ * ## Plural categories and explicit number rules
* There are two
* [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
- * in Angular's default en-US locale: "one" and "other".
+ * in AngularJS's default en-US locale: "one" and "other".
*
* While a plural category may match many numbers (for example, in en-US locale, "other" can match
* any number that is not 1), an explicit number rule can only match one number. For example, the
* explicit number rule for "3" matches the number 3. There are examples of plural categories
* and explicit number rules throughout the rest of this documentation.
*
- * # Configuring ngPluralize
+ * ## Configuring ngPluralize
* You configure ngPluralize by providing 2 attributes: `count` and `when`.
* You can also provide an optional attribute, `offset`.
*
* The value of the `count` attribute can be either a string or an {@link guide/expression
- * Angular expression}; these are evaluated on the current scope for its bound value.
+ * AngularJS expression}; these are evaluated on the current scope for its bound value.
*
* The `when` attribute specifies the mappings between plural categories and the actual
* string to be displayed. The value of the attribute should be a JSON object.
@@ -39225,14 +31327,14 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* show "a dozen people are viewing".
*
* You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted
- * into pluralized strings. In the previous example, Angular will replace `{}` with
+ * into pluralized strings. In the previous example, AngularJS will replace `{}` with
* <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder
* for <span ng-non-bindable>{{numberExpression}}</span>.
*
* If no rule is defined for a category, then an empty string is displayed and a warning is generated.
* Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`.
*
- * # Configuring ngPluralize with offset
+ * ## Configuring ngPluralize with offset
* The `offset` attribute allows further customization of pluralized text, which can result in
* a better user experience. For example, instead of the message "4 people are viewing this document",
* you might display "John, Kate and 2 others are viewing this document".
@@ -39253,7 +31355,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* three explicit number rules 0, 1 and 2.
* When one person, perhaps John, views the document, "John is viewing" will be shown.
* When three people view the document, no explicit number rule is found, so
- * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category.
+ * an offset of 2 is taken off 3, and AngularJS uses 1 to decide the plural category.
* In this case, plural category 'one' is matched and "John, Mary and one other person are viewing"
* is shown.
*
@@ -39267,7 +31369,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* @param {number=} offset Offset to deduct from the total number.
*
* @example
- <example module="pluralizeExample">
+ <example module="pluralizeExample" name="ng-pluralize">
<file name="index.html">
<script>
angular.module('pluralizeExample', [])
@@ -39381,7 +31483,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) {
var count = parseFloat(newVal);
- var countIsNaN = isNaN(count);
+ var countIsNaN = isNumberNaN(count);
if (!countIsNaN && !(count in whens)) {
// If an explicit number rule such as 1, 2, 3... is defined, just use it.
@@ -39391,12 +31493,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
// If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
// In JS `NaN !== NaN`, so we have to explicitly check.
- if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
+ if ((count !== lastCount) && !(countIsNaN && isNumberNaN(lastCount))) {
watchRemover();
var whenExpFn = whensExpFns[count];
if (isUndefined(whenExpFn)) {
if (newVal != null) {
- $log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp);
+ $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp);
}
watchRemover = noop;
updateElementText();
@@ -39414,10 +31516,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
};
}];
+/* exported ngRepeatDirective */
+
/**
* @ngdoc directive
* @name ngRepeat
* @multiElement
+ * @restrict A
*
* @description
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
@@ -39441,7 +31546,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* </div>
*
*
- * # Iterating over object properties
+ * ## Iterating over object properties
*
* It is possible to get `ngRepeat` to iterate over the properties of an object using the following
* syntax:
@@ -39450,17 +31555,17 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* <div ng-repeat="(key, value) in myObj"> ... </div>
* ```
*
- * However, there are a limitations compared to array iteration:
+ * However, there are a few limitations compared to array iteration:
*
* - The JavaScript specification does not define the order of keys
- * returned for an object, so Angular relies on the order returned by the browser
+ * returned for an object, so AngularJS relies on the order returned by the browser
* when running `for key in myObj`. Browsers generally follow the strategy of providing
* keys in the order in which they were defined, although there are exceptions when keys are deleted
* and reinstated. See the
* [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
*
* - `ngRepeat` will silently *ignore* object keys starting with `$`, because
- * it's a prefix used by Angular for public (`$`) and private (`$$`) properties.
+ * it's a prefix used by AngularJS for public (`$`) and private (`$$`) properties.
*
* - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
* objects, and will throw an error if used with one.
@@ -39471,10 +31576,10 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* or implement a `$watch` on the object yourself.
*
*
- * # Tracking and Duplicates
+ * ## Tracking and Duplicates
*
* `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
- * the collection. When a change happens, ngRepeat then makes the corresponding changes to the DOM:
+ * the collection. When a change happens, `ngRepeat` then makes the corresponding changes to the DOM:
*
* * When an item is added, a new instance of the template is added to the DOM.
* * When an item is removed, its template instance is removed from the DOM.
@@ -39482,64 +31587,153 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
*
* To minimize creation of DOM elements, `ngRepeat` uses a function
* to "keep track" of all items in the collection and their corresponding DOM elements.
- * For example, if an item is added to the collection, ngRepeat will know that all other items
+ * For example, if an item is added to the collection, `ngRepeat` will know that all other items
* already have DOM elements, and will not re-render them.
*
- * The default tracking function (which tracks items by their identity) does not allow
- * duplicate items in arrays. This is because when there are duplicates, it is not possible
- * to maintain a one-to-one mapping between collection items and DOM elements.
+ * All different types of tracking functions, their syntax, and their support for duplicate
+ * items in collections can be found in the
+ * {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
*
- * If you do need to repeat duplicate items, you can substitute the default tracking behavior
- * with your own using the `track by` expression.
+ * <div class="alert alert-success">
+ * **Best Practice:** If you are working with objects that have a unique identifier property, you
+ * should track by this identifier instead of the object instance,
+ * e.g. `item in items track by item.id`.
+ * Should you reload your data later, `ngRepeat` will not have to rebuild the DOM elements for items
+ * it has already rendered, even if the JavaScript objects in the collection have been substituted
+ * for new ones. For large collections, this significantly improves rendering performance.
+ * </div>
*
- * For example, you may track items by the index of each item in the collection, using the
- * special scope property `$index`:
- * ```html
- * <div ng-repeat="n in [42, 42, 43, 43] track by $index">
- * {{n}}
- * </div>
- * ```
+ * ### Effects of DOM Element re-use
*
- * You may also use arbitrary expressions in `track by`, including references to custom functions
- * on the scope:
- * ```html
- * <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
- * {{n}}
- * </div>
- * ```
+ * When DOM elements are re-used, ngRepeat updates the scope for the element, which will
+ * automatically update any active bindings on the template. However, other
+ * functionality will not be updated, because the element is not re-created:
*
- * <div class="alert alert-success">
- * If you are working with objects that have an identifier property, you should track
- * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
- * will not have to rebuild the DOM elements for items it has already rendered, even if the
- * JavaScript objects in the collection have been substituted for new ones. For large collections,
- * this significantly improves rendering performance. If you don't have a unique identifier,
- * `track by $index` can also provide a performance boost.
- * </div>
- * ```html
- * <div ng-repeat="model in collection track by model.id">
- * {{model.name}}
- * </div>
- * ```
+ * - Directives are not re-compiled
+ * - {@link guide/expression#one-time-binding one-time expressions} on the repeated template are not
+ * updated if they have stabilized.
*
- * When no `track by` expression is provided, it is equivalent to tracking by the built-in
- * `$id` function, which tracks items by their identity:
- * ```html
- * <div ng-repeat="obj in collection track by $id(obj)">
- * {{obj.prop}}
- * </div>
- * ```
+ * The above affects all kinds of element re-use due to tracking, but may be especially visible
+ * when tracking by `$index` due to the way ngRepeat re-uses elements.
*
- * <div class="alert alert-warning">
- * **Note:** `track by` must always be the last expression:
- * </div>
- * ```
- * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id">
- * {{model.name}}
- * </div>
- * ```
+ * The following example shows the effects of different actions with tracking:
+
+ <example module="ngRepeat" name="ngRepeat-tracking" deps="angular-animate.js" animations="true">
+ <file name="script.js">
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ var friends = [
+ {name:'John', age:25},
+ {name:'Mary', age:40},
+ {name:'Peter', age:85}
+ ];
+
+ $scope.removeFirst = function() {
+ $scope.friends.shift();
+ };
+
+ $scope.updateAge = function() {
+ $scope.friends.forEach(function(el) {
+ el.age = el.age + 5;
+ });
+ };
+
+ $scope.copy = function() {
+ $scope.friends = angular.copy($scope.friends);
+ };
+
+ $scope.reset = function() {
+ $scope.friends = angular.copy(friends);
+ };
+
+ $scope.reset();
+ });
+ </file>
+ <file name="index.html">
+ <div ng-controller="repeatController">
+ <ol>
+ <li>When you click "Update Age", only the first list updates the age, because all others have
+ a one-time binding on the age property. If you then click "Copy", the current friend list
+ is copied, and now the second list updates the age, because the identity of the collection items
+ has changed and the list must be re-rendered. The 3rd and 4th list stay the same, because all the
+ items are already known according to their tracking functions.
+ </li>
+ <li>When you click "Remove First", the 4th list has the wrong age on both remaining items. This is
+ due to tracking by $index: when the first collection item is removed, ngRepeat reuses the first
+ DOM element for the new first collection item, and so on. Since the age property is one-time
+ bound, the value remains from the collection item which was previously at this index.
+ </li>
+ </ol>
+
+ <button ng-click="removeFirst()">Remove First</button>
+ <button ng-click="updateAge()">Update Age</button>
+ <button ng-click="copy()">Copy</button>
+ <br><button ng-click="reset()">Reset List</button>
+ <br>
+ <code>track by $id(friend)</code> (default):
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends">
+ {{friend.name}} is {{friend.age}} years old.
+ </li>
+ </ul>
+ <code>track by $id(friend)</code> (default), with age one-time binding:
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends">
+ {{friend.name}} is {{::friend.age}} years old.
+ </li>
+ </ul>
+ <code>track by friend.name</code>, with age one-time binding:
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends track by friend.name">
+ {{friend.name}} is {{::friend.age}} years old.
+ </li>
+ </ul>
+ <code>track by $index</code>, with age one-time binding:
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends track by $index">
+ {{friend.name}} is {{::friend.age}} years old.
+ </li>
+ </ul>
+ </div>
+ </file>
+ <file name="animations.css">
+ .example-animate-container {
+ background:white;
+ border:1px solid black;
+ list-style:none;
+ margin:0;
+ padding:0 10px;
+ }
+
+ .animate-repeat {
+ line-height:30px;
+ list-style:none;
+ box-sizing:border-box;
+ }
+
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter,
+ .animate-repeat.ng-leave {
+ transition:all linear 0.5s;
+ }
+
+ .animate-repeat.ng-leave.ng-leave-active,
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter {
+ opacity:0;
+ max-height:0;
+ }
+
+ .animate-repeat.ng-leave,
+ .animate-repeat.ng-move.ng-move-active,
+ .animate-repeat.ng-enter.ng-enter-active {
+ opacity:1;
+ max-height:30px;
+ }
+ </file>
+ </example>
+
*
- * # Special repeat start and end points
+ * ## Special repeat start and end points
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
* the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
* The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on)
@@ -39614,22 +31808,38 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
* mapped to the same DOM element, which is not possible.)
*
- * Note that the tracking expression must come last, after any filters, and the alias expression.
- *
- * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements
- * will be associated by item identity in the array.
- *
- * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
- * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
- * with the corresponding item in the array by identity. Moving the same object in array would move the DOM
- * element in the same way in the DOM.
- *
- * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
- * case the object identity does not matter. Two objects are considered equivalent as long as their `id`
- * property is same.
+ * *Default tracking: $id()*: `item in items` is equivalent to `item in items track by $id(item)`.
+ * This implies that the DOM elements will be associated by item identity in the collection.
+ *
+ * The built-in `$id()` function can be used to assign a unique
+ * `$$hashKey` property to each item in the collection. This property is then used as a key to associated DOM elements
+ * with the corresponding item in the collection by identity. Moving the same object would move
+ * the DOM element in the same way in the DOM.
+ * Note that the default id function does not support duplicate primitive values (`number`, `string`),
+ * but supports duplictae non-primitive values (`object`) that are *equal* in shape.
+ *
+ * *Custom Expression*: It is possible to use any AngularJS expression to compute the tracking
+ * id, for example with a function, or using a property on the collection items.
+ * `item in items track by item.id` is a typical pattern when the items have a unique identifier,
+ * e.g. database id. In this case the object identity does not matter. Two objects are considered
+ * equivalent as long as their `id` property is same.
+ * Tracking by unique identifier is the most performant way and should be used whenever possible.
+ *
+ * *$index*: This special property tracks the collection items by their index, and
+ * re-uses the DOM elements that match that index, e.g. `item in items track by $index`. This can
+ * be used for a performance improvement if no unique identfier is available and the identity of
+ * the collection items cannot be easily computed. It also allows duplicates.
+ *
+ * <div class="alert alert-warning">
+ * <strong>Note:</strong> Re-using DOM elements can have unforeseen effects. Read the
+ * {@link ngRepeat#tracking-and-duplicates section on tracking and duplicates} for
+ * more info.
+ * </div>
*
- * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
- * to items in conjunction with a tracking expression.
+ * <div class="alert alert-warning">
+ * <strong>Note:</strong> the `track by` expression must come last - after any filters, and the alias expression:
+ * `item in items | filter:searchText as results track by item.id`
+ * </div>
*
* * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
* intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
@@ -39638,24 +31848,24 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
* the items have been processed through the filter.
*
- * Please note that `as [variable name] is not an operator but rather a part of ngRepeat micro-syntax so it can be used only at the end
- * (and not as operator, inside an expression).
+ * Please note that `as [variable name]` is not an operator but rather a part of ngRepeat
+ * micro-syntax so it can be used only after all filters (and not as operator, inside an expression).
*
- * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` .
+ * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results track by item.id` .
*
* @example
* This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
- * results by name. New (entering) and removed (leaving) items are animated.
+ * results by name or by age. New (entering) and removed (leaving) items are animated.
<example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true">
<file name="index.html">
<div ng-controller="repeatController">
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
<ul class="example-animate-container">
- <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
+ <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results track by friend.name">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
</li>
- <li class="animate-repeat" ng-if="results.length == 0">
+ <li class="animate-repeat" ng-if="results.length === 0">
<strong>No results found...</strong>
</li>
</ul>
@@ -39748,9 +31958,8 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
scope.$first = (index === 0);
scope.$last = (index === (arrayLength - 1));
scope.$middle = !(scope.$first || scope.$last);
- // jshint bitwise: false
- scope.$odd = !(scope.$even = (index&1) === 0);
- // jshint bitwise: true
+ // eslint-disable-next-line no-bitwise
+ scope.$odd = !(scope.$even = (index & 1) === 0);
};
var getBlockStart = function(block) {
@@ -39761,6 +31970,13 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
return block.clone[block.clone.length - 1];
};
+ var trackByIdArrayFn = function($scope, key, value) {
+ return hashKey(value);
+ };
+
+ var trackByIdObjFn = function($scope, key) {
+ return key;
+ };
return {
restrict: 'A',
@@ -39776,7 +31992,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if (!match) {
- throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
+ throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.',
expression);
}
@@ -39785,10 +32001,10 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
var aliasAs = match[3];
var trackByExp = match[4];
- match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);
+ match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);
if (!match) {
- throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
+ throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.',
lhs);
}
var valueIdentifier = match[3] || match[1];
@@ -39796,40 +32012,31 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
- throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.",
+ throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.',
aliasAs);
}
- var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
- var hashFnLocals = {$id: hashKey};
+ var trackByIdExpFn;
if (trackByExp) {
- trackByExpGetter = $parse(trackByExp);
- } else {
- trackByIdArrayFn = function(key, value) {
- return hashKey(value);
- };
- trackByIdObjFn = function(key) {
- return key;
+ var hashFnLocals = {$id: hashKey};
+ var trackByExpGetter = $parse(trackByExp);
+
+ trackByIdExpFn = function($scope, key, value, index) {
+ // assign key, value, and $index to the locals so that they can be used in hash functions
+ if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
+ hashFnLocals[valueIdentifier] = value;
+ hashFnLocals.$index = index;
+ return trackByExpGetter($scope, hashFnLocals);
};
}
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
- if (trackByExpGetter) {
- trackByIdExpFn = function(key, value, index) {
- // assign key, value, and $index to the locals so that they can be used in hash functions
- if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
- hashFnLocals[valueIdentifier] = value;
- hashFnLocals.$index = index;
- return trackByExpGetter($scope, hashFnLocals);
- };
- }
-
// Store a list of elements from previous run. This is a hash where key is the item from the
// iterator, and the value is objects with following properties.
// - scope: bound scope
- // - element: previous element.
+ // - clone: previous element.
// - index: position
//
// We are using no-proto object so that we don't need to guard against inherited props via
@@ -39879,7 +32086,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
for (index = 0; index < collectionLength; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
- trackById = trackByIdFn(key, value, index);
+ trackById = trackByIdFn($scope, key, value, index);
if (lastBlockMap[trackById]) {
// found previously seen block
block = lastBlockMap[trackById];
@@ -39892,7 +32099,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
if (block && block.scope) lastBlockMap[block.id] = block;
});
throw ngRepeatMinErr('dupes',
- "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
+ 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}',
expression, trackById, value);
} else {
// new never before seen block
@@ -39901,6 +32108,12 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
}
}
+ // Clear the value property from the hashFnLocals object to prevent a reference to the last value
+ // being leaked into the ngRepeatCompile function scope
+ if (hashFnLocals) {
+ hashFnLocals[valueIdentifier] = undefined;
+ }
+
// remove leftover items
for (var blockKey in lastBlockMap) {
block = lastBlockMap[blockKey];
@@ -39933,7 +32146,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
nextNode = nextNode.nextSibling;
} while (nextNode && nextNode[NG_REMOVED]);
- if (getBlockStart(block) != nextNode) {
+ if (getBlockStart(block) !== nextNode) {
// existing item which got moved
$animate.move(getBlockNodes(block.clone), null, previousNode);
}
@@ -39973,11 +32186,13 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* @multiElement
*
* @description
- * The `ngShow` directive shows or hides the given HTML element based on the expression
- * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding
- * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngShow` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngShow` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
* <!-- when $scope.myValue is truthy (element is visible) -->
@@ -39987,31 +32202,32 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* <div ng-show="myValue" class="ng-hide"></div>
* ```
*
- * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class
- * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When truthy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
- * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
- * class CSS. Note that the selector that needs to be used is actually `.ng-hide:not(.ng-hide-animate)` to cope
- * with extra animation classes that can be added.
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
* ```css
* .ng-hide:not(.ng-hide-animate) {
- * /&#42; this is just another form of hiding an element &#42;/
+ * /&#42; These are just alternative ways of hiding an element &#42;/
* display: block!important;
* position: absolute;
* top: -9999px;
@@ -40019,29 +32235,24 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* }
* ```
*
- * By default you don't need to override in CSS anything and the animations will work around the display style.
+ * By default you don't need to override anything in CSS and the animations will work around the
+ * display style.
*
- * ## A note about animations with `ngShow`
+ * @animations
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngShow` expression evaluates to a truthy value and just before contents are set to visible. |
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass except that
- * you must also include the !important flag to override the display property
- * so that you can perform an animation when the element is hidden during the time of the animation.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /&#42; A working example can be found at the bottom of this page. &#42;/
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * /&#42; this is required as of 1.3x to properly
- * apply all styling in a show/hide animation &#42;/
- * transition: 0s linear all;
- * }
- *
- * .my-element.ng-hide-add-active,
- * .my-element.ng-hide-remove-active {
- * /&#42; the transition is defined in the active class &#42;/
- * transition: 1s linear all;
+ * transition: all 0.5s linear;
* }
*
* .my-element.ng-hide-add { ... }
@@ -40050,79 +32261,124 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
- * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden |
- * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngShow` expression evaluates to a truthy value and just before contents are set to visible |
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngShow If the {@link guide/expression expression} is truthy
- * then the element is shown or hidden respectively.
+ * @param {expression} ngShow If the {@link guide/expression expression} is truthy/falsy then the
+ * element is shown/hidden respectively.
*
* @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * A simple example, animating the element's opacity:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-simple">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/>
- <div>
- Show:
- <div class="check-element animate-show" ng-show="checked">
- <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked.
- </div>
- </div>
- <div>
- Hide:
- <div class="check-element animate-show" ng-hide="checked">
- <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked.
- </div>
+ Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br />
+ <div class="check-element animate-show-hide" ng-show="checked">
+ I show up when your checkbox is checked.
</div>
</file>
- <file name="glyphicons.css">
- @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
- </file>
<file name="animations.css">
- .animate-show {
- line-height: 20px;
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
+ transition: all linear 0.5s;
+ }
+
+ .check-element {
+ border: 1px solid black;
opacity: 1;
padding: 10px;
- border: 1px solid black;
- background: white;
}
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- .animate-show.ng-hide-add, .animate-show.ng-hide-remove {
- transition: all linear 0.5s;
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
+ });
+ </file>
+ </example>
+ *
+ * <hr />
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-complex">
+ <file name="index.html">
+ Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br />
+ <div class="check-element funky-show-hide" ng-show="checked">
+ I show up when your checkbox is checked.
+ </div>
+ </file>
+ <file name="animations.css">
+ body {
+ overflow: hidden;
+ perspective: 1000px;
}
- .animate-show.ng-hide {
- line-height: 0;
- opacity: 0;
- padding: 0 10px;
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding: 10px;
border: 1px solid black;
- background: white;
+ opacity: 1;
+ padding: 10px;
}
</file>
<file name="protractor.js" type="protractor">
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
-
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- element(by.model('checked')).click();
-
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
});
</file>
</example>
+ *
+ * @knownIssue
+ *
+ * ### Flickering when using ngShow to toggle between elements
+ *
+ * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can
+ * happen that both the element to show and the element to hide are visible for a very short time.
+ *
+ * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations
+ * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than
+ * other browsers.
+ *
+ * There are several way to mitigate this problem:
+ *
+ * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}.
+ * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}.
+ * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements.
+ * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}.
+ * - Define an animation on the affected elements.
*/
var ngShowDirective = ['$animate', function($animate) {
return {
@@ -40149,11 +32405,13 @@ var ngShowDirective = ['$animate', function($animate) {
* @multiElement
*
* @description
- * The `ngHide` directive shows or hides the given HTML element based on the expression
- * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding
- * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngHide` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngHide` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
* <!-- when $scope.myValue is truthy (element is hidden) -->
@@ -40163,30 +32421,32 @@ var ngShowDirective = ['$animate', function($animate) {
* <div ng-hide="myValue"></div>
* ```
*
- * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class
- * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When falsy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
- * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
- * class in CSS:
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
* ```css
- * .ng-hide {
- * /&#42; this is just another form of hiding an element &#42;/
+ * .ng-hide:not(.ng-hide-animate) {
+ * /&#42; These are just alternative ways of hiding an element &#42;/
* display: block!important;
* position: absolute;
* top: -9999px;
@@ -40194,20 +32454,24 @@ var ngShowDirective = ['$animate', function($animate) {
* }
* ```
*
- * By default you don't need to override in CSS anything and the animations will work around the display style.
+ * By default you don't need to override in CSS anything and the animations will work around the
+ * display style.
*
- * ## A note about animations with `ngHide`
+ * @animations
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible. |
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide`
- * CSS class is added and removed for you instead of your own CSS class.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /&#42; A working example can be found at the bottom of this page. &#42;/
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * transition: 0.5s linear all;
+ * transition: all 0.5s linear;
* }
*
* .my-element.ng-hide-add { ... }
@@ -40216,77 +32480,124 @@ var ngShowDirective = ['$animate', function($animate) {
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
- * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden |
- * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible |
- *
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngHide If the {@link guide/expression expression} is truthy then
- * the element is shown or hidden respectively.
+ * @param {expression} ngHide If the {@link guide/expression expression} is truthy/falsy then the
+ * element is hidden/shown respectively.
*
* @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * A simple example, animating the element's opacity:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-simple">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/>
- <div>
- Show:
- <div class="check-element animate-hide" ng-show="checked">
- <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked.
- </div>
+ Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br />
+ <div class="check-element animate-show-hide" ng-hide="checked">
+ I hide when your checkbox is checked.
</div>
- <div>
- Hide:
- <div class="check-element animate-hide" ng-hide="checked">
- <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked.
- </div>
- </div>
- </file>
- <file name="glyphicons.css">
- @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
</file>
<file name="animations.css">
- .animate-hide {
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
transition: all linear 0.5s;
- line-height: 20px;
+ }
+
+ .check-element {
+ border: 1px solid black;
opacity: 1;
padding: 10px;
- border: 1px solid black;
- background: white;
}
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- .animate-hide.ng-hide {
- line-height: 0;
- opacity: 0;
- padding: 0 10px;
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
+ });
+ </file>
+ </example>
+ *
+ * <hr />
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-complex">
+ <file name="index.html">
+ Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br />
+ <div class="check-element funky-show-hide" ng-hide="checked">
+ I hide when your checkbox is checked.
+ </div>
+ </file>
+ <file name="animations.css">
+ body {
+ overflow: hidden;
+ perspective: 1000px;
+ }
+
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding: 10px;
border: 1px solid black;
- background: white;
+ opacity: 1;
+ padding: 10px;
}
</file>
<file name="protractor.js" type="protractor">
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
-
- element(by.model('checked')).click();
-
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
});
</file>
</example>
+ *
+ * @knownIssue
+ *
+ * ### Flickering when using ngHide to toggle between elements
+ *
+ * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can
+ * happen that both the element to show and the element to hide are visible for a very short time.
+ *
+ * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations
+ * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than
+ * other browsers.
+ *
+ * There are several way to mitigate this problem:
+ *
+ * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}.
+ * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}.
+ * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements.
+ * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}.
+ * - Define an animation on the affected elements.
*/
var ngHideDirective = ['$animate', function($animate) {
return {
@@ -40304,6 +32615,7 @@ var ngHideDirective = ['$animate', function($animate) {
};
}];
+
/**
* @ngdoc directive
* @name ngStyle
@@ -40328,7 +32640,7 @@ var ngHideDirective = ['$animate', function($animate) {
* See the 'background-color' style in the example below.
*
* @example
- <example>
+ <example name="ng-style">
<file name="index.html">
<input type="button" value="set color" ng-click="myStyle={color:'red'}">
<input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}">
@@ -40346,22 +32658,22 @@ var ngHideDirective = ['$animate', function($animate) {
var colorSpan = element(by.css('span'));
it('should check ng-style', function() {
- expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)');
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(0, 0, 0, 1\)|rgb\(0, 0, 0\)/);
element(by.css('input[value=\'set color\']')).click();
- expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)');
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(255, 0, 0, 1\)|rgb\(255, 0, 0\)/);
element(by.css('input[value=clear]')).click();
- expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)');
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(0, 0, 0, 1\)|rgb\(0, 0, 0\)/);
});
</file>
</example>
*/
var ngStyleDirective = ngDirective(function(scope, element, attr) {
- scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
+ scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
- forEach(oldStyles, function(val, style) { element.css(style, '');});
+ forEach(oldStyles, function(val, style) { element.css(style, ''); });
}
if (newStyles) element.css(newStyles);
- }, true);
+ });
});
/**
@@ -40414,14 +32726,18 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
*
* * `ngSwitchWhen`: the case statement to match against. If match then this
* case will be displayed. If the same match appears multiple times, all the
- * elements will be displayed.
+ * elements will be displayed. It is possible to associate multiple values to
+ * the same `ngSwitchWhen` by defining the optional attribute
+ * `ngSwitchWhenSeparator`. The separator will be used to split the value of
+ * the `ngSwitchWhen` attribute into multiple tokens, and the element will show
+ * if any of the `ngSwitch` evaluates to any of these tokens.
* * `ngSwitchDefault`: the default case when no other case match. If there
* are multiple default cases, all of them will be displayed when no other
* case match.
*
*
* @example
- <example module="switchExample" deps="angular-animate.js" animations="true">
+ <example module="switchExample" deps="angular-animate.js" animations="true" name="ng-switch">
<file name="index.html">
<div ng-controller="ExampleController">
<select ng-model="selection" ng-options="item for item in items">
@@ -40430,7 +32746,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
<hr/>
<div class="animate-switch-container"
ng-switch on="selection">
- <div class="animate-switch" ng-switch-when="settings">Settings Div</div>
+ <div class="animate-switch" ng-switch-when="settings|options" ng-switch-when-separator="|">Settings Div</div>
<div class="animate-switch" ng-switch-when="home">Home Span</div>
<div class="animate-switch" ng-switch-default>default</div>
</div>
@@ -40439,7 +32755,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
<file name="script.js">
angular.module('switchExample', ['ngAnimate'])
.controller('ExampleController', ['$scope', function($scope) {
- $scope.items = ['settings', 'home', 'other'];
+ $scope.items = ['settings', 'home', 'options', 'other'];
$scope.selection = $scope.items[0];
}]);
</file>
@@ -40486,8 +32802,12 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
select.all(by.css('option')).get(1).click();
expect(switchElem.getText()).toMatch(/Home Span/);
});
- it('should select default', function() {
+ it('should change to settings via "options"', function() {
select.all(by.css('option')).get(2).click();
+ expect(switchElem.getText()).toMatch(/Settings Div/);
+ });
+ it('should select default', function() {
+ select.all(by.css('option')).get(3).click();
expect(switchElem.getText()).toMatch(/default/);
});
</file>
@@ -40498,7 +32818,7 @@ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
require: 'ngSwitch',
// asks for $scope to fool the BC controller module
- controller: ['$scope', function ngSwitchController() {
+ controller: ['$scope', function NgSwitchController() {
this.cases = {};
}],
link: function(scope, element, attr, ngSwitchController) {
@@ -40509,21 +32829,24 @@ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
selectedScopes = [];
var spliceFactory = function(array, index) {
- return function() { array.splice(index, 1); };
+ return function(response) {
+ if (response !== false) array.splice(index, 1);
+ };
};
scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
var i, ii;
- for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
- $animate.cancel(previousLeaveAnimations[i]);
+
+ // Start with the last, in case the array is modified during the loop
+ while (previousLeaveAnimations.length) {
+ $animate.cancel(previousLeaveAnimations.pop());
}
- previousLeaveAnimations.length = 0;
for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
var selected = getBlockNodes(selectedElements[i].clone);
selectedScopes[i].$destroy();
- var promise = previousLeaveAnimations[i] = $animate.leave(selected);
- promise.then(spliceFactory(previousLeaveAnimations, i));
+ var runner = previousLeaveAnimations[i] = $animate.leave(selected);
+ runner.done(spliceFactory(previousLeaveAnimations, i));
}
selectedElements.length = 0;
@@ -40553,8 +32876,16 @@ var ngSwitchWhenDirective = ngDirective({
require: '^ngSwitch',
multiElement: true,
link: function(scope, element, attrs, ctrl, $transclude) {
- ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []);
- ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element });
+
+ var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter(
+ // Filter duplicate cases
+ function(element, index, array) { return array[index - 1] !== element; }
+ );
+
+ forEach(cases, function(whenCase) {
+ ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []);
+ ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element });
+ });
}
});
@@ -40582,8 +32913,8 @@ var ngSwitchDefaultDirective = ngDirective({
*
* If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing
* content of this element will be removed before the transcluded content is inserted.
- * If the transcluded content is empty, the existing content is left intact. This lets you provide fallback content in the case
- * that no transcluded content is provided.
+ * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback
+ * content in the case that no transcluded content is provided.
*
* @element ANY
*
@@ -40616,7 +32947,7 @@ var ngSwitchDefaultDirective = ngDirective({
* <div ng-controller="ExampleController">
* <input ng-model="title" aria-label="title"> <br/>
* <textarea ng-model="text" aria-label="text"></textarea> <br/>
- * <pane title="{{title}}">{{text}}</pane>
+ * <pane title="{{title}}"><span>{{text}}</span></pane>
* </div>
* </file>
* <file name="protractor.js" type="protractor">
@@ -40638,7 +32969,7 @@ var ngSwitchDefaultDirective = ngDirective({
* This example shows how to use `NgTransclude` with fallback content, that
* is displayed if no transcluded content is provided.
*
- * <example module="transcludeFallbackContentExample">
+ * <example module="transcludeFallbackContentExample" name="ng-transclude">
* <file name="index.html">
* <script>
* angular.module('transcludeFallbackContentExample', [])
@@ -40691,7 +33022,7 @@ var ngSwitchDefaultDirective = ngDirective({
* </file>
* <file name="app.js">
* angular.module('multiSlotTranscludeExample', [])
- * .directive('pane', function(){
+ * .directive('pane', function() {
* return {
* restrict: 'E',
* transclude: {
@@ -40708,7 +33039,7 @@ var ngSwitchDefaultDirective = ngDirective({
* })
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.title = 'Lorem Ipsum';
- * $scope.link = "https://google.com";
+ * $scope.link = 'https://google.com';
* $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
* }]);
* </file>
@@ -40731,7 +33062,6 @@ var ngTranscludeMinErr = minErr('ngTransclude');
var ngTranscludeDirective = ['$compile', function($compile) {
return {
restrict: 'EAC',
- terminal: true,
compile: function ngTranscludeCompile(tElement) {
// Remove and cache any original content to act as a fallback
@@ -40764,7 +33094,7 @@ var ngTranscludeDirective = ['$compile', function($compile) {
}
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
- if (clone.length) {
+ if (clone.length && notWhitespace(clone)) {
$element.append(clone);
} else {
useFallbackContent();
@@ -40781,6 +33111,15 @@ var ngTranscludeDirective = ['$compile', function($compile) {
$element.append(clone);
});
}
+
+ function notWhitespace(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ var node = nodes[i];
+ if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) {
+ return true;
+ }
+ }
+ }
};
}
};
@@ -40802,7 +33141,7 @@ var ngTranscludeDirective = ['$compile', function($compile) {
* @param {string} id Cache name of the template.
*
* @example
- <example>
+ <example name="script-tag">
<file name="index.html">
<script type="text/ng-template" id="/tpl.html">
Content of the template.
@@ -40824,7 +33163,7 @@ var scriptDirective = ['$templateCache', function($templateCache) {
restrict: 'E',
terminal: true,
compile: function(element, attr) {
- if (attr.type == 'text/ng-template') {
+ if (attr.type === 'text/ng-template') {
var templateUrl = attr.id,
text = element[0].text;
@@ -40834,80 +33173,263 @@ var scriptDirective = ['$templateCache', function($templateCache) {
};
}];
+/* exported selectDirective, optionDirective */
+
var noopNgModelController = { $setViewValue: noop, $render: noop };
-function chromeHack(optionElement) {
- // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
- // Adding an <option selected="selected"> element to a <select required="required"> should
- // automatically select the new element
- if (optionElement[0].hasAttribute('selected')) {
- optionElement[0].selected = true;
- }
+function setOptionSelectedStatus(optionEl, value) {
+ optionEl.prop('selected', value);
+ /**
+ * When unselecting an option, setting the property to null / false should be enough
+ * However, screenreaders might react to the selected attribute instead, see
+ * https://github.com/angular/angular.js/issues/14419
+ * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false
+ * or null
+ */
+ optionEl.attr('selected', value);
}
/**
* @ngdoc type
* @name select.SelectController
+ *
* @description
- * The controller for the `<select>` directive. This provides support for reading
- * and writing the selected value(s) of the control and also coordinates dynamically
- * added `<option>` elements, perhaps by an `ngRepeat` directive.
+ * The controller for the {@link ng.select select} directive. The controller exposes
+ * a few utility methods that can be used to augment the behavior of a regular or an
+ * {@link ng.ngOptions ngOptions} select element.
+ *
+ * @example
+ * ### Set a custom error when the unknown option is selected
+ *
+ * This example sets a custom error "unknownValue" on the ngModelController
+ * when the select element's unknown option is selected, i.e. when the model is set to a value
+ * that is not matched by any option.
+ *
+ * <example name="select-unknown-value-error" module="staticSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="testSelect"> Single select: </label><br>
+ * <select name="testSelect" ng-model="selected" unknown-value-error>
+ * <option value="option-1">Option 1</option>
+ * <option value="option-2">Option 2</option>
+ * </select><br>
+ * <span class="error" ng-if="myForm.testSelect.$error.unknownValue">
+ * Error: The current model doesn't match any option</span><br>
+ *
+ * <button ng-click="forceUnknownOption()">Force unknown option</button><br>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.selected = null;
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.selected = 'nonsense';
+ * };
+ * }])
+ * .directive('unknownValueError', function() {
+ * return {
+ * require: ['ngModel', 'select'],
+ * link: function(scope, element, attrs, ctrls) {
+ * var ngModelCtrl = ctrls[0];
+ * var selectCtrl = ctrls[1];
+ *
+ * ngModelCtrl.$validators.unknownValue = function(modelValue, viewValue) {
+ * if (selectCtrl.$isUnknownOptionSelected()) {
+ * return false;
+ * }
+ *
+ * return true;
+ * };
+ * }
+ *
+ * };
+ * });
+ * </file>
+ *</example>
+ *
+ *
+ * @example
+ * ### Set the "required" error when the unknown option is selected.
+ *
+ * By default, the "required" error on the ngModelController is only set on a required select
+ * when the empty option is selected. This example adds a custom directive that also sets the
+ * error when the unknown option is selected.
+ *
+ * <example name="select-unknown-value-required" module="staticSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="testSelect"> Select: </label><br>
+ * <select name="testSelect" ng-model="selected" required unknown-value-required>
+ * <option value="option-1">Option 1</option>
+ * <option value="option-2">Option 2</option>
+ * </select><br>
+ * <span class="error" ng-if="myForm.testSelect.$error.required">Error: Please select a value</span><br>
+ *
+ * <button ng-click="forceUnknownOption()">Force unknown option</button><br>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.selected = null;
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.selected = 'nonsense';
+ * };
+ * }])
+ * .directive('unknownValueRequired', function() {
+ * return {
+ * priority: 1, // This directive must run after the required directive has added its validator
+ * require: ['ngModel', 'select'],
+ * link: function(scope, element, attrs, ctrls) {
+ * var ngModelCtrl = ctrls[0];
+ * var selectCtrl = ctrls[1];
+ *
+ * var originalRequiredValidator = ngModelCtrl.$validators.required;
+ *
+ * ngModelCtrl.$validators.required = function() {
+ * if (attrs.required && selectCtrl.$isUnknownOptionSelected()) {
+ * return false;
+ * }
+ *
+ * return originalRequiredValidator.apply(this, arguments);
+ * };
+ * }
+ * };
+ * });
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should show the error message when the unknown option is selected', function() {
+
+ var error = element(by.className('error'));
+
+ expect(error.getText()).toBe('Error: Please select a value');
+
+ element(by.cssContainingText('option', 'Option 1')).click();
+
+ expect(error.isPresent()).toBe(false);
+
+ element(by.tagName('button')).click();
+
+ expect(error.getText()).toBe('Error: Please select a value');
+ });
+ * </file>
+ *</example>
+ *
+ *
*/
var SelectController =
- ['$element', '$scope', function($element, $scope) {
+ ['$element', '$scope', /** @this */ function($element, $scope) {
var self = this,
- optionsMap = new HashMap();
+ optionsMap = new NgMap();
+
+ self.selectValueMap = {}; // Keys are the hashed values, values the original values
// If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
self.ngModelCtrl = noopNgModelController;
+ self.multiple = false;
// The "unknown" option is one that is prepended to the list if the viewValue
// does not match any of the options. When it is rendered the value of the unknown
// option is '? XXX ?' where XXX is the hashKey of the value that is not known.
//
+ // Support: IE 9 only
// We can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
self.unknownOption = jqLite(window.document.createElement('option'));
+
+ // The empty option is an option with the value '' that the application developer can
+ // provide inside the select. It is always selectable and indicates that a "null" selection has
+ // been made by the user.
+ // If the select has an empty option, and the model of the select is set to "undefined" or "null",
+ // the empty option is selected.
+ // If the model is set to a different unmatched value, the unknown option is rendered and
+ // selected, i.e both are present, because a "null" selection and an unknown value are different.
+ self.hasEmptyOption = false;
+ self.emptyOption = undefined;
+
self.renderUnknownOption = function(val) {
- var unknownVal = '? ' + hashKey(val) + ' ?';
+ var unknownVal = self.generateUnknownOptionValue(val);
self.unknownOption.val(unknownVal);
$element.prepend(self.unknownOption);
+ setOptionSelectedStatus(self.unknownOption, true);
$element.val(unknownVal);
};
- $scope.$on('$destroy', function() {
- // disable unknown option so that we don't do work when the whole select is being destroyed
- self.renderUnknownOption = noop;
- });
+ self.updateUnknownOption = function(val) {
+ var unknownVal = self.generateUnknownOptionValue(val);
+ self.unknownOption.val(unknownVal);
+ setOptionSelectedStatus(self.unknownOption, true);
+ $element.val(unknownVal);
+ };
+
+ self.generateUnknownOptionValue = function(val) {
+ return '? ' + hashKey(val) + ' ?';
+ };
self.removeUnknownOption = function() {
if (self.unknownOption.parent()) self.unknownOption.remove();
};
+ self.selectEmptyOption = function() {
+ if (self.emptyOption) {
+ $element.val('');
+ setOptionSelectedStatus(self.emptyOption, true);
+ }
+ };
+
+ self.unselectEmptyOption = function() {
+ if (self.hasEmptyOption) {
+ setOptionSelectedStatus(self.emptyOption, false);
+ }
+ };
+
+ $scope.$on('$destroy', function() {
+ // disable unknown option so that we don't do work when the whole select is being destroyed
+ self.renderUnknownOption = noop;
+ });
// Read the value of the select control, the implementation of this changes depending
// upon whether the select can have multiple values and whether ngOptions is at work.
self.readValue = function readSingleValue() {
- self.removeUnknownOption();
- return $element.val();
+ var val = $element.val();
+ // ngValue added option values are stored in the selectValueMap, normal interpolations are not
+ var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val;
+
+ if (self.hasOption(realVal)) {
+ return realVal;
+ }
+
+ return null;
};
// Write the value to the select control, the implementation of this changes depending
// upon whether the select can have multiple values and whether ngOptions is at work.
self.writeValue = function writeSingleValue(value) {
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ var currentlySelectedOption = $element[0].options[$element[0].selectedIndex];
+ if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false);
+
if (self.hasOption(value)) {
self.removeUnknownOption();
- $element.val(value);
- if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
+
+ var hashedVal = hashKey(value);
+ $element.val(hashedVal in self.selectValueMap ? hashedVal : value);
+
+ // Set selected attribute and property on selected option for screen readers
+ var selectedOption = $element[0].options[$element[0].selectedIndex];
+ setOptionSelectedStatus(jqLite(selectedOption), true);
} else {
- if (value == null && self.emptyOption) {
- self.removeUnknownOption();
- $element.val('');
- } else {
- self.renderUnknownOption(value);
- }
+ self.selectUnknownOrEmptyOption(value);
}
};
@@ -40919,12 +33441,14 @@ var SelectController =
assertNotHasOwnProperty(value, '"option value"');
if (value === '') {
+ self.hasEmptyOption = true;
self.emptyOption = element;
}
var count = optionsMap.get(value) || 0;
- optionsMap.put(value, count + 1);
- self.ngModelCtrl.$render();
- chromeHack(element);
+ optionsMap.set(value, count + 1);
+ // Only render at the end of a digest. This improves render performance when many options
+ // are added during a digest and ensures all relevant options are correctly marked as selected
+ scheduleRender();
};
// Tell the select control that an option, with the given value, has been removed
@@ -40932,12 +33456,13 @@ var SelectController =
var count = optionsMap.get(value);
if (count) {
if (count === 1) {
- optionsMap.remove(value);
+ optionsMap.delete(value);
if (value === '') {
+ self.hasEmptyOption = false;
self.emptyOption = undefined;
}
} else {
- optionsMap.put(value, count - 1);
+ optionsMap.set(value, count - 1);
}
}
};
@@ -40947,36 +33472,185 @@ var SelectController =
return !!optionsMap.get(value);
};
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$hasEmptyOption
+ *
+ * @description
+ *
+ * Returns `true` if the select element currently has an empty option
+ * element, i.e. an option that signifies that the select is empty / the selection is null.
+ *
+ */
+ self.$hasEmptyOption = function() {
+ return self.hasEmptyOption;
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isUnknownOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element's unknown option is selected. The unknown option is added
+ * and automatically selected whenever the select model doesn't match any option.
+ *
+ */
+ self.$isUnknownOptionSelected = function() {
+ // Presence of the unknown option means it is selected
+ return $element[0].options[0] === self.unknownOption[0];
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isEmptyOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element has an empty option and this empty option is currently
+ * selected. Returns `false` if the select element has no empty option or it is not selected.
+ *
+ */
+ self.$isEmptyOptionSelected = function() {
+ return self.hasEmptyOption && $element[0].options[$element[0].selectedIndex] === self.emptyOption[0];
+ };
+
+ self.selectUnknownOrEmptyOption = function(value) {
+ if (value == null && self.emptyOption) {
+ self.removeUnknownOption();
+ self.selectEmptyOption();
+ } else if (self.unknownOption.parent().length) {
+ self.updateUnknownOption(value);
+ } else {
+ self.renderUnknownOption(value);
+ }
+ };
+
+ var renderScheduled = false;
+ function scheduleRender() {
+ if (renderScheduled) return;
+ renderScheduled = true;
+ $scope.$$postDigest(function() {
+ renderScheduled = false;
+ self.ngModelCtrl.$render();
+ });
+ }
+
+ var updateScheduled = false;
+ function scheduleViewValueUpdate(renderAfter) {
+ if (updateScheduled) return;
+
+ updateScheduled = true;
+
+ $scope.$$postDigest(function() {
+ if ($scope.$$destroyed) return;
+
+ updateScheduled = false;
+ self.ngModelCtrl.$setViewValue(self.readValue());
+ if (renderAfter) self.ngModelCtrl.$render();
+ });
+ }
+
self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
- if (interpolateValueFn) {
+ if (optionAttrs.$attr.ngValue) {
+ // The value attribute is set by ngValue
+ var oldVal, hashedVal;
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
+ if (isDefined(hashedVal)) {
+ self.removeOption(oldVal);
+ delete self.selectValueMap[hashedVal];
+ removal = true;
+ }
+
+ hashedVal = hashKey(newVal);
+ oldVal = newVal;
+ self.selectValueMap[hashedVal] = newVal;
+ self.addOption(newVal, optionElement);
+ // Set the attribute directly instead of using optionAttrs.$set - this stops the observer
+ // from firing a second time. Other $observers on value will also get the result of the
+ // ngValue expression, not the hashed value
+ optionElement.attr('value', hashedVal);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
+
+ });
+ } else if (interpolateValueFn) {
// The value attribute is interpolated
- var oldVal;
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+ // This method is overwritten in ngOptions and has side-effects!
+ self.readValue();
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
if (isDefined(oldVal)) {
self.removeOption(oldVal);
+ removal = true;
}
oldVal = newVal;
self.addOption(newVal, optionElement);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
});
} else if (interpolateTextFn) {
// The text content is interpolated
optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
optionAttrs.$set('value', newVal);
+ var previouslySelected = optionElement.prop('selected');
if (oldVal !== newVal) {
self.removeOption(oldVal);
}
self.addOption(newVal, optionElement);
+
+ if (oldVal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
});
} else {
// The value attribute is static
self.addOption(optionAttrs.value, optionElement);
}
+
+ optionAttrs.$observe('disabled', function(newVal) {
+
+ // Since model updates will also select disabled options (like ngOptions),
+ // we only have to handle options becoming disabled, not enabled
+
+ if (newVal === 'true' || newVal && optionElement.prop('selected')) {
+ if (self.multiple) {
+ scheduleViewValueUpdate(true);
+ } else {
+ self.ngModelCtrl.$setViewValue(null);
+ self.ngModelCtrl.$render();
+ }
+ }
+ });
+
optionElement.on('$destroy', function() {
- self.removeOption(optionAttrs.value);
- self.ngModelCtrl.$render();
+ var currentValue = self.readValue();
+ var removeValue = optionAttrs.value;
+
+ self.removeOption(removeValue);
+ scheduleRender();
+
+ if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 ||
+ currentValue === removeValue
+ ) {
+ // When multiple (selected) options are destroyed at the same time, we don't want
+ // to run a model update for each of them. Instead, run a single update in the $$postDigest
+ scheduleViewValueUpdate(true);
+ }
});
};
}];
@@ -40987,7 +33661,7 @@ var SelectController =
* @restrict E
*
* @description
- * HTML `SELECT` element with angular data-binding.
+ * HTML `select` element with AngularJS data-binding.
*
* The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
* between the scope and the `<select>` control (including setting default values).
@@ -40997,14 +33671,27 @@ var SelectController =
* When an item in the `<select>` menu is selected, the value of the selected option will be bound
* to the model identified by the `ngModel` directive. With static or repeated options, this is
* the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing.
- * If you want dynamic value attributes, you can use interpolation inside the value attribute.
+ * Value and textContent can be interpolated.
*
- * <div class="alert alert-warning">
- * Note that the value of a `select` directive used without `ngOptions` is always a string.
- * When the model needs to be bound to a non-string value, you must either explicitly convert it
- * using a directive (see example below) or use `ngOptions` to specify the set of options.
- * This is because an option element can only be bound to string values at present.
- * </div>
+ * The {@link select.SelectController select controller} exposes utility functions that can be used
+ * to manipulate the select's behavior.
+ *
+ * ## Matching model and option values
+ *
+ * In general, the match between the model and an option is evaluated by strictly comparing the model
+ * value against the value of the available options.
+ *
+ * If you are setting the option value with the option's `value` attribute, or textContent, the
+ * value will always be a `string` which means that the model value must also be a string.
+ * Otherwise the `select` directive cannot match them correctly.
+ *
+ * To bind the model to a non-string value, you can use one of the following strategies:
+ * - the {@link ng.ngOptions `ngOptions`} directive
+ * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value})
+ * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be
+ * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example})
+ * - model $parsers / $formatters to convert the string value
+ * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example})
*
* If the viewValue of `ngModel` does not match any of the options, then the control
* will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
@@ -41013,16 +33700,20 @@ var SelectController =
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
* option. See example below for demonstration.
*
- * <div class="alert alert-info">
+ * ## Choosing between `ngRepeat` and `ngOptions`
+ *
* In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
- * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
- * comprehension expression, and additionally in reducing memory and increasing speed by not creating
- * a new scope for each repeated instance.
- * </div>
+ * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
*
+ * Specifically, select with repeated options slows down significantly starting at 2000 options in
+ * Chrome and Internet Explorer / Edge.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} multiple Allows multiple options to be selected. The selected values will be
* bound to the model as an array.
@@ -41030,10 +33721,13 @@ var SelectController =
* @param {string=} ngRequired Adds required attribute and required validation constraint to
* the element when the ngRequired expression evaluates to true. Use ngRequired instead of required
* when you want to data-bind to the required attribute.
- * @param {string=} ngChange Angular expression to be executed when selected option(s) changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when selected option(s) changes due to user
* interaction with the select element.
* @param {string=} ngOptions sets the options that the select is populated with and defines what is
* set on the model on selection. See {@link ngOptions `ngOptions`}.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
+ *
*
* @example
* ### Simple `select` elements with static options
@@ -41074,7 +33768,7 @@ var SelectController =
* $scope.data = {
* singleSelect: null,
* multipleSelect: [],
- * option1: 'option-1',
+ * option1: 'option-1'
* };
*
* $scope.forceUnknownOption = function() {
@@ -41084,36 +33778,70 @@ var SelectController =
* </file>
*</example>
*
+ * @example
* ### Using `ngRepeat` to generate `select` options
- * <example name="ngrepeat-select" module="ngrepeatSelect">
+ * <example name="select-ngrepeat" module="ngrepeatSelect">
* <file name="index.html">
* <div ng-controller="ExampleController">
* <form name="myForm">
* <label for="repeatSelect"> Repeat select: </label>
- * <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect">
+ * <select name="repeatSelect" id="repeatSelect" ng-model="data.model">
* <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option>
* </select>
* </form>
* <hr>
- * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/>
+ * <tt>model = {{data.model}}</tt><br/>
* </div>
* </file>
* <file name="app.js">
* angular.module('ngrepeatSelect', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.data = {
- * repeatSelect: null,
+ * model: null,
* availableOptions: [
* {id: '1', name: 'Option A'},
* {id: '2', name: 'Option B'},
* {id: '3', name: 'Option C'}
- * ],
+ * ]
* };
* }]);
* </file>
*</example>
*
+ * @example
+ * ### Using `ngValue` to bind the model to an array of objects
+ * <example name="select-ngvalue" module="ngvalueSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="ngvalueselect"> ngvalue select: </label>
+ * <select size="6" name="ngvalueselect" ng-model="data.model" multiple>
+ * <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option>
+ * </select>
+ * </form>
+ * <hr>
+ * <pre>model = {{data.model | json}}</pre><br/>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('ngvalueSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * model: null,
+ * availableOptions: [
+ {value: 'myString', name: 'string'},
+ {value: 1, name: 'integer'},
+ {value: true, name: 'boolean'},
+ {value: null, name: 'null'},
+ {value: {prop: 'value'}, name: 'object'},
+ {value: ['a'], name: 'array'}
+ * ]
+ * };
+ * }]);
+ * </file>
+ *</example>
*
+ * @example
* ### Using `select` with `ngOptions` and setting a default value
* See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
*
@@ -41145,7 +33873,7 @@ var SelectController =
* </file>
*</example>
*
- *
+ * @example
* ### Binding `select` to a non-string value via `ngModel` parsing / formatting
*
* <example name="select-with-non-string-options" module="nonStringSelect">
@@ -41178,7 +33906,6 @@ var SelectController =
* </file>
* <file name="protractor.js" type="protractor">
* it('should initialize to model', function() {
- * var select = element(by.css('select'));
* expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two');
* });
* </file>
@@ -41200,11 +33927,16 @@ var selectDirective = function() {
function selectPreLink(scope, element, attr, ctrls) {
- // if ngModel is not defined, we don't need to do anything
+ var selectCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
- if (!ngModelCtrl) return;
- var selectCtrl = ctrls[0];
+ // if ngModel is not defined, we don't need to do anything but set the registerOption
+ // function to noop, so options don't get added internally
+ if (!ngModelCtrl) {
+ selectCtrl.registerOption = noop;
+ return;
+ }
+
selectCtrl.ngModelCtrl = ngModelCtrl;
@@ -41212,6 +33944,7 @@ var selectDirective = function() {
// to the `readValue` method, which can be changed if the select can have multiple
// selected values or if the options are being generated by `ngOptions`
element.on('change', function() {
+ selectCtrl.removeUnknownOption();
scope.$apply(function() {
ngModelCtrl.$setViewValue(selectCtrl.readValue());
});
@@ -41222,13 +33955,15 @@ var selectDirective = function() {
// we have to add an extra watch since ngModel doesn't work well with arrays - it
// doesn't trigger rendering if only an item in the array changes.
if (attr.multiple) {
+ selectCtrl.multiple = true;
// Read value now needs to check each option to see if it is selected
selectCtrl.readValue = function readMultipleValue() {
var array = [];
forEach(element.find('option'), function(option) {
- if (option.selected) {
- array.push(option.value);
+ if (option.selected && !option.disabled) {
+ var val = option.value;
+ array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val);
}
});
return array;
@@ -41236,9 +33971,22 @@ var selectDirective = function() {
// Write value now needs to set the selected property of each matching option
selectCtrl.writeValue = function writeMultipleValue(value) {
- var items = new HashMap(value);
forEach(element.find('option'), function(option) {
- option.selected = isDefined(items.get(option.value));
+ var shouldBeSelected = !!value && (includes(value, option.value) ||
+ includes(value, selectCtrl.selectValueMap[option.value]));
+ var currentlySelected = option.selected;
+
+ // Support: IE 9-11 only, Edge 12-15+
+ // In IE and Edge adding options to the selection via shift+click/UP/DOWN
+ // will de-select already selected options if "selected" on those options was set
+ // more than once (i.e. when the options were already selected)
+ // So we only modify the selected property if necessary.
+ // Note: this behavior cannot be replicated via unit tests because it only shows in the
+ // actual user interface.
+ if (shouldBeSelected !== currentlySelected) {
+ setOptionSelectedStatus(jqLite(option), shouldBeSelected);
+ }
+
});
};
@@ -41289,13 +34037,17 @@ var optionDirective = ['$interpolate', function($interpolate) {
restrict: 'E',
priority: 100,
compile: function(element, attr) {
- if (isDefined(attr.value)) {
+ var interpolateValueFn, interpolateTextFn;
+
+ if (isDefined(attr.ngValue)) {
+ // Will be handled by registerOption
+ } else if (isDefined(attr.value)) {
// If the value attribute is defined, check if it contains an interpolation
- var interpolateValueFn = $interpolate(attr.value, true);
+ interpolateValueFn = $interpolate(attr.value, true);
} else {
// If the value attribute is not defined then we fall back to the
// text content of the option element, which may be interpolated
- var interpolateTextFn = $interpolate(element.text(), true);
+ interpolateTextFn = $interpolate(element.text(), true);
if (!interpolateTextFn) {
attr.$set('value', element.text());
}
@@ -41317,23 +34069,22 @@ var optionDirective = ['$interpolate', function($interpolate) {
};
}];
-var styleDirective = valueFn({
- restrict: 'E',
- terminal: false
-});
-
/**
* @ngdoc directive
* @name ngRequired
* @restrict A
*
+ * @param {expression} ngRequired AngularJS expression. If it evaluates to `true`, it sets the
+ * `required` attribute to the element and adds the `required`
+ * {@link ngModel.NgModelController#$validators `validator`}.
+ *
* @description
*
* ngRequired adds the required {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
* It is most often used for {@link input `input`} and {@link select `select`} controls, but can also be
* applied to custom controls.
*
- * The directive sets the `required` attribute on the element if the Angular expression inside
+ * The directive sets the `required` attribute on the element if the AngularJS expression inside
* `ngRequired` evaluates to true. A special directive for setting `required` is necessary because we
* cannot use interpolation inside `required`. See the {@link guide/interpolation interpolation guide}
* for more info.
@@ -41381,28 +34132,44 @@ var styleDirective = valueFn({
* </file>
* </example>
*/
-var requiredDirective = function() {
+var requiredDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
- attr.required = true; // force truthy in case we are on non input element
+ // For boolean attributes like required, presence means true
+ var value = attr.hasOwnProperty('required') || $parse(attr.ngRequired)(scope);
+
+ if (!attr.ngRequired) {
+ // force truthy in case we are on non input element
+ // (input elements do this automatically for boolean attributes like required)
+ attr.required = true;
+ }
ctrl.$validators.required = function(modelValue, viewValue) {
- return !attr.required || !ctrl.$isEmpty(viewValue);
+ return !value || !ctrl.$isEmpty(viewValue);
};
- attr.$observe('required', function() {
- ctrl.$validate();
+ attr.$observe('required', function(newVal) {
+
+ if (value !== newVal) {
+ value = newVal;
+ ctrl.$validate();
+ }
});
}
};
-};
+}];
/**
* @ngdoc directive
* @name ngPattern
+ * @restrict A
+ *
+ * @param {expression|RegExp} ngPattern AngularJS expression that must evaluate to a `RegExp` or a `String`
+ * parsable into a `RegExp`, or a `RegExp` literal. See above for
+ * more details.
*
* @description
*
@@ -41410,11 +34177,12 @@ var requiredDirective = function() {
* It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
*
* The validator sets the `pattern` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
- * does not match a RegExp which is obtained by evaluating the Angular expression given in the
- * `ngPattern` attribute value:
- * * If the expression evaluates to a RegExp object, then this is used directly.
- * * If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it
- * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * does not match a RegExp which is obtained from the `ngPattern` attribute value:
+ * - the value is an AngularJS expression:
+ * - If the expression evaluates to a RegExp object, then this is used directly.
+ * - If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it
+ * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * - If the value is a RegExp literal, e.g. `ngPattern="/^\d+$/"`, it is used directly.
*
* <div class="alert alert-info">
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
@@ -41475,40 +34243,68 @@ var requiredDirective = function() {
* </file>
* </example>
*/
-var patternDirective = function() {
+var patternDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
- link: function(scope, elm, attr, ctrl) {
- if (!ctrl) return;
-
- var regexp, patternExp = attr.ngPattern || attr.pattern;
- attr.$observe('pattern', function(regex) {
- if (isString(regex) && regex.length > 0) {
- regex = new RegExp('^' + regex + '$');
+ compile: function(tElm, tAttr) {
+ var patternExp;
+ var parseFn;
+
+ if (tAttr.ngPattern) {
+ patternExp = tAttr.ngPattern;
+
+ // ngPattern might be a scope expression, or an inlined regex, which is not parsable.
+ // We get value of the attribute here, so we can compare the old and the new value
+ // in the observer to avoid unnecessary validations
+ if (tAttr.ngPattern.charAt(0) === '/' && REGEX_STRING_REGEXP.test(tAttr.ngPattern)) {
+ parseFn = function() { return tAttr.ngPattern; };
+ } else {
+ parseFn = $parse(tAttr.ngPattern);
}
+ }
+
+ return function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
- if (regex && !regex.test) {
- throw minErr('ngPattern')('noregexp',
- 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
- regex, startingTag(elm));
+ var attrVal = attr.pattern;
+
+ if (attr.ngPattern) {
+ attrVal = parseFn(scope);
+ } else {
+ patternExp = attr.pattern;
}
- regexp = regex || undefined;
- ctrl.$validate();
- });
+ var regexp = parsePatternAttr(attrVal, patternExp, elm);
+
+ attr.$observe('pattern', function(newVal) {
+ var oldRegexp = regexp;
+
+ regexp = parsePatternAttr(newVal, patternExp, elm);
+
+ if ((oldRegexp && oldRegexp.toString()) !== (regexp && regexp.toString())) {
+ ctrl.$validate();
+ }
+ });
- ctrl.$validators.pattern = function(modelValue, viewValue) {
- // HTML5 pattern constraint validates the input value, so we validate the viewValue
- return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
+ ctrl.$validators.pattern = function(modelValue, viewValue) {
+ // HTML5 pattern constraint validates the input value, so we validate the viewValue
+ return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
+ };
};
}
+
};
-};
+}];
/**
* @ngdoc directive
* @name ngMaxlength
+ * @restrict A
+ *
+ * @param {expression} ngMaxlength AngularJS expression that must evaluate to a `Number` or `String`
+ * parsable into a `Number`. Used as value for the `maxlength`
+ * {@link ngModel.NgModelController#$validators validator}.
*
* @description
*
@@ -41516,7 +34312,7 @@ var patternDirective = function() {
* It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
*
* The validator sets the `maxlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
- * is longer than the integer obtained by evaluating the Angular expression given in the
+ * is longer than the integer obtained by evaluating the AngularJS expression given in the
* `ngMaxlength` attribute value.
*
* <div class="alert alert-info">
@@ -41572,29 +34368,38 @@ var patternDirective = function() {
* </file>
* </example>
*/
-var maxlengthDirective = function() {
+var maxlengthDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
- var maxlength = -1;
+ var maxlength = attr.maxlength || $parse(attr.ngMaxlength)(scope);
+ var maxlengthParsed = parseLength(maxlength);
+
attr.$observe('maxlength', function(value) {
- var intVal = toInt(value);
- maxlength = isNaN(intVal) ? -1 : intVal;
- ctrl.$validate();
+ if (maxlength !== value) {
+ maxlengthParsed = parseLength(value);
+ maxlength = value;
+ ctrl.$validate();
+ }
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
- return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
+ return (maxlengthParsed < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlengthParsed);
};
}
};
-};
+}];
/**
* @ngdoc directive
* @name ngMinlength
+ * @restrict A
+ *
+ * @param {expression} ngMinlength AngularJS expression that must evaluate to a `Number` or `String`
+ * parsable into a `Number`. Used as value for the `minlength`
+ * {@link ngModel.NgModelController#$validators validator}.
*
* @description
*
@@ -41602,7 +34407,7 @@ var maxlengthDirective = function() {
* It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
*
* The validator sets the `minlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
- * is shorter than the integer obtained by evaluating the Angular expression given in the
+ * is shorter than the integer obtained by evaluating the AngularJS expression given in the
* `ngMinlength` attribute value.
*
* <div class="alert alert-info">
@@ -41656,35 +34461,62 @@ var maxlengthDirective = function() {
* </file>
* </example>
*/
-var minlengthDirective = function() {
+var minlengthDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
- var minlength = 0;
+ var minlength = attr.minlength || $parse(attr.ngMinlength)(scope);
+ var minlengthParsed = parseLength(minlength) || -1;
+
attr.$observe('minlength', function(value) {
- minlength = toInt(value) || 0;
- ctrl.$validate();
+ if (minlength !== value) {
+ minlengthParsed = parseLength(value) || -1;
+ minlength = value;
+ ctrl.$validate();
+ }
+
});
ctrl.$validators.minlength = function(modelValue, viewValue) {
- return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
+ return ctrl.$isEmpty(viewValue) || viewValue.length >= minlengthParsed;
};
}
};
-};
+}];
+
+function parsePatternAttr(regex, patternExp, elm) {
+ if (!regex) return undefined;
+
+ if (isString(regex)) {
+ regex = new RegExp('^' + regex + '$');
+ }
+
+ if (!regex.test) {
+ throw minErr('ngPattern')('noregexp',
+ 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
+ regex, startingTag(elm));
+ }
+
+ return regex;
+}
+
+function parseLength(val) {
+ var intVal = toInt(val);
+ return isNumberNaN(intVal) ? -1 : intVal;
+}
if (window.angular.bootstrap) {
- //AngularJS is already loaded, so we can return here...
+ // AngularJS is already loaded, so we can return here...
if (window.console) {
- console.log('WARNING: Tried to load angular more than once.');
+ console.log('WARNING: Tried to load AngularJS more than once.');
}
return;
}
-//try to bind to jquery now so that one can write jqLite(document).ready()
-//but we will rebind on bootstrap again.
+// try to bind to jquery now so that one can write jqLite(fn)
+// but we will rebind on bootstrap again.
bindJQuery();
publishExternalAPI(angular);
@@ -41732,7 +34564,7 @@ $provide.value("$locale", {
"BC",
"AD"
],
- "FIRSTDAYOFWEEK": 6,
+ "FIRSTDAYOFWEEK": 0,
"MONTH": [
"January",
"February",
@@ -41788,13 +34620,13 @@ $provide.value("$locale", {
5,
6
],
- "fullDate": "EEEE, MMMM d, y",
- "longDate": "MMMM d, y",
- "medium": "MMM d, y h:mm:ss a",
- "mediumDate": "MMM d, y",
+ "fullDate": "EEEE, d MMMM y",
+ "longDate": "d MMMM y",
+ "medium": "d MMM y h:mm:ss a",
+ "mediumDate": "d MMM y",
"mediumTime": "h:mm:ss a",
- "short": "M/d/yy h:mm a",
- "shortDate": "M/d/yy",
+ "short": "dd/MM/y h:mm a",
+ "shortDate": "dd/MM/y",
"shortTime": "h:mm a"
},
"NUMBER_FORMATS": {
@@ -41826,2309 +34658,16 @@ $provide.value("$locale", {
}
]
},
- "id": "en-us",
- "localeID": "en_US",
+ "id": "en-na",
+ "localeID": "en_NA",
"pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
-/**
- * Setup file for the Scenario.
- * Must be first in the compilation/bootstrap list.
- */
-
-// Public namespace
-angular.scenario = angular.scenario || {};
-
-/**
- * Expose jQuery (e.g. for custom dsl extensions).
- */
-angular.scenario.jQuery = _jQuery;
-
-/**
- * Defines a new output format.
- *
- * @param {string} name the name of the new output format
- * @param {function()} fn function(context, runner) that generates the output
- */
-angular.scenario.output = angular.scenario.output || function(name, fn) {
- angular.scenario.output[name] = fn;
-};
-
-/**
- * Defines a new DSL statement. If your factory function returns a Future
- * it's returned, otherwise the result is assumed to be a map of functions
- * for chaining. Chained functions are subject to the same rules.
- *
- * Note: All functions on the chain are bound to the chain scope so values
- * set on "this" in your statement function are available in the chained
- * functions.
- *
- * @param {string} name The name of the statement
- * @param {function()} fn Factory function(), return a function for
- * the statement.
- */
-angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
- angular.scenario.dsl[name] = function() {
- /* jshint -W040 *//* The dsl binds `this` for us when calling chained functions */
- function executeStatement(statement, args) {
- var result = statement.apply(this, args);
- if (angular.isFunction(result) || result instanceof angular.scenario.Future) {
- return result;
- }
- var self = this;
- var chain = angular.extend({}, result);
- angular.forEach(chain, function(value, name) {
- if (angular.isFunction(value)) {
- chain[name] = function() {
- return executeStatement.call(self, value, arguments);
- };
- } else {
- chain[name] = value;
- }
- });
- return chain;
- }
- var statement = fn.apply(this, arguments);
- return function() {
- return executeStatement.call(this, statement, arguments);
- };
- };
-};
-
-/**
- * Defines a new matcher for use with the expects() statement. The value
- * this.actual (like in Jasmine) is available in your matcher to compare
- * against. Your function should return a boolean. The future is automatically
- * created for you.
- *
- * @param {string} name The name of the matcher
- * @param {function()} fn The matching function(expected).
- */
-angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
- angular.scenario.matcher[name] = function(expected) {
- var description = this.future.name +
- (this.inverse ? ' not ' : ' ') + name +
- ' ' + angular.toJson(expected);
- var self = this;
- this.addFuture('expect ' + description,
- function(done) {
- var error;
- self.actual = self.future.value;
- if ((self.inverse && fn.call(self, expected)) ||
- (!self.inverse && !fn.call(self, expected))) {
- error = 'expected ' + description +
- ' but was ' + angular.toJson(self.actual);
- }
- done(error);
- });
- };
-};
-
-/**
- * Initialize the scenario runner and run !
- *
- * Access global window and document object
- * Access $runner through closure
- *
- * @param {Object=} config Config options
- */
-angular.scenario.setUpAndRun = function(config) {
- var href = window.location.href;
- var body = _jQuery(window.document.body);
- var output = [];
- var objModel = new angular.scenario.ObjectModel($runner);
-
- if (config && config.scenario_output) {
- output = config.scenario_output.split(',');
- }
-
- angular.forEach(angular.scenario.output, function(fn, name) {
- if (!output.length || output.indexOf(name) != -1) {
- var context = body.append('<div></div>').find('div:last');
- context.attr('id', name);
- fn.call({}, context, $runner, objModel);
- }
- });
-
- if (!/^http/.test(href) && !/^https/.test(href)) {
- body.append('<p id="system-error"></p>');
- body.find('#system-error').text(
- 'Scenario runner must be run using http or https. The protocol ' +
- href.split(':')[0] + ':// is not supported.'
- );
- return;
- }
-
- var appFrame = body.append('<div id="application"></div>').find('#application');
- var application = new angular.scenario.Application(appFrame);
-
- $runner.on('RunnerEnd', function() {
- appFrame.css('display', 'none');
- appFrame.find('iframe').attr('src', 'about:blank');
- });
-
- $runner.on('RunnerError', function(error) {
- if (window.console) {
- console.log(formatException(error));
- } else {
- // Do something for IE
- alert(error);
- }
- });
-
- $runner.run(application);
-};
-
-/**
- * Iterates through list with iterator function that must call the
- * continueFunction to continue iterating.
- *
- * @param {Array} list list to iterate over
- * @param {function()} iterator Callback function(value, continueFunction)
- * @param {function()} done Callback function(error, result) called when
- * iteration finishes or an error occurs.
- */
-function asyncForEach(list, iterator, done) {
- var i = 0;
- function loop(error, index) {
- if (index && index > i) {
- i = index;
- }
- if (error || i >= list.length) {
- done(error);
- } else {
- try {
- iterator(list[i++], loop);
- } catch (e) {
- done(e);
- }
- }
- }
- loop();
-}
-
-/**
- * Formats an exception into a string with the stack trace, but limits
- * to a specific line length.
- *
- * @param {Object} error The exception to format, can be anything throwable
- * @param {Number=} [maxStackLines=5] max lines of the stack trace to include
- * default is 5.
- */
-function formatException(error, maxStackLines) {
- maxStackLines = maxStackLines || 5;
- var message = error.toString();
- if (error.stack) {
- var stack = error.stack.split('\n');
- if (stack[0].indexOf(message) === -1) {
- maxStackLines++;
- stack.unshift(error.message);
- }
- message = stack.slice(0, maxStackLines).join('\n');
- }
- return message;
-}
-
-/**
- * Returns a function that gets the file name and line number from a
- * location in the stack if available based on the call site.
- *
- * Note: this returns another function because accessing .stack is very
- * expensive in Chrome.
- *
- * @param {Number} offset Number of stack lines to skip
- */
-function callerFile(offset) {
- var error = new Error();
-
- return function() {
- var line = (error.stack || '').split('\n')[offset];
-
- // Clean up the stack trace line
- if (line) {
- if (line.indexOf('@') !== -1) {
- // Firefox
- line = line.substring(line.indexOf('@') + 1);
- } else {
- // Chrome
- line = line.substring(line.indexOf('(') + 1).replace(')', '');
- }
- }
-
- return line || '';
- };
-}
-
-
-/**
- * Don't use the jQuery trigger method since it works incorrectly.
- *
- * jQuery notifies listeners and then changes the state of a checkbox and
- * does not create a real browser event. A real click changes the state of
- * the checkbox and then notifies listeners.
- *
- * To work around this we instead use our own handler that fires a real event.
- */
-(function(fn) {
- // We need a handle to the original trigger function for input tests.
- var parentTrigger = fn._originalTrigger = fn.trigger;
- fn.trigger = function(type) {
- if (/(click|change|keydown|blur|input|mousedown|mouseup)/.test(type)) {
- var processDefaults = [];
- this.each(function(index, node) {
- processDefaults.push(browserTrigger(node, type));
- });
-
- // this is not compatible with jQuery - we return an array of returned values,
- // so that scenario runner know whether JS code has preventDefault() of the event or not...
- return processDefaults;
- }
- return parentTrigger.apply(this, arguments);
- };
-})(_jQuery.fn);
-
-/**
- * Finds all bindings with the substring match of name and returns an
- * array of their values.
- *
- * @param {string} bindExp The name to match
- * @return {Array.<string>} String of binding values
- */
-_jQuery.fn.bindings = function(windowJquery, bindExp) {
- var result = [], match,
- bindSelector = '.ng-binding:visible';
- if (angular.isString(bindExp)) {
- bindExp = bindExp.replace(/\s/g, '');
- match = function(actualExp) {
- if (actualExp) {
- actualExp = actualExp.replace(/\s/g, '');
- if (actualExp == bindExp) return true;
- if (actualExp.indexOf(bindExp) === 0) {
- return actualExp.charAt(bindExp.length) == '|';
- }
- }
- };
- } else if (bindExp) {
- match = function(actualExp) {
- return actualExp && bindExp.exec(actualExp);
- };
- } else {
- match = function(actualExp) {
- return !!actualExp;
- };
- }
- var selection = this.find(bindSelector);
- if (this.is(bindSelector)) {
- selection = selection.add(this);
- }
-
- function push(value) {
- if (angular.isUndefined(value)) {
- value = '';
- } else if (typeof value !== 'string') {
- value = angular.toJson(value);
- }
- result.push('' + value);
- }
-
- selection.each(function() {
- var element = windowJquery(this),
- bindings;
- if (bindings = element.data('$binding')) {
- for (var expressions = [], binding, j=0, jj=bindings.length; j < jj; j++) {
- binding = bindings[j];
-
- if (binding.expressions) {
- expressions = binding.expressions;
- } else {
- expressions = [binding];
- }
- for (var scope, expression, i = 0, ii = expressions.length; i < ii; i++) {
- expression = expressions[i];
- if (match(expression)) {
- scope = scope || element.scope();
- push(scope.$eval(expression));
- }
- }
- }
- }
- });
- return result;
-};
-
-(function() {
- /**
- * Triggers a browser event. Attempts to choose the right event if one is
- * not specified.
- *
- * @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement
- * @param {string} eventType Optional event type
- * @param {Object=} eventData An optional object which contains additional event data (such as x,y
- * coordinates, keys, etc...) that are passed into the event when triggered
- */
- window.browserTrigger = function browserTrigger(element, eventType, eventData) {
- if (element && !element.nodeName) element = element[0];
- if (!element) return;
-
- eventData = eventData || {};
- var relatedTarget = eventData.relatedTarget || element;
- var keys = eventData.keys;
- var x = eventData.x;
- var y = eventData.y;
-
- var inputType = (element.type) ? element.type.toLowerCase() : null,
- nodeName = element.nodeName.toLowerCase();
- if (!eventType) {
- eventType = {
- 'text': 'change',
- 'textarea': 'change',
- 'hidden': 'change',
- 'password': 'change',
- 'button': 'click',
- 'submit': 'click',
- 'reset': 'click',
- 'image': 'click',
- 'checkbox': 'click',
- 'radio': 'click',
- 'select-one': 'change',
- 'select-multiple': 'change',
- '_default_': 'click'
- }[inputType || '_default_'];
- }
-
- if (nodeName === 'option') {
- element.parentNode.value = element.value;
- element = element.parentNode;
- eventType = 'change';
- }
-
- keys = keys || [];
- function pressed(key) {
- return keys.indexOf(key) !== -1;
- }
-
- var evnt;
- if (/transitionend/.test(eventType)) {
- if (window.WebKitTransitionEvent) {
- evnt = new WebKitTransitionEvent(eventType, eventData);
- evnt.initEvent(eventType, false, true);
- } else {
- try {
- evnt = new TransitionEvent(eventType, eventData);
- }
- catch (e) {
- evnt = window.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 {
- try {
- evnt = new AnimationEvent(eventType, eventData);
- }
- catch (e) {
- evnt = window.document.createEvent('AnimationEvent');
- evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0);
- }
- }
- } else if (/touch/.test(eventType) && supportsTouchEvents()) {
- evnt = createTouchEvent(element, eventType, x, y);
- } else if (/key/.test(eventType)) {
- evnt = window.document.createEvent('Events');
- evnt.initEvent(eventType, eventData.bubbles, eventData.cancelable);
- evnt.view = window;
- evnt.ctrlKey = pressed('ctrl');
- evnt.altKey = pressed('alt');
- evnt.shiftKey = pressed('shift');
- evnt.metaKey = pressed('meta');
- evnt.keyCode = eventData.keyCode;
- evnt.charCode = eventData.charCode;
- evnt.which = eventData.which;
- } else {
- evnt = window.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, relatedTarget);
- }
-
- /* 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;
-
- 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);
- };
-
- if (!eventData.bubbles || supportsEventBubblingInDetachedTree() || isAttachedToDocument(element)) {
- element.dispatchEvent(evnt);
- } else {
- triggerForPath(element, evnt);
- }
-
- finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault);
-
- delete angular['ff-684208-preventDefault'];
-
- return finalProcessDefault;
- };
-
- function supportsTouchEvents() {
- if ('_cached' in supportsTouchEvents) {
- return supportsTouchEvents._cached;
- }
- if (!window.document.createTouch || !window.document.createTouchList) {
- supportsTouchEvents._cached = false;
- return false;
- }
- try {
- window.document.createEvent('TouchEvent');
- } catch (e) {
- supportsTouchEvents._cached = false;
- return false;
- }
- supportsTouchEvents._cached = true;
- return true;
- }
-
- function createTouchEvent(element, eventType, x, y) {
- var evnt = new window.Event(eventType);
- x = x || 0;
- y = y || 0;
-
- var touch = window.document.createTouch(window, element, Date.now(), x, y, x, y);
- var touches = window.document.createTouchList(touch);
-
- evnt.touches = touches;
-
- return evnt;
- }
-
- function supportsEventBubblingInDetachedTree() {
- if ('_cached' in supportsEventBubblingInDetachedTree) {
- return supportsEventBubblingInDetachedTree._cached;
- }
- supportsEventBubblingInDetachedTree._cached = false;
- var doc = window.document;
- if (doc) {
- var parent = doc.createElement('div'),
- child = parent.cloneNode();
- parent.appendChild(child);
- parent.addEventListener('e', function() {
- supportsEventBubblingInDetachedTree._cached = true;
- });
- var evnt = window.document.createEvent('Events');
- evnt.initEvent('e', true, true);
- child.dispatchEvent(evnt);
- }
- return supportsEventBubblingInDetachedTree._cached;
- }
-
- function triggerForPath(element, evnt) {
- var stop = false;
-
- var _stopPropagation = evnt.stopPropagation;
- evnt.stopPropagation = function() {
- stop = true;
- _stopPropagation.apply(evnt, arguments);
- };
- patchEventTargetForBubbling(evnt, element);
- do {
- element.dispatchEvent(evnt);
- } while (!stop && (element = element.parentNode));
- }
-
- function patchEventTargetForBubbling(event, target) {
- event._target = target;
- Object.defineProperty(event, "target", {get: function() { return this._target;}});
- }
-
- function isAttachedToDocument(element) {
- while (element = element.parentNode) {
- if (element === window) {
- return true;
- }
- }
- return false;
- }
-}());
-
-/**
- * Represents the application currently being tested and abstracts usage
- * of iframes or separate windows.
- *
- * @param {Object} context jQuery wrapper around HTML context.
- */
-angular.scenario.Application = function(context) {
- this.context = context;
- context.append(
- '<h2>Current URL: <a href="about:blank">None</a></h2>' +
- '<div id="test-frames"></div>'
- );
-};
-
-/**
- * Gets the jQuery collection of frames. Don't use this directly because
- * frames may go stale.
- *
- * @private
- * @return {Object} jQuery collection
- */
-angular.scenario.Application.prototype.getFrame_ = function() {
- return this.context.find('#test-frames iframe:last');
-};
-
-/**
- * Gets the window of the test runner frame. Always favor executeAction()
- * instead of this method since it prevents you from getting a stale window.
- *
- * @private
- * @return {Object} the window of the frame
- */
-angular.scenario.Application.prototype.getWindow_ = function() {
- var contentWindow = this.getFrame_().prop('contentWindow');
- if (!contentWindow) {
- throw 'Frame window is not accessible.';
- }
- return contentWindow;
-};
-
-/**
- * Changes the location of the frame.
- *
- * @param {string} url The URL. If it begins with a # then only the
- * hash of the page is changed.
- * @param {function()} loadFn function($window, $document) Called when frame loads.
- * @param {function()} errorFn function(error) Called if any error when loading.
- */
-angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorFn) {
- var self = this;
- var frame = self.getFrame_();
- //TODO(esprehn): Refactor to use rethrow()
- errorFn = errorFn || function(e) { throw e; };
- if (url === 'about:blank') {
- errorFn('Sandbox Error: Navigating to about:blank is not allowed.');
- } else if (url.charAt(0) === '#') {
- url = frame.attr('src').split('#')[0] + url;
- frame.attr('src', url);
- self.executeAction(loadFn);
- } else {
- frame.remove();
- self.context.find('#test-frames').append('<iframe>');
- frame = self.getFrame_();
-
- frame.on('load', function() {
- frame.off();
- try {
- var $window = self.getWindow_();
-
- if (!$window.angular) {
- self.executeAction(loadFn);
- return;
- }
-
- if (!$window.angular.resumeBootstrap) {
- $window.angular.resumeDeferredBootstrap = resumeDeferredBootstrap;
- } else {
- resumeDeferredBootstrap();
- }
-
- } catch (e) {
- errorFn(e);
- }
-
- function resumeDeferredBootstrap() {
- // Disable animations
- var $injector = $window.angular.resumeBootstrap([['$provide', function($provide) {
- return ['$animate', function($animate) {
- $animate.enabled(false);
- }];
- }]]);
- self.rootElement = $injector.get('$rootElement')[0];
- self.executeAction(loadFn);
- }
- }).attr('src', url);
-
- // for IE compatibility set the name *after* setting the frame url
- frame[0].contentWindow.name = "NG_DEFER_BOOTSTRAP!";
- }
- self.context.find('> h2 a').attr('href', url).text(url);
-};
-
-/**
- * Executes a function in the context of the tested application. Will wait
- * for all pending angular xhr requests before executing.
- *
- * @param {function()} action The callback to execute. function($window, $document)
- * $document is a jQuery wrapped document.
- */
-angular.scenario.Application.prototype.executeAction = function(action) {
- var self = this;
- var $window = this.getWindow_();
- if (!$window.document) {
- throw 'Sandbox Error: Application document not accessible.';
- }
- if (!$window.angular) {
- return action.call(this, $window, _jQuery($window.document));
- }
-
- if (!!this.rootElement) {
- executeWithElement(this.rootElement);
- } else {
- angularInit($window.document, angular.bind(this, executeWithElement));
- }
-
- function executeWithElement(element) {
- var $injector = $window.angular.element(element).injector();
- var $element = _jQuery(element);
-
- $element.injector = function() {
- return $injector;
- };
-
- $injector.invoke(function($browser) {
- $browser.notifyWhenNoOutstandingRequests(function() {
- action.call(self, $window, $element);
- });
- });
- }
-};
-
-/**
- * The representation of define blocks. Don't used directly, instead use
- * define() in your tests.
- *
- * @param {string} descName Name of the block
- * @param {Object} parent describe or undefined if the root.
- */
-angular.scenario.Describe = function(descName, parent) {
- this.only = parent && parent.only;
- this.beforeEachFns = [];
- this.afterEachFns = [];
- this.its = [];
- this.children = [];
- this.name = descName;
- this.parent = parent;
- this.id = angular.scenario.Describe.id++;
-
- /**
- * Calls all before functions.
- */
- var beforeEachFns = this.beforeEachFns;
- this.setupBefore = function() {
- if (parent) parent.setupBefore.call(this);
- angular.forEach(beforeEachFns, function(fn) { fn.call(this); }, this);
- };
-
- /**
- * Calls all after functions.
- */
- var afterEachFns = this.afterEachFns;
- this.setupAfter = function() {
- angular.forEach(afterEachFns, function(fn) { fn.call(this); }, this);
- if (parent) parent.setupAfter.call(this);
- };
-};
-
-// Shared Unique ID generator for every describe block
-angular.scenario.Describe.id = 0;
-
-// Shared Unique ID generator for every it (spec)
-angular.scenario.Describe.specId = 0;
-
-/**
- * Defines a block to execute before each it or nested describe.
- *
- * @param {function()} body Body of the block.
- */
-angular.scenario.Describe.prototype.beforeEach = function(body) {
- this.beforeEachFns.push(body);
-};
-
-/**
- * Defines a block to execute after each it or nested describe.
- *
- * @param {function()} body Body of the block.
- */
-angular.scenario.Describe.prototype.afterEach = function(body) {
- this.afterEachFns.push(body);
-};
-
-/**
- * Creates a new describe block that's a child of this one.
- *
- * @param {string} name Name of the block. Appended to the parent block's name.
- * @param {function()} body Body of the block.
- */
-angular.scenario.Describe.prototype.describe = function(name, body) {
- var child = new angular.scenario.Describe(name, this);
- this.children.push(child);
- body.call(child);
-};
-
-/**
- * Same as describe() but makes ddescribe blocks the only to run.
- *
- * @param {string} name Name of the test.
- * @param {function()} body Body of the block.
- */
-angular.scenario.Describe.prototype.ddescribe = function(name, body) {
- var child = new angular.scenario.Describe(name, this);
- child.only = true;
- this.children.push(child);
- body.call(child);
-};
-
-/**
- * Use to disable a describe block.
- */
-angular.scenario.Describe.prototype.xdescribe = angular.noop;
-
-/**
- * Defines a test.
- *
- * @param {string} name Name of the test.
- * @param {function()} body Body of the block.
- */
-angular.scenario.Describe.prototype.it = function(name, body) {
- this.its.push({
- id: angular.scenario.Describe.specId++,
- definition: this,
- only: this.only,
- name: name,
- before: this.setupBefore,
- body: body,
- after: this.setupAfter
- });
-};
-
-/**
- * Same as it() but makes iit tests the only test to run.
- *
- * @param {string} name Name of the test.
- * @param {function()} body Body of the block.
- */
-angular.scenario.Describe.prototype.iit = function(name, body) {
- this.it.apply(this, arguments);
- this.its[this.its.length - 1].only = true;
-};
-
-/**
- * Use to disable a test block.
- */
-angular.scenario.Describe.prototype.xit = angular.noop;
-
-/**
- * Gets an array of functions representing all the tests (recursively).
- * that can be executed with SpecRunner's.
- *
- * @return {Array<Object>} Array of it blocks {
- * definition : Object // parent Describe
- * only: boolean
- * name: string
- * before: Function
- * body: Function
- * after: Function
- * }
- */
-angular.scenario.Describe.prototype.getSpecs = function() {
- var specs = arguments[0] || [];
- angular.forEach(this.children, function(child) {
- child.getSpecs(specs);
- });
- angular.forEach(this.its, function(it) {
- specs.push(it);
- });
- var only = [];
- angular.forEach(specs, function(it) {
- if (it.only) {
- only.push(it);
- }
- });
- return (only.length && only) || specs;
-};
-
-/**
- * A future action in a spec.
- *
- * @param {string} name name of the future action
- * @param {function()} behavior future callback(error, result)
- * @param {function()} line Optional. function that returns the file/line number.
- */
-angular.scenario.Future = function(name, behavior, line) {
- this.name = name;
- this.behavior = behavior;
- this.fulfilled = false;
- this.value = undefined;
- this.parser = angular.identity;
- this.line = line || function() { return ''; };
-};
-
-/**
- * Executes the behavior of the closure.
- *
- * @param {function()} doneFn Callback function(error, result)
- */
-angular.scenario.Future.prototype.execute = function(doneFn) {
- var self = this;
- this.behavior(function(error, result) {
- self.fulfilled = true;
- if (result) {
- try {
- result = self.parser(result);
- } catch (e) {
- error = e;
- }
- }
- self.value = error || result;
- doneFn(error, result);
- });
-};
-
-/**
- * Configures the future to convert its final with a function fn(value)
- *
- * @param {function()} fn function(value) that returns the parsed value
- */
-angular.scenario.Future.prototype.parsedWith = function(fn) {
- this.parser = fn;
- return this;
-};
-
-/**
- * Configures the future to parse its final value from JSON
- * into objects.
- */
-angular.scenario.Future.prototype.fromJson = function() {
- return this.parsedWith(angular.fromJson);
-};
-
-/**
- * Configures the future to convert its final value from objects
- * into JSON.
- */
-angular.scenario.Future.prototype.toJson = function() {
- return this.parsedWith(angular.toJson);
-};
-
-/**
- * Maintains an object tree from the runner events.
- *
- * @param {Object} runner The scenario Runner instance to connect to.
- *
- * TODO(esprehn): Every output type creates one of these, but we probably
- * want one global shared instance. Need to handle events better too
- * so the HTML output doesn't need to do spec model.getSpec(spec.id)
- * silliness.
- *
- * TODO(vojta) refactor on, emit methods (from all objects) - use inheritance
- */
-angular.scenario.ObjectModel = function(runner) {
- var self = this;
-
- this.specMap = {};
- this.listeners = [];
- this.value = {
- name: '',
- children: {}
- };
-
- runner.on('SpecBegin', function(spec) {
- var block = self.value,
- definitions = [];
-
- angular.forEach(self.getDefinitionPath(spec), function(def) {
- if (!block.children[def.name]) {
- block.children[def.name] = {
- id: def.id,
- name: def.name,
- children: {},
- specs: {}
- };
- }
- block = block.children[def.name];
- definitions.push(def.name);
- });
-
- var it = self.specMap[spec.id] =
- block.specs[spec.name] =
- new angular.scenario.ObjectModel.Spec(spec.id, spec.name, definitions);
-
- // forward the event
- self.emit('SpecBegin', it);
- });
-
- runner.on('SpecError', function(spec, error) {
- var it = self.getSpec(spec.id);
- it.status = 'error';
- it.error = error;
-
- // forward the event
- self.emit('SpecError', it, error);
+ jqLite(function() {
+ angularInit(window.document, bootstrap);
});
- runner.on('SpecEnd', function(spec) {
- var it = self.getSpec(spec.id);
- complete(it);
-
- // forward the event
- self.emit('SpecEnd', it);
- });
-
- runner.on('StepBegin', function(spec, step) {
- var it = self.getSpec(spec.id);
- step = new angular.scenario.ObjectModel.Step(step.name);
- it.steps.push(step);
-
- // forward the event
- self.emit('StepBegin', it, step);
- });
-
- runner.on('StepEnd', function(spec) {
- var it = self.getSpec(spec.id);
- var step = it.getLastStep();
- if (step.name !== step.name) {
- throw 'Events fired in the wrong order. Step names don\'t match.';
- }
- complete(step);
-
- // forward the event
- self.emit('StepEnd', it, step);
- });
-
- runner.on('StepFailure', function(spec, step, error) {
- var it = self.getSpec(spec.id),
- modelStep = it.getLastStep();
-
- modelStep.setErrorStatus('failure', error, step.line());
- it.setStatusFromStep(modelStep);
-
- // forward the event
- self.emit('StepFailure', it, modelStep, error);
- });
-
- runner.on('StepError', function(spec, step, error) {
- var it = self.getSpec(spec.id),
- modelStep = it.getLastStep();
-
- modelStep.setErrorStatus('error', error, step.line());
- it.setStatusFromStep(modelStep);
-
- // forward the event
- self.emit('StepError', it, modelStep, error);
- });
-
- runner.on('RunnerBegin', function() {
- self.emit('RunnerBegin');
- });
- runner.on('RunnerEnd', function() {
- self.emit('RunnerEnd');
- });
-
- function complete(item) {
- item.endTime = Date.now();
- item.duration = item.endTime - item.startTime;
- item.status = item.status || 'success';
- }
-};
-
-/**
- * Adds a listener for an event.
- *
- * @param {string} eventName Name of the event to add a handler for
- * @param {function()} listener Function that will be called when event is fired
- */
-angular.scenario.ObjectModel.prototype.on = function(eventName, listener) {
- eventName = eventName.toLowerCase();
- this.listeners[eventName] = this.listeners[eventName] || [];
- this.listeners[eventName].push(listener);
-};
-
-/**
- * Emits an event which notifies listeners and passes extra
- * arguments.
- *
- * @param {string} eventName Name of the event to fire.
- */
-angular.scenario.ObjectModel.prototype.emit = function(eventName) {
- var self = this,
- args = Array.prototype.slice.call(arguments, 1);
-
- eventName = eventName.toLowerCase();
-
- if (this.listeners[eventName]) {
- angular.forEach(this.listeners[eventName], function(listener) {
- listener.apply(self, args);
- });
- }
-};
-
-/**
- * Computes the path of definition describe blocks that wrap around
- * this spec.
- *
- * @param spec Spec to compute the path for.
- * @return {Array<Describe>} The describe block path
- */
-angular.scenario.ObjectModel.prototype.getDefinitionPath = function(spec) {
- var path = [];
- var currentDefinition = spec.definition;
- while (currentDefinition && currentDefinition.name) {
- path.unshift(currentDefinition);
- currentDefinition = currentDefinition.parent;
- }
- return path;
-};
-
-/**
- * Gets a spec by id.
- *
- * @param {string} id The id of the spec to get the object for.
- * @return {Object} the Spec instance
- */
-angular.scenario.ObjectModel.prototype.getSpec = function(id) {
- return this.specMap[id];
-};
-
-/**
- * A single it block.
- *
- * @param {string} id Id of the spec
- * @param {string} name Name of the spec
- * @param {Array<string>=} definitionNames List of all describe block names that wrap this spec
- */
-angular.scenario.ObjectModel.Spec = function(id, name, definitionNames) {
- this.id = id;
- this.name = name;
- this.startTime = Date.now();
- this.steps = [];
- this.fullDefinitionName = (definitionNames || []).join(' ');
-};
-
-/**
- * Adds a new step to the Spec.
- *
- * @param {string} name Name of the step (really name of the future)
- * @return {Object} the added step
- */
-angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) {
- var step = new angular.scenario.ObjectModel.Step(name);
- this.steps.push(step);
- return step;
-};
-
-/**
- * Gets the most recent step.
- *
- * @return {Object} the step
- */
-angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() {
- return this.steps[this.steps.length - 1];
-};
-
-/**
- * Set status of the Spec from given Step
- *
- * @param {angular.scenario.ObjectModel.Step} step
- */
-angular.scenario.ObjectModel.Spec.prototype.setStatusFromStep = function(step) {
- if (!this.status || step.status == 'error') {
- this.status = step.status;
- this.error = step.error;
- this.line = step.line;
- }
-};
-
-/**
- * A single step inside a Spec.
- *
- * @param {string} name Name of the step
- */
-angular.scenario.ObjectModel.Step = function(name) {
- this.name = name;
- this.startTime = Date.now();
-};
-
-/**
- * Helper method for setting all error status related properties
- *
- * @param {string} status
- * @param {string} error
- * @param {string} line
- */
-angular.scenario.ObjectModel.Step.prototype.setErrorStatus = function(status, error, line) {
- this.status = status;
- this.error = error;
- this.line = line;
-};
-
-/**
- * Runner for scenarios
- *
- * Has to be initialized before any test is loaded,
- * because it publishes the API into window (global space).
- */
-angular.scenario.Runner = function($window) {
- this.listeners = [];
- this.$window = $window;
- this.rootDescribe = new angular.scenario.Describe();
- this.currentDescribe = this.rootDescribe;
- this.api = {
- it: this.it,
- iit: this.iit,
- xit: angular.noop,
- describe: this.describe,
- ddescribe: this.ddescribe,
- xdescribe: angular.noop,
- beforeEach: this.beforeEach,
- afterEach: this.afterEach
- };
- angular.forEach(this.api, angular.bind(this, function(fn, key) {
- this.$window[key] = angular.bind(this, fn);
- }));
-};
-
-/**
- * Emits an event which notifies listeners and passes extra
- * arguments.
- *
- * @param {string} eventName Name of the event to fire.
- */
-angular.scenario.Runner.prototype.emit = function(eventName) {
- var self = this;
- var args = Array.prototype.slice.call(arguments, 1);
- eventName = eventName.toLowerCase();
- if (!this.listeners[eventName]) {
- return;
- }
- angular.forEach(this.listeners[eventName], function(listener) {
- listener.apply(self, args);
- });
-};
-
-/**
- * Adds a listener for an event.
- *
- * @param {string} eventName The name of the event to add a handler for
- * @param {string} listener The fn(...) that takes the extra arguments from emit()
- */
-angular.scenario.Runner.prototype.on = function(eventName, listener) {
- eventName = eventName.toLowerCase();
- this.listeners[eventName] = this.listeners[eventName] || [];
- this.listeners[eventName].push(listener);
-};
-
-/**
- * Defines a describe block of a spec.
- *
- * @see Describe.js
- *
- * @param {string} name Name of the block
- * @param {function()} body Body of the block
- */
-angular.scenario.Runner.prototype.describe = function(name, body) {
- var self = this;
- this.currentDescribe.describe(name, function() {
- var parentDescribe = self.currentDescribe;
- self.currentDescribe = this;
- try {
- body.call(this);
- } finally {
- self.currentDescribe = parentDescribe;
- }
- });
-};
-
-/**
- * Same as describe, but makes ddescribe the only blocks to run.
- *
- * @see Describe.js
- *
- * @param {string} name Name of the block
- * @param {function()} body Body of the block
- */
-angular.scenario.Runner.prototype.ddescribe = function(name, body) {
- var self = this;
- this.currentDescribe.ddescribe(name, function() {
- var parentDescribe = self.currentDescribe;
- self.currentDescribe = this;
- try {
- body.call(this);
- } finally {
- self.currentDescribe = parentDescribe;
- }
- });
-};
-
-/**
- * Defines a test in a describe block of a spec.
- *
- * @see Describe.js
- *
- * @param {string} name Name of the block
- * @param {function()} body Body of the block
- */
-angular.scenario.Runner.prototype.it = function(name, body) {
- this.currentDescribe.it(name, body);
-};
-
-/**
- * Same as it, but makes iit tests the only tests to run.
- *
- * @see Describe.js
- *
- * @param {string} name Name of the block
- * @param {function()} body Body of the block
- */
-angular.scenario.Runner.prototype.iit = function(name, body) {
- this.currentDescribe.iit(name, body);
-};
-
-/**
- * Defines a function to be called before each it block in the describe
- * (and before all nested describes).
- *
- * @see Describe.js
- *
- * @param {function()} Callback to execute
- */
-angular.scenario.Runner.prototype.beforeEach = function(body) {
- this.currentDescribe.beforeEach(body);
-};
-
-/**
- * Defines a function to be called after each it block in the describe
- * (and before all nested describes).
- *
- * @see Describe.js
- *
- * @param {function()} Callback to execute
- */
-angular.scenario.Runner.prototype.afterEach = function(body) {
- this.currentDescribe.afterEach(body);
-};
-
-/**
- * Creates a new spec runner.
- *
- * @private
- * @param {Object} scope parent scope
- */
-angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) {
- var child = scope.$new();
- var Cls = angular.scenario.SpecRunner;
-
- // Export all the methods to child scope manually as now we don't mess controllers with scopes
- // TODO(vojta): refactor scenario runner so that these objects are not tightly coupled as current
- for (var name in Cls.prototype) {
- child[name] = angular.bind(child, Cls.prototype[name]);
- }
-
- Cls.call(child);
- return child;
-};
-
-/**
- * Runs all the loaded tests with the specified runner class on the
- * provided application.
- *
- * @param {angular.scenario.Application} application App to remote control.
- */
-angular.scenario.Runner.prototype.run = function(application) {
- var self = this;
- var $root = angular.injector(['ng']).get('$rootScope');
- angular.extend($root, this);
- angular.forEach(angular.scenario.Runner.prototype, function(fn, name) {
- $root[name] = angular.bind(self, fn);
- });
- $root.application = application;
- $root.emit('RunnerBegin');
- asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) {
- var dslCache = {};
- var runner = self.createSpecRunner_($root);
- angular.forEach(angular.scenario.dsl, function(fn, key) {
- dslCache[key] = fn.call($root);
- });
- angular.forEach(angular.scenario.dsl, function(fn, key) {
- self.$window[key] = function() {
- var line = callerFile(3);
- var scope = runner.$new();
-
- // Make the dsl accessible on the current chain
- scope.dsl = {};
- angular.forEach(dslCache, function(fn, key) {
- scope.dsl[key] = function() {
- return dslCache[key].apply(scope, arguments);
- };
- });
-
- // Make these methods work on the current chain
- scope.addFuture = function() {
- Array.prototype.push.call(arguments, line);
- return angular.scenario.SpecRunner.
- prototype.addFuture.apply(scope, arguments);
- };
- scope.addFutureAction = function() {
- Array.prototype.push.call(arguments, line);
- return angular.scenario.SpecRunner.
- prototype.addFutureAction.apply(scope, arguments);
- };
-
- return scope.dsl[key].apply(scope, arguments);
- };
- });
- runner.run(spec, function() {
- runner.$destroy();
- specDone.apply(this, arguments);
- });
- },
- function(error) {
- if (error) {
- self.emit('RunnerError', error);
- }
- self.emit('RunnerEnd');
- });
-};
-
-/**
- * This class is the "this" of the it/beforeEach/afterEach method.
- * Responsibilities:
- * - "this" for it/beforeEach/afterEach
- * - keep state for single it/beforeEach/afterEach execution
- * - keep track of all of the futures to execute
- * - run single spec (execute each future)
- */
-angular.scenario.SpecRunner = function() {
- this.futures = [];
- this.afterIndex = 0;
-};
-
-/**
- * Executes a spec which is an it block with associated before/after functions
- * based on the describe nesting.
- *
- * @param {Object} spec A spec object
- * @param {function()} specDone function that is called when the spec finishes,
- * of the form `Function(error, index)`
- */
-angular.scenario.SpecRunner.prototype.run = function(spec, specDone) {
- var self = this;
- this.spec = spec;
-
- this.emit('SpecBegin', spec);
-
- try {
- spec.before.call(this);
- spec.body.call(this);
- this.afterIndex = this.futures.length;
- spec.after.call(this);
- } catch (e) {
- this.emit('SpecError', spec, e);
- this.emit('SpecEnd', spec);
- specDone();
- return;
- }
-
- var handleError = function(error, done) {
- if (self.error) {
- return done();
- }
- self.error = true;
- done(null, self.afterIndex);
- };
-
- asyncForEach(
- this.futures,
- function(future, futureDone) {
- self.step = future;
- self.emit('StepBegin', spec, future);
- try {
- future.execute(function(error) {
- if (error) {
- self.emit('StepFailure', spec, future, error);
- self.emit('StepEnd', spec, future);
- return handleError(error, futureDone);
- }
- self.emit('StepEnd', spec, future);
- self.$window.setTimeout(function() { futureDone(); }, 0);
- });
- } catch (e) {
- self.emit('StepError', spec, future, e);
- self.emit('StepEnd', spec, future);
- handleError(e, futureDone);
- }
- },
- function(e) {
- if (e) {
- self.emit('SpecError', spec, e);
- }
- self.emit('SpecEnd', spec);
- // Call done in a timeout so exceptions don't recursively
- // call this function
- self.$window.setTimeout(function() { specDone(); }, 0);
- }
- );
-};
-
-/**
- * Adds a new future action.
- *
- * Note: Do not pass line manually. It happens automatically.
- *
- * @param {string} name Name of the future
- * @param {function()} behavior Behavior of the future
- * @param {function()} line fn() that returns file/line number
- */
-angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) {
- var future = new angular.scenario.Future(name, angular.bind(this, behavior), line);
- this.futures.push(future);
- return future;
-};
-
-/**
- * Adds a new future action to be executed on the application window.
- *
- * Note: Do not pass line manually. It happens automatically.
- *
- * @param {string} name Name of the future
- * @param {function()} behavior Behavior of the future
- * @param {function()} line fn() that returns file/line number
- */
-angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) {
- var self = this;
- var NG = /\[ng\\\:/;
- return this.addFuture(name, function(done) {
- this.application.executeAction(function($window, $document) {
-
- //TODO(esprehn): Refactor this so it doesn't need to be in here.
- $document.elements = function(selector) {
- var args = Array.prototype.slice.call(arguments, 1);
- selector = (self.selector || '') + ' ' + (selector || '');
- selector = _jQuery.trim(selector) || '*';
- angular.forEach(args, function(value, index) {
- selector = selector.replace('$' + (index + 1), value);
- });
- var result = $document.find(selector);
- if (selector.match(NG)) {
- angular.forEach(['[ng-','[data-ng-','[x-ng-'], function(value, index) {
- result = result.add(selector.replace(NG, value), $document);
- });
- }
- if (!result.length) {
- throw {
- type: 'selector',
- message: 'Selector ' + selector + ' did not match any elements.'
- };
- }
-
- return result;
- };
-
- try {
- behavior.call(self, $window, $document, done);
- } catch (e) {
- if (e.type && e.type === 'selector') {
- done(e.message);
- } else {
- throw e;
- }
- }
- });
- }, line);
-};
-
-/**
- * Shared DSL statements that are useful to all scenarios.
- */
-
- /**
- * Usage:
- * pause() pauses until you call resume() in the console
- */
-angular.scenario.dsl('pause', function() {
- return function() {
- return this.addFuture('pausing for you to resume', function(done) {
- this.emit('InteractivePause', this.spec, this.step);
- this.$window.resume = function() { done(); };
- });
- };
-});
-
-/**
- * Usage:
- * sleep(seconds) pauses the test for specified number of seconds
- */
-angular.scenario.dsl('sleep', function() {
- return function(time) {
- return this.addFuture('sleep for ' + time + ' seconds', function(done) {
- this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000);
- });
- };
-});
-
-/**
- * Usage:
- * browser().navigateTo(url) Loads the url into the frame
- * browser().navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to
- * browser().reload() refresh the page (reload the same URL)
- * browser().window.href() window.location.href
- * browser().window.path() window.location.pathname
- * browser().window.search() window.location.search
- * browser().window.hash() window.location.hash without # prefix
- * browser().location().url() see ng.$location#url
- * browser().location().path() see ng.$location#path
- * browser().location().search() see ng.$location#search
- * browser().location().hash() see ng.$location#hash
- */
-angular.scenario.dsl('browser', function() {
- var chain = {};
-
- chain.navigateTo = function(url, delegate) {
- var application = this.application;
- return this.addFuture("browser navigate to '" + url + "'", function(done) {
- if (delegate) {
- url = delegate.call(this, url);
- }
- application.navigateTo(url, function() {
- done(null, url);
- }, done);
- });
- };
-
- chain.reload = function() {
- var application = this.application;
- return this.addFutureAction('browser reload', function($window, $document, done) {
- var href = $window.location.href;
- application.navigateTo(href, function() {
- done(null, href);
- }, done);
- });
- };
-
- chain.window = function() {
- var api = {};
-
- api.href = function() {
- return this.addFutureAction('window.location.href', function($window, $document, done) {
- done(null, $window.location.href);
- });
- };
-
- api.path = function() {
- return this.addFutureAction('window.location.path', function($window, $document, done) {
- done(null, $window.location.pathname);
- });
- };
-
- api.search = function() {
- return this.addFutureAction('window.location.search', function($window, $document, done) {
- done(null, $window.location.search);
- });
- };
-
- api.hash = function() {
- return this.addFutureAction('window.location.hash', function($window, $document, done) {
- done(null, $window.location.hash.replace('#', ''));
- });
- };
-
- return api;
- };
-
- chain.location = function() {
- var api = {};
-
- api.url = function() {
- return this.addFutureAction('$location.url()', function($window, $document, done) {
- done(null, $document.injector().get('$location').url());
- });
- };
-
- api.path = function() {
- return this.addFutureAction('$location.path()', function($window, $document, done) {
- done(null, $document.injector().get('$location').path());
- });
- };
-
- api.search = function() {
- return this.addFutureAction('$location.search()', function($window, $document, done) {
- done(null, $document.injector().get('$location').search());
- });
- };
-
- api.hash = function() {
- return this.addFutureAction('$location.hash()', function($window, $document, done) {
- done(null, $document.injector().get('$location').hash());
- });
- };
-
- return api;
- };
-
- return function() {
- return chain;
- };
-});
-
-/**
- * Usage:
- * expect(future).{matcher} where matcher is one of the matchers defined
- * with angular.scenario.matcher
- *
- * ex. expect(binding("name")).toEqual("Elliott")
- */
-angular.scenario.dsl('expect', function() {
- var chain = angular.extend({}, angular.scenario.matcher);
-
- chain.not = function() {
- this.inverse = true;
- return chain;
- };
-
- return function(future) {
- this.future = future;
- return chain;
- };
-});
-
-/**
- * Usage:
- * using(selector, label) scopes the next DSL element selection
- *
- * ex.
- * using('#foo', "'Foo' text field").input('bar')
- */
-angular.scenario.dsl('using', function() {
- return function(selector, label) {
- this.selector = _jQuery.trim((this.selector || '') + ' ' + selector);
- if (angular.isString(label) && label.length) {
- this.label = label + ' ( ' + this.selector + ' )';
- } else {
- this.label = this.selector;
- }
- return this.dsl;
- };
-});
-
-/**
- * Usage:
- * binding(name) returns the value of the first matching binding
- */
-angular.scenario.dsl('binding', function() {
- return function(name) {
- return this.addFutureAction("select binding '" + name + "'",
- function($window, $document, done) {
- var values = $document.elements().bindings($window.angular.element, name);
- if (!values.length) {
- return done("Binding selector '" + name + "' did not match.");
- }
- done(null, values[0]);
- });
- };
-});
-
-/**
- * Usage:
- * input(name).enter(value) enters value in input with specified name
- * input(name).check() checks checkbox
- * input(name).select(value) selects the radio button with specified name/value
- * input(name).val() returns the value of the input.
- */
-angular.scenario.dsl('input', function() {
- var chain = {};
- var supportInputEvent = 'oninput' in window.document.createElement('div') && !msie;
-
- chain.enter = function(value, event) {
- return this.addFutureAction("input '" + this.name + "' enter '" + value + "'",
- function($window, $document, done) {
- var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
- input.val(value);
- input.trigger(event || (supportInputEvent ? 'input' : 'change'));
- done();
- });
- };
-
- chain.check = function() {
- return this.addFutureAction("checkbox '" + this.name + "' toggle",
- function($window, $document, done) {
- var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':checkbox');
- input.trigger('click');
- done();
- });
- };
-
- chain.select = function(value) {
- return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'",
- function($window, $document, done) {
- var input = $document.
- elements('[ng\\:model="$1"][value="$2"]', this.name, value).filter(':radio');
- input.trigger('click');
- done();
- });
- };
-
- chain.val = function() {
- return this.addFutureAction("return input val", function($window, $document, done) {
- var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
- done(null,input.val());
- });
- };
-
- return function(name) {
- this.name = name;
- return chain;
- };
-});
-
-
-/**
- * Usage:
- * repeater('#products table', 'Product List').count() number of rows
- * repeater('#products table', 'Product List').row(1) all bindings in row as an array
- * repeater('#products table', 'Product List').column('product.name') all values across all rows
- * in an array
- */
-angular.scenario.dsl('repeater', function() {
- var chain = {};
-
- chain.count = function() {
- return this.addFutureAction("repeater '" + this.label + "' count",
- function($window, $document, done) {
- try {
- done(null, $document.elements().length);
- } catch (e) {
- done(null, 0);
- }
- });
- };
-
- chain.column = function(binding) {
- return this.addFutureAction("repeater '" + this.label + "' column '" + binding + "'",
- function($window, $document, done) {
- done(null, $document.elements().bindings($window.angular.element, binding));
- });
- };
-
- chain.row = function(index) {
- return this.addFutureAction("repeater '" + this.label + "' row '" + index + "'",
- function($window, $document, done) {
- var matches = $document.elements().slice(index, index + 1);
- if (!matches.length) {
- return done('row ' + index + ' out of bounds');
- }
- done(null, matches.bindings($window.angular.element));
- });
- };
-
- return function(selector, label) {
- this.dsl.using(selector, label);
- return chain;
- };
-});
-
-/**
- * Usage:
- * select(name).option('value') select one option
- * select(name).options('value1', 'value2', ...) select options from a multi select
- */
-angular.scenario.dsl('select', function() {
- var chain = {};
-
- chain.option = function(value) {
- return this.addFutureAction("select '" + this.name + "' option '" + value + "'",
- function($window, $document, done) {
- var select = $document.elements('select[ng\\:model="$1"]', this.name);
- var option = select.find('option[value="' + value + '"]');
- if (option.length) {
- select.val(value);
- } else {
- option = select.find('option').filter(function() {
- return _jQuery(this).text() === value;
- });
- if (!option.length) {
- option = select.find('option:contains("' + value + '")');
- }
- if (option.length) {
- select.val(option.val());
- } else {
- return done("option '" + value + "' not found");
- }
- }
- select.trigger('change');
- done();
- });
- };
-
- chain.options = function() {
- var values = arguments;
- return this.addFutureAction("select '" + this.name + "' options '" + values + "'",
- function($window, $document, done) {
- var select = $document.elements('select[multiple][ng\\:model="$1"]', this.name);
- select.val(values);
- select.trigger('change');
- done();
- });
- };
-
- return function(name) {
- this.name = name;
- return chain;
- };
-});
-
-/**
- * Usage:
- * element(selector, label).count() get the number of elements that match selector
- * element(selector, label).click() clicks an element
- * element(selector, label).mouseover() mouseover an element
- * element(selector, label).mousedown() mousedown an element
- * element(selector, label).mouseup() mouseup an element
- * element(selector, label).query(fn) executes fn(selectedElements, done)
- * element(selector, label).{method}() gets the value (as defined by jQuery, ex. val)
- * element(selector, label).{method}(value) sets the value (as defined by jQuery, ex. val)
- * element(selector, label).{method}(key) gets the value (as defined by jQuery, ex. attr)
- * element(selector, label).{method}(key, value) sets the value (as defined by jQuery, ex. attr)
- */
-angular.scenario.dsl('element', function() {
- var KEY_VALUE_METHODS = ['attr', 'css', 'prop'];
- var VALUE_METHODS = [
- 'val', 'text', 'html', 'height', 'innerHeight', 'outerHeight', 'width',
- 'innerWidth', 'outerWidth', 'position', 'scrollLeft', 'scrollTop', 'offset'
- ];
- var chain = {};
-
- chain.count = function() {
- return this.addFutureAction("element '" + this.label + "' count",
- function($window, $document, done) {
- try {
- done(null, $document.elements().length);
- } catch (e) {
- done(null, 0);
- }
- });
- };
-
- chain.click = function() {
- return this.addFutureAction("element '" + this.label + "' click",
- function($window, $document, done) {
- var elements = $document.elements();
- var href = elements.attr('href');
- var eventProcessDefault = elements.trigger('click')[0];
-
- if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) {
- this.application.navigateTo(href, function() {
- done();
- }, done);
- } else {
- done();
- }
- });
- };
-
- chain.dblclick = function() {
- return this.addFutureAction("element '" + this.label + "' dblclick",
- function($window, $document, done) {
- var elements = $document.elements();
- var href = elements.attr('href');
- var eventProcessDefault = elements.trigger('dblclick')[0];
-
- if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) {
- this.application.navigateTo(href, function() {
- done();
- }, done);
- } else {
- done();
- }
- });
- };
-
- chain.mouseover = function() {
- return this.addFutureAction("element '" + this.label + "' mouseover",
- function($window, $document, done) {
- var elements = $document.elements();
- elements.trigger('mouseover');
- done();
- });
- };
-
- chain.mousedown = function() {
- return this.addFutureAction("element '" + this.label + "' mousedown",
- function($window, $document, done) {
- var elements = $document.elements();
- elements.trigger('mousedown');
- done();
- });
- };
-
- chain.mouseup = function() {
- return this.addFutureAction("element '" + this.label + "' mouseup",
- function($window, $document, done) {
- var elements = $document.elements();
- elements.trigger('mouseup');
- done();
- });
- };
-
- chain.query = function(fn) {
- return this.addFutureAction('element ' + this.label + ' custom query',
- function($window, $document, done) {
- fn.call(this, $document.elements(), done);
- });
- };
-
- angular.forEach(KEY_VALUE_METHODS, function(methodName) {
- chain[methodName] = function(name, value) {
- var args = arguments,
- futureName = (args.length == 1)
- ? "element '" + this.label + "' get " + methodName + " '" + name + "'"
- : "element '" + this.label + "' set " + methodName + " '" + name + "' to " + "'" +
- value + "'";
-
- return this.addFutureAction(futureName, function($window, $document, done) {
- var element = $document.elements();
- done(null, element[methodName].apply(element, args));
- });
- };
- });
-
- angular.forEach(VALUE_METHODS, function(methodName) {
- chain[methodName] = function(value) {
- var args = arguments,
- futureName = (args.length === 0)
- ? "element '" + this.label + "' " + methodName
- : "element '" + this.label + "' set " + methodName + " to '" + value + "'";
-
- return this.addFutureAction(futureName, function($window, $document, done) {
- var element = $document.elements();
- done(null, element[methodName].apply(element, args));
- });
- };
- });
-
- return function(selector, label) {
- this.dsl.using(selector, label);
- return chain;
- };
-});
-
-/**
- * Matchers for implementing specs. Follows the Jasmine spec conventions.
- */
-
-angular.scenario.matcher('toEqual', function(expected) {
- return angular.equals(this.actual, expected);
-});
-
-angular.scenario.matcher('toBe', function(expected) {
- return this.actual === expected;
-});
-
-angular.scenario.matcher('toBeDefined', function() {
- return angular.isDefined(this.actual);
-});
-
-angular.scenario.matcher('toBeTruthy', function() {
- return this.actual;
-});
-
-angular.scenario.matcher('toBeFalsy', function() {
- return !this.actual;
-});
-
-angular.scenario.matcher('toMatch', function(expected) {
- return new RegExp(expected).test(this.actual);
-});
-
-angular.scenario.matcher('toBeNull', function() {
- return this.actual === null;
-});
-
-angular.scenario.matcher('toContain', function(expected) {
- return includes(this.actual, expected);
-});
-
-angular.scenario.matcher('toBeLessThan', function(expected) {
- return this.actual < expected;
-});
-
-angular.scenario.matcher('toBeGreaterThan', function(expected) {
- return this.actual > expected;
-});
-
-/**
- * User Interface for the Scenario Runner.
- *
- * TODO(esprehn): This should be refactored now that ObjectModel exists
- * to use angular bindings for the UI.
- */
-angular.scenario.output('html', function(context, runner, model) {
- var specUiMap = {},
- lastStepUiMap = {};
-
- context.append(
- '<div id="header">' +
- ' <h1><span class="angular">AngularJS</span>: Scenario Test Runner</h1>' +
- ' <ul id="status-legend" class="status-display">' +
- ' <li class="status-error">0 Errors</li>' +
- ' <li class="status-failure">0 Failures</li>' +
- ' <li class="status-success">0 Passed</li>' +
- ' </ul>' +
- '</div>' +
- '<div id="specs">' +
- ' <div class="test-children"></div>' +
- '</div>'
- );
-
- runner.on('InteractivePause', function(spec) {
- var ui = lastStepUiMap[spec.id];
- ui.find('.test-title').
- html('paused... <a href="javascript:resume()">resume</a> when ready.');
- });
-
- runner.on('SpecBegin', function(spec) {
- var ui = findContext(spec);
- ui.find('> .tests').append(
- '<li class="status-pending test-it"></li>'
- );
- ui = ui.find('> .tests li:last');
- ui.append(
- '<div class="test-info">' +
- ' <p class="test-title">' +
- ' <span class="timer-result"></span>' +
- ' <span class="test-name"></span>' +
- ' </p>' +
- '</div>' +
- '<div class="scrollpane">' +
- ' <ol class="test-actions"></ol>' +
- '</div>'
- );
- ui.find('> .test-info .test-name').text(spec.name);
- ui.find('> .test-info').click(function() {
- var scrollpane = ui.find('> .scrollpane');
- var actions = scrollpane.find('> .test-actions');
- var name = context.find('> .test-info .test-name');
- if (actions.find(':visible').length) {
- actions.hide();
- name.removeClass('open').addClass('closed');
- } else {
- actions.show();
- scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
- name.removeClass('closed').addClass('open');
- }
- });
-
- specUiMap[spec.id] = ui;
- });
-
- runner.on('SpecError', function(spec, error) {
- var ui = specUiMap[spec.id];
- ui.append('<pre></pre>');
- ui.find('> pre').text(formatException(error));
- });
-
- runner.on('SpecEnd', function(spec) {
- var ui = specUiMap[spec.id];
- spec = model.getSpec(spec.id);
- ui.removeClass('status-pending');
- ui.addClass('status-' + spec.status);
- ui.find("> .test-info .timer-result").text(spec.duration + "ms");
- if (spec.status === 'success') {
- ui.find('> .test-info .test-name').addClass('closed');
- ui.find('> .scrollpane .test-actions').hide();
- }
- updateTotals(spec.status);
- });
-
- runner.on('StepBegin', function(spec, step) {
- var ui = specUiMap[spec.id];
- spec = model.getSpec(spec.id);
- step = spec.getLastStep();
- ui.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>');
- var stepUi = lastStepUiMap[spec.id] = ui.find('> .scrollpane .test-actions li:last');
- stepUi.append(
- '<div class="timer-result"></div>' +
- '<div class="test-title"></div>'
- );
- stepUi.find('> .test-title').text(step.name);
- var scrollpane = stepUi.parents('.scrollpane');
- scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
- });
-
- runner.on('StepFailure', function(spec, step, error) {
- var ui = lastStepUiMap[spec.id];
- addError(ui, step.line, error);
- });
-
- runner.on('StepError', function(spec, step, error) {
- var ui = lastStepUiMap[spec.id];
- addError(ui, step.line, error);
- });
-
- runner.on('StepEnd', function(spec, step) {
- var stepUi = lastStepUiMap[spec.id];
- spec = model.getSpec(spec.id);
- step = spec.getLastStep();
- stepUi.find('.timer-result').text(step.duration + 'ms');
- stepUi.removeClass('status-pending');
- stepUi.addClass('status-' + step.status);
- var scrollpane = specUiMap[spec.id].find('> .scrollpane');
- scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
- });
-
- /**
- * Finds the context of a spec block defined by the passed definition.
- *
- * @param {Object} The definition created by the Describe object.
- */
- function findContext(spec) {
- var currentContext = context.find('#specs');
- angular.forEach(model.getDefinitionPath(spec), function(defn) {
- var id = 'describe-' + defn.id;
- if (!context.find('#' + id).length) {
- currentContext.find('> .test-children').append(
- '<div class="test-describe" id="' + id + '">' +
- ' <h2></h2>' +
- ' <div class="test-children"></div>' +
- ' <ul class="tests"></ul>' +
- '</div>'
- );
- context.find('#' + id).find('> h2').text('describe: ' + defn.name);
- }
- currentContext = context.find('#' + id);
- });
- return context.find('#describe-' + spec.definition.id);
- }
-
- /**
- * Updates the test counter for the status.
- *
- * @param {string} the status.
- */
- function updateTotals(status) {
- var legend = context.find('#status-legend .status-' + status);
- var parts = legend.text().split(' ');
- var value = (parts[0] * 1) + 1;
- legend.text(value + ' ' + parts[1]);
- }
-
- /**
- * Add an error to a step.
- *
- * @param {Object} The JQuery wrapped context
- * @param {function()} fn() that should return the file/line number of the error
- * @param {Object} the error.
- */
- function addError(context, line, error) {
- context.find('.test-title').append('<pre></pre>');
- var message = _jQuery.trim(line() + '\n\n' + formatException(error));
- context.find('.test-title pre:last').text(message);
- }
-});
-
-/**
- * Generates JSON output into a context.
- */
-angular.scenario.output('json', function(context, runner, model) {
- model.on('RunnerEnd', function() {
- context.text(angular.toJson(model.value));
- });
-});
-
-/**
- * Generates XML output into a context.
- */
-angular.scenario.output('xml', function(context, runner, model) {
- var $ = function(args) {return new context.init(args);};
- model.on('RunnerEnd', function() {
- var scenario = $('<scenario></scenario>');
- context.append(scenario);
- serializeXml(scenario, model.value);
- });
-
- /**
- * Convert the tree into XML.
- *
- * @param {Object} context jQuery context to add the XML to.
- * @param {Object} tree node to serialize
- */
- function serializeXml(context, tree) {
- angular.forEach(tree.children, function(child) {
- var describeContext = $('<describe></describe>');
- describeContext.attr('id', child.id);
- describeContext.attr('name', child.name);
- context.append(describeContext);
- serializeXml(describeContext, child);
- });
- var its = $('<its></its>');
- context.append(its);
- angular.forEach(tree.specs, function(spec) {
- var it = $('<it></it>');
- it.attr('id', spec.id);
- it.attr('name', spec.name);
- it.attr('duration', spec.duration);
- it.attr('status', spec.status);
- its.append(it);
- angular.forEach(spec.steps, function(step) {
- var stepContext = $('<step></step>');
- stepContext.attr('name', step.name);
- stepContext.attr('duration', step.duration);
- stepContext.attr('status', step.status);
- it.append(stepContext);
- if (step.error) {
- var error = $('<error></error>');
- stepContext.append(error);
- error.text(formatException(step.error));
- }
- });
- });
- }
-});
-
-/**
- * Creates a global value $result with the result of the runner.
- */
-angular.scenario.output('object', function(context, runner, model) {
- runner.$window.$result = model.value;
-});
-
-bindJQuery();
-publishExternalAPI(angular);
-
-var $runner = new angular.scenario.Runner(window),
- scripts = window.document.getElementsByTagName('script'),
- script = scripts[scripts.length - 1],
- config = {};
-
-angular.forEach(script.attributes, function(attr) {
- var match = attr.name.match(/ng[:\-](.*)/);
- if (match) {
- config[match[1]] = attr.value || true;
- }
-});
-
-if (config.autotest) {
- JQLite(window.document).ready(function() {
- angular.scenario.setUpAndRun(config);
- });
-}
})(window);
-
-!window.angular.$$csp().noInlineStyle && window.angular.element(document.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\n.ng-animate-shim {\n visibility:hidden;\n}\n\n.ng-anchor {\n position:absolute;\n}\n</style>');
-!window.angular.$$csp().noInlineStyle && window.angular.element(document.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
+!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(window.angular.element('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>'));
diff --git a/xstatic/pkg/angular/data/angular-touch.js b/xstatic/pkg/angular/data/angular-touch.js
index 79b8d5e..eb73beb 100644
--- a/xstatic/pkg/angular/data/angular-touch.js
+++ b/xstatic/pkg/angular/data/angular-touch.js
@@ -1,135 +1,38 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular) {'use strict';
-/* global ngTouchClickDirectiveFactory: false,
- */
-
/**
* @ngdoc module
* @name ngTouch
* @description
*
- * # ngTouch
- *
- * The `ngTouch` module provides touch events and other helpers for touch-enabled devices.
+ * The `ngTouch` module provides helpers for touch-enabled devices.
* The implementation is based on jQuery Mobile touch event handling
- * ([jquerymobile.com](http://jquerymobile.com/)).
- *
+ * ([jquerymobile.com](http://jquerymobile.com/)). *
*
* See {@link ngTouch.$swipe `$swipe`} for usage.
*
- * <div doc-module-components="ngTouch"></div>
- *
+ * @deprecated
+ * sinceVersion="1.7.0"
+ * The ngTouch module with the {@link ngTouch.$swipe `$swipe`} service and
+ * the {@link ngTouch.ngSwipeLeft} and {@link ngTouch.ngSwipeRight} directives are
+ * deprecated. Instead, stand-alone libraries for touch handling and gesture interaction
+ * should be used, for example [HammerJS](https://hammerjs.github.io/) (which is also used by
+ * Angular).
*/
// define ngTouch module
-/* global -ngTouch */
+/* global ngTouch */
var ngTouch = angular.module('ngTouch', []);
-ngTouch.provider('$touch', $TouchProvider);
+ngTouch.info({ angularVersion: '"1.8.2"' });
function nodeName_(element) {
- return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName));
-}
-
-/**
- * @ngdoc provider
- * @name $touchProvider
- *
- * @description
- * The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}.
- */
-$TouchProvider.$inject = ['$provide', '$compileProvider'];
-function $TouchProvider($provide, $compileProvider) {
-
- /**
- * @ngdoc method
- * @name $touchProvider#ngClickOverrideEnabled
- *
- * @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the
- * current ngClickOverrideEnabled state
- * @returns {*} current value if used as getter or itself (chaining) if used as setter
- *
- * @kind function
- *
- * @description
- * Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled,
- * the default ngClick directive will be replaced by a version that eliminates the 300ms delay for
- * click events on browser for touch-devices.
- *
- * The default is `false`.
- *
- */
- var ngClickOverrideEnabled = false;
- var ngClickDirectiveAdded = false;
- this.ngClickOverrideEnabled = function(enabled) {
- if (angular.isDefined(enabled)) {
-
- if (enabled && !ngClickDirectiveAdded) {
- ngClickDirectiveAdded = true;
-
- // Use this to identify the correct directive in the delegate
- ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch';
- $compileProvider.directive('ngClick', ngTouchClickDirectiveFactory);
-
- $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
- if (ngClickOverrideEnabled) {
- // drop the default ngClick directive
- $delegate.shift();
- } else {
- // drop the ngTouch ngClick directive if the override has been re-disabled (because
- // we cannot de-register added directives)
- var i = $delegate.length - 1;
- while (i >= 0) {
- if ($delegate[i].$$moduleName === 'ngTouch') {
- $delegate.splice(i, 1);
- break;
- }
- i--;
- }
- }
-
- return $delegate;
- }]);
- }
-
- ngClickOverrideEnabled = enabled;
- return this;
- }
-
- return ngClickOverrideEnabled;
- };
-
- /**
- * @ngdoc service
- * @name $touch
- * @kind object
- *
- * @description
- * Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method.
- *
- */
- this.$get = function() {
- return {
- /**
- * @ngdoc method
- * @name $touch#ngClickOverrideEnabled
- *
- * @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider},
- * i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled.
- *
- * @kind function
- */
- ngClickOverrideEnabled: function() {
- return ngClickOverrideEnabled;
- }
- };
- };
-
+ return angular.$$lowercase(element.nodeName || (element[0] && element[0].nodeName));
}
/* global ngTouch: false */
@@ -138,6 +41,11 @@ function $TouchProvider($provide, $compileProvider) {
* @ngdoc service
* @name $swipe
*
+ * @deprecated
+ * sinceVersion="1.7.0"
+ *
+ * See the {@link ngTouch module} documentation for more information.
+ *
* @description
* The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe
* behavior, to make implementing swipe-related directives more convenient.
@@ -249,13 +157,17 @@ ngTouch.factory('$swipe', [function() {
totalX = 0;
totalY = 0;
lastPos = startCoords;
- eventHandlers['start'] && eventHandlers['start'](startCoords, event);
+ if (eventHandlers['start']) {
+ eventHandlers['start'](startCoords, event);
+ }
});
var events = getEvents(pointerTypes, 'cancel');
if (events) {
element.on(events, function(event) {
active = false;
- eventHandlers['cancel'] && eventHandlers['cancel'](event);
+ if (eventHandlers['cancel']) {
+ eventHandlers['cancel'](event);
+ }
});
}
@@ -284,325 +196,41 @@ ngTouch.factory('$swipe', [function() {
if (totalY > totalX) {
// Allow native scrolling to take over.
active = false;
- eventHandlers['cancel'] && eventHandlers['cancel'](event);
+ if (eventHandlers['cancel']) {
+ eventHandlers['cancel'](event);
+ }
return;
} else {
// Prevent the browser from scrolling.
event.preventDefault();
- eventHandlers['move'] && eventHandlers['move'](coords, event);
+ if (eventHandlers['move']) {
+ eventHandlers['move'](coords, event);
+ }
}
});
element.on(getEvents(pointerTypes, 'end'), function(event) {
if (!active) return;
active = false;
- eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event);
+ if (eventHandlers['end']) {
+ eventHandlers['end'](getCoordinates(event), event);
+ }
});
}
};
}]);
-/* global ngTouch: false,
- nodeName_: false
-*/
-
-/**
- * @ngdoc directive
- * @name ngClick
- * @deprecated
- *
- * @description
- * <div class="alert alert-danger">
- * **DEPRECATION NOTICE**: Beginning with Angular 1.5, this directive is deprecated and by default **disabled**.
- * The directive will receive no further support and might be removed from future releases.
- * If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled}
- * function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick).
- * To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/)
- * gives a good overview.
- * </div>
- * A more powerful replacement for the default ngClick designed to be used on touchscreen
- * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending
- * the click event. This version handles them immediately, and then prevents the
- * following click event from propagating.
- *
- * Requires the {@link ngTouch `ngTouch`} module to be installed.
- *
- * This directive can fall back to using an ordinary click event, and so works on desktop
- * browsers as well as mobile.
- *
- * This directive also sets the CSS class `ng-click-active` while the element is being held
- * down (by a mouse click or touch) so you can restyle the depressed element if you wish.
- *
- * @element ANY
- * @param {expression} ngClick {@link guide/expression Expression} to evaluate
- * upon tap. (Event object is available as `$event`)
- *
- * @example
- <example module="ngClickExample" deps="angular-touch.js">
- <file name="index.html">
- <button ng-click="count = count + 1" ng-init="count=0">
- Increment
- </button>
- count: {{ count }}
- </file>
- <file name="script.js">
- angular.module('ngClickExample', ['ngTouch']);
- </file>
- </example>
- */
-
-var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
- function($parse, $timeout, $rootElement) {
- var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
- var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
- var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click
- var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
-
- var ACTIVE_CLASS_NAME = 'ng-click-active';
- var lastPreventedTime;
- var touchCoordinates;
- var lastLabelClickCoordinates;
-
-
- // TAP EVENTS AND GHOST CLICKS
- //
- // Why tap events?
- // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're
- // double-tapping, and then fire a click event.
- //
- // This delay sucks and makes mobile apps feel unresponsive.
- // So we detect touchstart, touchcancel and touchend ourselves and determine when
- // the user has tapped on something.
- //
- // What happens when the browser then generates a click event?
- // The browser, of course, also detects the tap and fires a click after a delay. This results in
- // tapping/clicking twice. We do "clickbusting" to prevent it.
- //
- // How does it work?
- // We attach global touchstart and click handlers, that run during the capture (early) phase.
- // So the sequence for a tap is:
- // - global touchstart: Sets an "allowable region" at the point touched.
- // - element's touchstart: Starts a touch
- // (- touchcancel ends the touch, no click follows)
- // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
- // too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
- // - preventGhostClick() removes the allowable region the global touchstart created.
- // - The browser generates a click event.
- // - The global click handler catches the click, and checks whether it was in an allowable region.
- // - If preventGhostClick was called, the region will have been removed, the click is busted.
- // - If the region is still there, the click proceeds normally. Therefore clicks on links and
- // other elements without ngTap on them work normally.
- //
- // This is an ugly, terrible hack!
- // Yeah, tell me about it. The alternatives are using the slow click events, or making our users
- // deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular
- // encapsulates this ugly logic away from the user.
- //
- // Why not just put click handlers on the element?
- // We do that too, just to be sure. If the tap event caused the DOM to change,
- // it is possible another element is now in that position. To take account for these possibly
- // distinct elements, the handlers are global and care only about coordinates.
-
- // Checks if the coordinates are close enough to be within the region.
- function hit(x1, y1, x2, y2) {
- return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD;
- }
-
- // Checks a list of allowable regions against a click location.
- // Returns true if the click should be allowed.
- // Splices out the allowable region from the list after it has been used.
- function checkAllowableRegions(touchCoordinates, x, y) {
- for (var i = 0; i < touchCoordinates.length; i += 2) {
- if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) {
- touchCoordinates.splice(i, i + 2);
- return true; // allowable region
- }
- }
- return false; // No allowable region; bust it.
- }
-
- // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick
- // was called recently.
- function onClick(event) {
- if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
- return; // Too old.
- }
-
- var touches = event.touches && event.touches.length ? event.touches : [event];
- var x = touches[0].clientX;
- var y = touches[0].clientY;
- // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label
- // and on the input element). Depending on the exact browser, this second click we don't want
- // to bust has either (0,0), negative coordinates, or coordinates equal to triggering label
- // click event
- if (x < 1 && y < 1) {
- return; // offscreen
- }
- if (lastLabelClickCoordinates &&
- lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) {
- return; // input click triggered by label click
- }
- // reset label click coordinates on first subsequent click
- if (lastLabelClickCoordinates) {
- lastLabelClickCoordinates = null;
- }
- // remember label click coordinates to prevent click busting of trigger click event on input
- if (nodeName_(event.target) === 'label') {
- lastLabelClickCoordinates = [x, y];
- }
-
- // Look for an allowable region containing this click.
- // If we find one, that means it was created by touchstart and not removed by
- // preventGhostClick, so we don't bust it.
- if (checkAllowableRegions(touchCoordinates, x, y)) {
- return;
- }
-
- // If we didn't find an allowable region, bust the click.
- event.stopPropagation();
- event.preventDefault();
-
- // Blur focused form elements
- event.target && event.target.blur && event.target.blur();
- }
-
-
- // Global touchstart handler that creates an allowable region for a click event.
- // This allowable region can be removed by preventGhostClick if we want to bust it.
- function onTouchStart(event) {
- var touches = event.touches && event.touches.length ? event.touches : [event];
- var x = touches[0].clientX;
- var y = touches[0].clientY;
- touchCoordinates.push(x, y);
-
- $timeout(function() {
- // Remove the allowable region.
- for (var i = 0; i < touchCoordinates.length; i += 2) {
- if (touchCoordinates[i] == x && touchCoordinates[i + 1] == y) {
- touchCoordinates.splice(i, i + 2);
- return;
- }
- }
- }, PREVENT_DURATION, false);
- }
-
- // On the first call, attaches some event handlers. Then whenever it gets called, it creates a
- // zone around the touchstart where clicks will get busted.
- function preventGhostClick(x, y) {
- if (!touchCoordinates) {
- $rootElement[0].addEventListener('click', onClick, true);
- $rootElement[0].addEventListener('touchstart', onTouchStart, true);
- touchCoordinates = [];
- }
-
- lastPreventedTime = Date.now();
-
- checkAllowableRegions(touchCoordinates, x, y);
- }
-
- // Actual linking function.
- return function(scope, element, attr) {
- var clickHandler = $parse(attr.ngClick),
- tapping = false,
- tapElement, // Used to blur the element after a tap.
- startTime, // Used to check if the tap was held too long.
- touchStartX,
- touchStartY;
-
- function resetState() {
- tapping = false;
- element.removeClass(ACTIVE_CLASS_NAME);
- }
-
- element.on('touchstart', function(event) {
- tapping = true;
- tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement.
- // Hack for Safari, which can target text nodes instead of containers.
- if (tapElement.nodeType == 3) {
- tapElement = tapElement.parentNode;
- }
-
- element.addClass(ACTIVE_CLASS_NAME);
-
- startTime = Date.now();
-
- // Use jQuery originalEvent
- var originalEvent = event.originalEvent || event;
- var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent];
- var e = touches[0];
- touchStartX = e.clientX;
- touchStartY = e.clientY;
- });
-
- element.on('touchcancel', function(event) {
- resetState();
- });
-
- element.on('touchend', function(event) {
- var diff = Date.now() - startTime;
-
- // Use jQuery originalEvent
- var originalEvent = event.originalEvent || event;
- var touches = (originalEvent.changedTouches && originalEvent.changedTouches.length) ?
- originalEvent.changedTouches :
- ((originalEvent.touches && originalEvent.touches.length) ? originalEvent.touches : [originalEvent]);
- var e = touches[0];
- var x = e.clientX;
- var y = e.clientY;
- var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2));
-
- if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) {
- // Call preventGhostClick so the clickbuster will catch the corresponding click.
- preventGhostClick(x, y);
-
- // Blur the focused element (the button, probably) before firing the callback.
- // This doesn't work perfectly on Android Chrome, but seems to work elsewhere.
- // I couldn't get anything to work reliably on Android Chrome.
- if (tapElement) {
- tapElement.blur();
- }
-
- if (!angular.isDefined(attr.disabled) || attr.disabled === false) {
- element.triggerHandler('click', [event]);
- }
- }
-
- resetState();
- });
-
- // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
- // something else nearby.
- element.onclick = function(event) { };
-
- // Actual click handler.
- // There are three different kinds of clicks, only two of which reach this point.
- // - On desktop browsers without touch events, their clicks will always come here.
- // - On mobile browsers, the simulated "fast" click will call this.
- // - But the browser's follow-up slow click will be "busted" before it reaches this handler.
- // Therefore it's safe to use this directive on both mobile and desktop.
- element.on('click', function(event, touchend) {
- scope.$apply(function() {
- clickHandler(scope, {$event: (touchend || event)});
- });
- });
-
- element.on('mousedown', function(event) {
- element.addClass(ACTIVE_CLASS_NAME);
- });
-
- element.on('mousemove mouseup', function(event) {
- element.removeClass(ACTIVE_CLASS_NAME);
- });
-
- };
-}];
-
/* global ngTouch: false */
/**
* @ngdoc directive
* @name ngSwipeLeft
*
+ * @deprecated
+ * sinceVersion="1.7.0"
+ *
+ * See the {@link ngTouch module} documentation for more information.
+ *
* @description
* Specify custom behavior when an element is swiped to the left on a touchscreen device.
* A leftward swipe is a quick, right-to-left slide of the finger.
@@ -619,7 +247,7 @@ var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
* upon left swipe. (Event object is available as `$event`)
*
* @example
- <example module="ngSwipeLeftExample" deps="angular-touch.js">
+ <example module="ngSwipeLeftExample" deps="angular-touch.js" name="ng-swipe-left">
<file name="index.html">
<div ng-show="!showActions" ng-swipe-left="showActions = true">
Some list content, like an email in the inbox
@@ -639,6 +267,11 @@ var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
* @ngdoc directive
* @name ngSwipeRight
*
+ * @deprecated
+ * sinceVersion="1.7.0"
+ *
+ * See the {@link ngTouch module} documentation for more information.
+ *
* @description
* Specify custom behavior when an element is swiped to the right on a touchscreen device.
* A rightward swipe is a quick, left-to-right slide of the finger.
@@ -652,7 +285,7 @@ var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
* upon right swipe. (Event object is available as `$event`)
*
* @example
- <example module="ngSwipeRightExample" deps="angular-touch.js">
+ <example module="ngSwipeRightExample" deps="angular-touch.js" name="ng-swipe-right">
<file name="index.html">
<div ng-show="!showActions" ng-swipe-left="showActions = true">
Some list content, like an email in the inbox
diff --git a/xstatic/pkg/angular/data/angular.js b/xstatic/pkg/angular/data/angular.js
index 54f6558..f9edf28 100644
--- a/xstatic/pkg/angular/data/angular.js
+++ b/xstatic/pkg/angular/data/angular.js
@@ -1,15 +1,75 @@
/**
- * @license AngularJS v1.5.8
- * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.8.2
+ * (c) 2010-2020 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window) {'use strict';
+/* exported
+ minErrConfig,
+ errorHandlingConfig,
+ isValidObjectMaxDepth
+*/
+
+var minErrConfig = {
+ objectMaxDepth: 5,
+ urlErrorParamsEnabled: true
+};
+
+/**
+ * @ngdoc function
+ * @name angular.errorHandlingConfig
+ * @module ng
+ * @kind function
+ *
+ * @description
+ * Configure several aspects of error handling in AngularJS if used as a setter or return the
+ * current configuration if used as a getter. The following options are supported:
+ *
+ * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages.
+ *
+ * Omitted or undefined options will leave the corresponding configuration values unchanged.
+ *
+ * @param {Object=} config - The configuration object. May only contain the options that need to be
+ * updated. Supported keys:
+ *
+ * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
+ * non-positive or non-numeric value, removes the max depth limit.
+ * Default: 5
+ *
+ * * `urlErrorParamsEnabled` **{Boolean}** - Specifies whether the generated error url will
+ * contain the parameters of the thrown error. Disabling the parameters can be useful if the
+ * generated error url is very long.
+ *
+ * Default: true. When used without argument, it returns the current value.
+ */
+function errorHandlingConfig(config) {
+ if (isObject(config)) {
+ if (isDefined(config.objectMaxDepth)) {
+ minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
+ }
+ if (isDefined(config.urlErrorParamsEnabled) && isBoolean(config.urlErrorParamsEnabled)) {
+ minErrConfig.urlErrorParamsEnabled = config.urlErrorParamsEnabled;
+ }
+ } else {
+ return minErrConfig;
+ }
+}
+
+/**
+ * @private
+ * @param {Number} maxDepth
+ * @return {boolean}
+ */
+function isValidObjectMaxDepth(maxDepth) {
+ return isNumber(maxDepth) && maxDepth > 0;
+}
+
/**
* @description
*
* This object provides a utility for producing rich Error messages within
- * Angular. It can be called as follows:
+ * AngularJS. It can be called as follows:
*
* var exampleMinErr = minErr('example');
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
@@ -26,7 +86,7 @@
* Since data will be parsed statically during a build step, some restrictions
* are applied with respect to how minErr instances are created and called.
* Instances should have names of the form namespaceMinErr for a minErr created
- * using minErr('namespace') . Error codes, namespaces and template strings
+ * using minErr('namespace'). Error codes, namespaces and template strings
* should all be static strings, not variables or general expressions.
*
* @param {string} module The namespace to use for the new minErr instance.
@@ -37,131 +97,148 @@
function minErr(module, ErrorConstructor) {
ErrorConstructor = ErrorConstructor || Error;
- return function() {
- var SKIP_INDEXES = 2;
- var templateArgs = arguments,
- code = templateArgs[0],
+ var url = 'https://errors.angularjs.org/"1.8.2"/';
+ var regex = url.replace('.', '\\.') + '[\\s\\S]*';
+ var errRegExp = new RegExp(regex, 'g');
+
+ return function() {
+ var code = arguments[0],
+ template = arguments[1],
message = '[' + (module ? module + ':' : '') + code + '] ',
- template = templateArgs[1],
+ templateArgs = sliceArgs(arguments, 2).map(function(arg) {
+ return toDebugString(arg, minErrConfig.objectMaxDepth);
+ }),
paramPrefix, i;
+ // A minErr message has two parts: the message itself and the url that contains the
+ // encoded message.
+ // The message's parameters can contain other error messages which also include error urls.
+ // To prevent the messages from getting too long, we strip the error urls from the parameters.
+
message += template.replace(/\{\d+\}/g, function(match) {
- var index = +match.slice(1, -1),
- shiftedIndex = index + SKIP_INDEXES;
+ var index = +match.slice(1, -1);
- if (shiftedIndex < templateArgs.length) {
- return toDebugString(templateArgs[shiftedIndex]);
+ if (index < templateArgs.length) {
+ return templateArgs[index].replace(errRegExp, '');
}
return match;
});
- message += '\nhttp://errors.angularjs.org/1.5.8/' +
- (module ? module + '/' : '') + code;
+ message += '\n' + url + (module ? module + '/' : '') + code;
- for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
- message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
- encodeURIComponent(toDebugString(templateArgs[i]));
+ if (minErrConfig.urlErrorParamsEnabled) {
+ for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
+ message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
+ }
}
return new ErrorConstructor(message);
};
}
-/* We need to tell jshint what variables are being exported */
-/* global angular: true,
- msie: true,
- jqLite: true,
- jQuery: true,
- slice: true,
- splice: true,
- push: true,
- toString: true,
- ngMinErr: true,
- angularModule: true,
- uid: true,
- REGEX_STRING_REGEXP: true,
- VALIDITY_STATE_PROPERTY: true,
-
- lowercase: true,
- uppercase: true,
- manualLowercase: true,
- manualUppercase: true,
- nodeName_: true,
- isArrayLike: true,
- forEach: true,
- forEachSorted: true,
- reverseParams: true,
- nextUid: true,
- setHashKey: true,
- extend: true,
- toInt: true,
- inherit: true,
- merge: true,
- noop: true,
- identity: true,
- valueFn: true,
- isUndefined: true,
- isDefined: true,
- isObject: true,
- isBlankObject: true,
- isString: true,
- isNumber: true,
- isDate: true,
- isArray: true,
- isFunction: true,
- isRegExp: true,
- isWindow: true,
- isScope: true,
- isFile: true,
- isFormData: true,
- isBlob: true,
- isBoolean: true,
- isPromiseLike: true,
- trim: true,
- escapeForRegexp: true,
- isElement: true,
- makeMap: true,
- includes: true,
- arrayRemove: true,
- copy: true,
- equals: true,
- csp: true,
- jq: true,
- concat: true,
- sliceArgs: true,
- bind: true,
- toJsonReplacer: true,
- toJson: true,
- fromJson: true,
- convertTimezoneToLocal: true,
- timezoneToOffset: true,
- startingTag: true,
- tryDecodeURIComponent: true,
- parseKeyValue: true,
- toKeyValue: true,
- encodeUriSegment: true,
- encodeUriQuery: true,
- angularInit: true,
- bootstrap: true,
- getTestability: true,
- snake_case: true,
- bindJQuery: true,
- assertArg: true,
- assertArgFn: true,
- assertNotHasOwnProperty: true,
- getter: true,
- getBlockNodes: true,
- hasOwnProperty: true,
- createMap: true,
-
- NODE_TYPE_ELEMENT: true,
- NODE_TYPE_ATTRIBUTE: true,
- NODE_TYPE_TEXT: true,
- NODE_TYPE_COMMENT: true,
- NODE_TYPE_DOCUMENT: true,
- NODE_TYPE_DOCUMENT_FRAGMENT: true,
+/* We need to tell ESLint what variables are being exported */
+/* exported
+ angular,
+ msie,
+ jqLite,
+ jQuery,
+ slice,
+ splice,
+ push,
+ toString,
+ minErrConfig,
+ errorHandlingConfig,
+ isValidObjectMaxDepth,
+ ngMinErr,
+ angularModule,
+ uid,
+ REGEX_STRING_REGEXP,
+ VALIDITY_STATE_PROPERTY,
+
+ lowercase,
+ uppercase,
+ nodeName_,
+ isArrayLike,
+ forEach,
+ forEachSorted,
+ reverseParams,
+ nextUid,
+ setHashKey,
+ extend,
+ toInt,
+ inherit,
+ merge,
+ noop,
+ identity,
+ valueFn,
+ isUndefined,
+ isDefined,
+ isObject,
+ isBlankObject,
+ isString,
+ isNumber,
+ isNumberNaN,
+ isDate,
+ isError,
+ isArray,
+ isFunction,
+ isRegExp,
+ isWindow,
+ isScope,
+ isFile,
+ isFormData,
+ isBlob,
+ isBoolean,
+ isPromiseLike,
+ trim,
+ escapeForRegexp,
+ isElement,
+ makeMap,
+ includes,
+ arrayRemove,
+ copy,
+ simpleCompare,
+ equals,
+ csp,
+ jq,
+ concat,
+ sliceArgs,
+ bind,
+ toJsonReplacer,
+ toJson,
+ fromJson,
+ convertTimezoneToLocal,
+ timezoneToOffset,
+ addDateMinutes,
+ startingTag,
+ tryDecodeURIComponent,
+ parseKeyValue,
+ toKeyValue,
+ encodeUriSegment,
+ encodeUriQuery,
+ angularInit,
+ bootstrap,
+ getTestability,
+ snake_case,
+ bindJQuery,
+ assertArg,
+ assertArgFn,
+ assertNotHasOwnProperty,
+ getter,
+ getBlockNodes,
+ hasOwnProperty,
+ createMap,
+ stringify,
+ UNSAFE_restoreLegacyJqLiteXHTMLReplacement,
+
+ NODE_TYPE_ELEMENT,
+ NODE_TYPE_ATTRIBUTE,
+ NODE_TYPE_TEXT,
+ NODE_TYPE_COMMENT,
+ NODE_TYPE_DOCUMENT,
+ NODE_TYPE_DOCUMENT_FRAGMENT
*/
////////////////////////////////////
@@ -173,13 +250,11 @@ function minErr(module, ErrorConstructor) {
* @installation
* @description
*
- * # ng (core module)
* The ng module is loaded by default when an AngularJS application is started. The module itself
* contains the essential components for an AngularJS application to function. The table below
* lists a high level breakdown of each of the services/factories, filters, directives and testing
* components available within this core module.
*
- * <div doc-module-components="ng"></div>
*/
var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
@@ -188,33 +263,26 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
// This is used so that it's possible for internal tests to create mock ValidityStates.
var VALIDITY_STATE_PROPERTY = 'validity';
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
+/**
+ * @private
+ *
+ * @description Converts the specified string to lowercase.
+ * @param {string} string String to be converted to lowercase.
+ * @returns {string} Lowercased string.
+ */
var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
-var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
-
-
-var manualLowercase = function(s) {
- /* jshint bitwise: false */
- return isString(s)
- ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);})
- : s;
-};
-var manualUppercase = function(s) {
- /* jshint bitwise: false */
- return isString(s)
- ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);})
- : s;
-};
-
-// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish
-// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods
-// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387
-if ('i' !== 'I'.toLowerCase()) {
- lowercase = manualLowercase;
- uppercase = manualUppercase;
-}
+/**
+ * @private
+ *
+ * @description Converts the specified string to uppercase.
+ * @param {string} string String to be converted to uppercase.
+ * @returns {string} Uppercased string.
+ */
+var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
var
@@ -233,6 +301,7 @@ var
angularModule,
uid = 0;
+// Support: IE 9-11 only
/**
* documentMode is an IE-only property
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
@@ -259,12 +328,11 @@ function isArrayLike(obj) {
// Support: iOS 8.2 (not reproducible in simulator)
// "length" in obj used to prevent JIT error (gh-11508)
- var length = "length" in Object(obj) && obj.length;
+ var length = 'length' in Object(obj) && obj.length;
// NodeList objects (with `item` method) and
// other objects with suitable length characteristics are array-like
- return isNumber(length) &&
- (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item == 'function');
+ return isNumber(length) && (length >= 0 && (length - 1) in obj || typeof obj.item === 'function');
}
@@ -308,9 +376,7 @@ function forEach(obj, iterator, context) {
if (obj) {
if (isFunction(obj)) {
for (key in obj) {
- // Need to check if hasOwnProperty exists,
- // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function
- if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) {
+ if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key, obj);
}
}
@@ -415,8 +481,10 @@ function baseExtend(dst, objs, deep) {
} else if (isElement(src)) {
dst[key] = src.clone();
} else {
- if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
- baseExtend(dst[key], [src], true);
+ if (key !== '__proto__') {
+ if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
+ baseExtend(dst[key], [src], true);
+ }
}
} else {
dst[key] = src;
@@ -465,6 +533,22 @@ function extend(dst) {
* Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source
* objects, performing a deep copy.
*
+* @deprecated
+* sinceVersion="1.6.5"
+* This function is deprecated, but will not be removed in the 1.x lifecycle.
+* There are edge cases (see {@link angular.merge#known-issues known issues}) that are not
+* supported by this function. We suggest using another, similar library for all-purpose merging,
+* such as [lodash's merge()](https://lodash.com/docs/4.17.4#merge).
+*
+* @knownIssue
+* This is a list of (known) object types that are not handled correctly by this function:
+* - [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob)
+* - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream)
+* - [`CanvasGradient`](https://developer.mozilla.org/docs/Web/API/CanvasGradient)
+* - AngularJS {@link $rootScope.Scope scopes};
+*
+* `angular.merge` also does not support merging objects with circular references.
+*
* @param {Object} dst Destination object.
* @param {...Object} src Source object(s).
* @returns {Object} Reference to `dst`.
@@ -479,6 +563,11 @@ function toInt(str) {
return parseInt(str, 10);
}
+var isNumberNaN = Number.isNaN || function isNumberNaN(num) {
+ // eslint-disable-next-line no-self-compare
+ return num !== num;
+};
+
function inherit(parent, extra) {
return extend(Object.create(parent), extra);
@@ -667,7 +756,27 @@ function isDate(value) {
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is an `Array`.
*/
-var isArray = Array.isArray;
+function isArray(arr) {
+ return Array.isArray(arr) || arr instanceof Array;
+}
+
+/**
+ * @description
+ * Determines if a reference is an `Error`.
+ * Loosely based on https://www.npmjs.com/package/iserror
+ *
+ * @param {*} value Reference to check.
+ * @returns {boolean} True if `value` is an `Error`.
+ */
+function isError(value) {
+ var tag = toString.call(value);
+ switch (tag) {
+ case '[object Error]': return true;
+ case '[object Exception]': return true;
+ case '[object DOMException]': return true;
+ default: return value instanceof Error;
+ }
+}
/**
* @ngdoc function
@@ -738,7 +847,7 @@ function isPromiseLike(obj) {
}
-var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/;
+var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/;
function isTypedArray(value) {
return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
}
@@ -756,8 +865,10 @@ var trim = function(value) {
// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021
// Prereq: s is a string.
var escapeForRegexp = function(s) {
- return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
- replace(/\x08/g, '\\x08');
+ return s
+ .replace(/([-()[\]{}+?*.$^|,:#<!\\])/g, '\\$1')
+ // eslint-disable-next-line no-control-regex
+ .replace(/\x08/g, '\\x08');
};
@@ -797,7 +908,7 @@ function nodeName_(element) {
}
function includes(array, obj) {
- return Array.prototype.indexOf.call(array, obj) != -1;
+ return Array.prototype.indexOf.call(array, obj) !== -1;
}
function arrayRemove(array, value) {
@@ -815,7 +926,9 @@ function arrayRemove(array, value) {
* @kind function
*
* @description
- * Creates a deep copy of `source`, which should be an object or an array.
+ * Creates a deep copy of `source`, which should be an object or an array. This functions is used
+ * internally, mostly in the change-detection code. It is not intended as an all-purpose copy
+ * function, and has several limitations (see below).
*
* * If no destination is supplied, a copy of the object or array is created.
* * If a destination is provided, all of its elements (for arrays) or properties (for objects)
@@ -824,19 +937,39 @@ function arrayRemove(array, value) {
* * If `source` is identical to `destination` an exception will be thrown.
*
* <br />
+ *
* <div class="alert alert-warning">
* Only enumerable properties are taken into account. Non-enumerable properties (both on `source`
* and on `destination`) will be ignored.
* </div>
*
- * @param {*} source The source that will be used to make a copy.
- * Can be any type, including primitives, `null`, and `undefined`.
- * @param {(Object|Array)=} destination Destination into which the source is copied. If
- * provided, must be of the same type as `source`.
+ * <div class="alert alert-warning">
+ * `angular.copy` does not check if destination and source are of the same type. It's the
+ * developer's responsibility to make sure they are compatible.
+ * </div>
+ *
+ * @knownIssue
+ * This is a non-exhaustive list of object types / features that are not handled correctly by
+ * `angular.copy`. Note that since this functions is used by the change detection code, this
+ * means binding or watching objects of these types (or that include these types) might not work
+ * correctly.
+ * - [`File`](https://developer.mozilla.org/docs/Web/API/File)
+ * - [`Map`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)
+ * - [`ImageData`](https://developer.mozilla.org/docs/Web/API/ImageData)
+ * - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream)
+ * - [`Set`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set)
+ * - [`WeakMap`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)
+ * - [`getter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)/
+ * [`setter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set)
+ *
+ * @param {*} source The source that will be used to make a copy. Can be any type, including
+ * primitives, `null`, and `undefined`.
+ * @param {(Object|Array)=} destination Destination into which the source is copied. If provided,
+ * must be of the same type as `source`.
* @returns {*} The copy or updated `destination`, if `destination` was specified.
*
* @example
- <example module="copyExample">
+ <example module="copyExample" name="angular-copy">
<file name="index.html">
<div ng-controller="ExampleController">
<form novalidate class="simple-form">
@@ -848,7 +981,7 @@ function arrayRemove(array, value) {
<button ng-click="update(user)">SAVE</button>
</form>
<pre>form = {{user | json}}</pre>
- <pre>master = {{master | json}}</pre>
+ <pre>leader = {{leader | json}}</pre>
</div>
</file>
<file name="script.js">
@@ -856,16 +989,16 @@ function arrayRemove(array, value) {
angular.
module('copyExample', []).
controller('ExampleController', ['$scope', function($scope) {
- $scope.master = {};
+ $scope.leader = {};
$scope.reset = function() {
// Example with 1 argument
- $scope.user = angular.copy($scope.master);
+ $scope.user = angular.copy($scope.leader);
};
$scope.update = function(user) {
// Example with 2 arguments
- angular.copy(user, $scope.master);
+ angular.copy(user, $scope.leader);
};
$scope.reset();
@@ -873,16 +1006,17 @@ function arrayRemove(array, value) {
</file>
</example>
*/
-function copy(source, destination) {
+function copy(source, destination, maxDepth) {
var stackSource = [];
var stackDest = [];
+ maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN;
if (destination) {
if (isTypedArray(destination) || isArrayBuffer(destination)) {
- throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
+ throw ngMinErr('cpta', 'Can\'t copy! TypedArray destination cannot be mutated.');
}
if (source === destination) {
- throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
+ throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.');
}
// Empty the destination object
@@ -898,35 +1032,39 @@ function copy(source, destination) {
stackSource.push(source);
stackDest.push(destination);
- return copyRecurse(source, destination);
+ return copyRecurse(source, destination, maxDepth);
}
- return copyElement(source);
+ return copyElement(source, maxDepth);
- function copyRecurse(source, destination) {
+ function copyRecurse(source, destination, maxDepth) {
+ maxDepth--;
+ if (maxDepth < 0) {
+ return '...';
+ }
var h = destination.$$hashKey;
var key;
if (isArray(source)) {
for (var i = 0, ii = source.length; i < ii; i++) {
- destination.push(copyElement(source[i]));
+ destination.push(copyElement(source[i], maxDepth));
}
} else if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
}
} else {
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
}
}
@@ -934,7 +1072,7 @@ function copy(source, destination) {
return destination;
}
- function copyElement(source) {
+ function copyElement(source, maxDepth) {
// Simple values
if (!isObject(source)) {
return source;
@@ -948,7 +1086,7 @@ function copy(source, destination) {
if (isWindow(source) || isScope(source)) {
throw ngMinErr('cpws',
- "Can't copy! Making copies of Window or Scope instances is not supported.");
+ 'Can\'t copy! Making copies of Window or Scope instances is not supported.');
}
var needsRecurse = false;
@@ -963,7 +1101,7 @@ function copy(source, destination) {
stackDest.push(destination);
return needsRecurse
- ? copyRecurse(source, destination)
+ ? copyRecurse(source, destination, maxDepth)
: destination;
}
@@ -981,10 +1119,13 @@ function copy(source, destination) {
return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length);
case '[object ArrayBuffer]':
- //Support: IE10
+ // Support: IE10
if (!source.slice) {
+ // If we're in this case we know the environment supports ArrayBuffer
+ /* eslint-disable no-undef */
var copied = new ArrayBuffer(source.byteLength);
new Uint8Array(copied).set(new Uint8Array(source));
+ /* eslint-enable */
return copied;
}
return source.slice(0);
@@ -996,7 +1137,7 @@ function copy(source, destination) {
return new source.constructor(source.valueOf());
case '[object RegExp]':
- var re = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
+ var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]);
re.lastIndex = source.lastIndex;
return re;
@@ -1011,6 +1152,10 @@ function copy(source, destination) {
}
+// eslint-disable-next-line no-self-compare
+function simpleCompare(a, b) { return a === b || (a !== a && b !== b); }
+
+
/**
* @ngdoc function
* @name angular.equals
@@ -1067,7 +1212,6 @@ function copy(source, destination) {
angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) {
$scope.user1 = {};
$scope.user2 = {};
- $scope.result;
$scope.compare = function() {
$scope.result = angular.equals($scope.user1, $scope.user2);
};
@@ -1078,12 +1222,13 @@ function copy(source, destination) {
function equals(o1, o2) {
if (o1 === o2) return true;
if (o1 === null || o2 === null) return false;
+ // eslint-disable-next-line no-self-compare
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
- if (t1 == t2 && t1 == 'object') {
+ if (t1 === t2 && t1 === 'object') {
if (isArray(o1)) {
if (!isArray(o2)) return false;
- if ((length = o1.length) == o2.length) {
+ if ((length = o1.length) === o2.length) {
for (key = 0; key < length; key++) {
if (!equals(o1[key], o2[key])) return false;
}
@@ -1091,10 +1236,10 @@ function equals(o1, o2) {
}
} else if (isDate(o1)) {
if (!isDate(o2)) return false;
- return equals(o1.getTime(), o2.getTime());
+ return simpleCompare(o1.getTime(), o2.getTime());
} else if (isRegExp(o1)) {
if (!isRegExp(o2)) return false;
- return o1.toString() == o2.toString();
+ return o1.toString() === o2.toString();
} else {
if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
@@ -1142,9 +1287,8 @@ var csp = function() {
function noUnsafeEval() {
try {
- /* jshint -W031, -W054 */
+ // eslint-disable-next-line no-new, no-new-func
new Function('');
- /* jshint +W031, +W054 */
return false;
} catch (e) {
return true;
@@ -1165,7 +1309,7 @@ var csp = function() {
* used to force either jqLite by leaving ng-jq blank or setting the name of
* the jquery variable under window (eg. jQuery).
*
- * Since angular looks for this directive when it is loaded (doesn't wait for the
+ * Since AngularJS looks for this directive when it is loaded (doesn't wait for the
* DOMContentLoaded event), it must be placed on an element that comes before the script
* which loads angular. Also, only the first instance of `ng-jq` will be used and all
* others ignored.
@@ -1196,7 +1340,8 @@ var jq = function() {
var i, ii = ngAttrPrefixes.length, prefix, name;
for (i = 0; i < ii; ++i) {
prefix = ngAttrPrefixes[i];
- if (el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
+ el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]');
+ if (el) {
name = el.getAttribute(prefix + 'jq');
break;
}
@@ -1214,7 +1359,6 @@ function sliceArgs(args, startIndex) {
}
-/* jshint -W101 */
/**
* @ngdoc function
* @name angular.bind
@@ -1232,7 +1376,6 @@ function sliceArgs(args, startIndex) {
* @param {...*} args Optional arguments to be prebound to the `fn` function call.
* @returns {function()} Function that wraps the `fn` with all the specified bindings.
*/
-/* jshint +W101 */
function bind(self, fn) {
var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : [];
if (isFunction(fn) && !(fn instanceof RegExp)) {
@@ -1279,9 +1422,9 @@ function toJsonReplacer(key, value) {
*
* @description
* Serializes input into a JSON-formatted string. Properties with leading $$ characters will be
- * stripped since angular uses this notation internally.
+ * stripped since AngularJS uses this notation internally.
*
- * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON.
+ * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON.
* @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace.
* If set to an integer, the JSON output will contain that many spaces per indentation.
* @returns {string|undefined} JSON-ified string representing `obj`.
@@ -1337,10 +1480,11 @@ function fromJson(json) {
var ALL_COLONS = /:/g;
function timezoneToOffset(timezone, fallback) {
+ // Support: IE 9-11 only, Edge 13-15+
// IE/Edge do not "understand" colon (`:`) in timezone
timezone = timezone.replace(ALL_COLONS, '');
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
- return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
+ return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
@@ -1363,18 +1507,13 @@ function convertTimezoneToLocal(date, timezone, reverse) {
* @returns {string} Returns the string representation of the element.
*/
function startingTag(element) {
- element = jqLite(element).clone();
- 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) {}
- var elemHtml = jqLite('<div>').append(element).html();
+ element = jqLite(element).clone().empty();
+ var elemHtml = jqLite('<div></div>').append(element).html();
try {
return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) :
elemHtml.
match(/^(<[^>]+>)/)[1].
- replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);});
+ replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);});
} catch (e) {
return lowercase(elemHtml);
}
@@ -1407,7 +1546,7 @@ function tryDecodeURIComponent(value) {
*/
function parseKeyValue(/**string*/keyValue) {
var obj = {};
- forEach((keyValue || "").split('&'), function(keyValue) {
+ forEach((keyValue || '').split('&'), function(keyValue) {
var splitPoint, key, val;
if (keyValue) {
key = keyValue = keyValue.replace(/\+/g,'%20');
@@ -1472,7 +1611,7 @@ function encodeUriSegment(val) {
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
* encoded per http://tools.ietf.org/html/rfc3986:
- * query = *( pchar / "/" / "?" )
+ * query = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
@@ -1502,6 +1641,58 @@ function getNgAttribute(element, ngAttr) {
return null;
}
+function allowAutoBootstrap(document) {
+ var script = document.currentScript;
+
+ if (!script) {
+ // Support: IE 9-11 only
+ // IE does not have `document.currentScript`
+ return true;
+ }
+
+ // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack
+ if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) {
+ return false;
+ }
+
+ var attributes = script.attributes;
+ var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')];
+
+ return srcs.every(function(src) {
+ if (!src) {
+ return true;
+ }
+ if (!src.value) {
+ return false;
+ }
+
+ var link = document.createElement('a');
+ link.href = src.value;
+
+ if (document.location.origin === link.origin) {
+ // Same-origin resources are always allowed, even for banned URL schemes.
+ return true;
+ }
+ // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web.
+ // This is to prevent angular.js bundled with browser extensions from being used to bypass the
+ // content security policy in web pages and other browser extensions.
+ switch (link.protocol) {
+ case 'http:':
+ case 'https:':
+ case 'ftp:':
+ case 'blob:':
+ case 'file:':
+ case 'data:':
+ return true;
+ default:
+ return false;
+ }
+ });
+}
+
+// Cached as it has to run during loading so that document.currentScript is available.
+var isAutoBootstrapAllowed = allowAutoBootstrap(window.document);
+
/**
* @ngdoc directive
* @name ngApp
@@ -1543,9 +1734,13 @@ function getNgAttribute(element, ngAttr) {
* document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}`
* would not be resolved to `3`.
*
+ * @example
+ *
+ * ### Simple Usage
+ *
* `ngApp` is the easiest, and most common way to bootstrap an application.
*
- <example module="ngAppDemo">
+ <example module="ngAppDemo" name="ng-app">
<file name="index.html">
<div ng-controller="ngAppDemoController">
I can add: {{a}} + {{b}} = {{ a+b }}
@@ -1559,9 +1754,13 @@ function getNgAttribute(element, ngAttr) {
</file>
</example>
*
+ * @example
+ *
+ * ### With `ngStrictDi`
+ *
* Using `ngStrictDi`, you would see something like this:
*
- <example ng-app-included="true">
+ <example ng-app-included="true" name="strict-di">
<file name="index.html">
<div ng-app="ngAppStrictDemo" ng-strict-di>
<div ng-controller="GoodController1">
@@ -1610,7 +1809,7 @@ function getNgAttribute(element, ngAttr) {
}])
.controller('GoodController2', GoodController2);
function GoodController2($scope) {
- $scope.name = "World";
+ $scope.name = 'World';
}
GoodController2.$inject = ['$scope'];
</file>
@@ -1660,7 +1859,12 @@ function angularInit(element, bootstrap) {
}
});
if (appElement) {
- config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
+ if (!isAutoBootstrapAllowed) {
+ window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
+ 'an extension, document.location.href does not match.');
+ return;
+ }
+ config.strictDi = getNgAttribute(appElement, 'strict-di') !== null;
bootstrap(appElement, module ? [module] : [], config);
}
}
@@ -1670,14 +1874,14 @@ function angularInit(element, bootstrap) {
* @name angular.bootstrap
* @module ng
* @description
- * Use this function to manually start up angular application.
+ * Use this function to manually start up AngularJS application.
*
* For more information, see the {@link guide/bootstrap Bootstrap guide}.
*
- * Angular will detect if it has been loaded into the browser more than once and only allow the
+ * AngularJS will detect if it has been loaded into the browser more than once and only allow the
* first loaded script to be bootstrapped and will report a warning to the browser console for
* each of the subsequent scripts. This prevents strange results in applications, where otherwise
- * multiple instances of Angular try to work on the DOM.
+ * multiple instances of AngularJS try to work on the DOM.
*
* <div class="alert alert-warning">
* **Note:** Protractor based end-to-end tests cannot use this function to bootstrap manually.
@@ -1711,7 +1915,7 @@ function angularInit(element, bootstrap) {
* </html>
* ```
*
- * @param {DOMElement} element DOM element which is the root of angular application.
+ * @param {DOMElement} element DOM element which is the root of AngularJS application.
* @param {Array<String|Function|Array>=} modules an array of modules to load into the application.
* Each item in the array should be the name of a predefined module or a (DI annotated)
* function that will be invoked by the injector as a `config` block.
@@ -1738,7 +1942,7 @@ function bootstrap(element, modules, config) {
// Encode angle brackets to prevent input from being sanitized to empty string #8683.
throw ngMinErr(
'btstrpd',
- "App already bootstrapped with this element '{0}'",
+ 'App already bootstrapped with this element \'{0}\'',
tag.replace(/</,'&lt;').replace(/>/,'&gt;'));
}
@@ -1811,9 +2015,9 @@ function reloadWithDebugInfo() {
* @name angular.getTestability
* @module ng
* @description
- * Get the testability service for the instance of Angular on the given
+ * Get the testability service for the instance of AngularJS on the given
* element.
- * @param {DOMElement} element DOM element which is the root of angular application.
+ * @param {DOMElement} element DOM element which is the root of AngularJS application.
*/
function getTestability(rootElement) {
var injector = angular.element(rootElement).injector();
@@ -1847,37 +2051,37 @@ function bindJQuery() {
window[jqName]; // use jQuery specified by `ngJq`
// Use jQuery if it exists with proper functionality, otherwise default to us.
- // Angular 1.2+ requires jQuery 1.7+ for on()/off() support.
- // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older
+ // AngularJS 1.2+ requires jQuery 1.7+ for on()/off() support.
+ // AngularJS 1.3+ technically requires at least jQuery 2.1+ but it may work with older
// versions. It will not work for sure with jQuery <1.7, though.
if (jQuery && jQuery.fn.on) {
jqLite = jQuery;
extend(jQuery.fn, {
scope: JQLitePrototype.scope,
isolateScope: JQLitePrototype.isolateScope,
- controller: JQLitePrototype.controller,
+ controller: /** @type {?} */ (JQLitePrototype).controller,
injector: JQLitePrototype.injector,
inheritedData: JQLitePrototype.inheritedData
});
-
- // All nodes removed from the DOM via various jQuery APIs like .remove()
- // are passed through jQuery.cleanData. Monkey-patch this method to fire
- // the $destroy event on all removed nodes.
- originalCleanData = jQuery.cleanData;
- jQuery.cleanData = function(elems) {
- var events;
- for (var i = 0, elem; (elem = elems[i]) != null; i++) {
- events = jQuery._data(elem, "events");
- if (events && events.$destroy) {
- jQuery(elem).triggerHandler('$destroy');
- }
- }
- originalCleanData(elems);
- };
} else {
jqLite = JQLite;
}
+ // All nodes removed from the DOM via various jqLite/jQuery APIs like .remove()
+ // are passed through jqLite/jQuery.cleanData. Monkey-patch this method to fire
+ // the $destroy event on all removed nodes.
+ originalCleanData = jqLite.cleanData;
+ jqLite.cleanData = function(elems) {
+ var events;
+ for (var i = 0, elem; (elem = elems[i]) != null; i++) {
+ events = (jqLite._data(elem) || {}).events;
+ if (events && events.$destroy) {
+ jqLite(elem).triggerHandler('$destroy');
+ }
+ }
+ originalCleanData(elems);
+ };
+
angular.element = jqLite;
// Prevent double-proxying.
@@ -1885,11 +2089,31 @@ function bindJQuery() {
}
/**
+ * @ngdoc function
+ * @name angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement
+ * @module ng
+ * @kind function
+ *
+ * @description
+ * Restores the pre-1.8 behavior of jqLite that turns XHTML-like strings like
+ * `<div /><span />` to `<div></div><span></span>` instead of `<div><span></span></div>`.
+ * The new behavior is a security fix. Thus, if you need to call this function, please try to adjust
+ * your code for this change and remove your use of this function as soon as possible.
+
+ * Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read the
+ * [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details
+ * about the workarounds.
+ */
+function UNSAFE_restoreLegacyJqLiteXHTMLReplacement() {
+ JQLite.legacyXHTMLReplacement = true;
+}
+
+/**
* throw error if the argument is falsy.
*/
function assertArg(arg, name, reason) {
if (!arg) {
- throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
+ throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required'));
}
return arg;
}
@@ -1911,7 +2135,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) {
*/
function assertNotHasOwnProperty(name, context) {
if (name === 'hasOwnProperty') {
- throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context);
+ throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
}
}
@@ -1981,6 +2205,27 @@ function createMap() {
return Object.create(null);
}
+function stringify(value) {
+ if (value == null) { // null || undefined
+ return '';
+ }
+ switch (typeof value) {
+ case 'string':
+ break;
+ case 'number':
+ value = '' + value;
+ break;
+ default:
+ if (hasCustomToString(value) && !isArray(value) && !isDate(value)) {
+ value = value.toString();
+ } else {
+ value = toJson(value);
+ }
+ }
+
+ return value;
+}
+
var NODE_TYPE_ELEMENT = 1;
var NODE_TYPE_ATTRIBUTE = 2;
var NODE_TYPE_TEXT = 3;
@@ -1994,7 +2239,7 @@ var NODE_TYPE_DOCUMENT_FRAGMENT = 11;
* @module ng
* @description
*
- * Interface for configuring angular {@link angular.module modules}.
+ * Interface for configuring AngularJS {@link angular.module modules}.
*/
function setupModuleLoader(window) {
@@ -2021,9 +2266,9 @@ function setupModuleLoader(window) {
* @module ng
* @description
*
- * The `angular.module` is a global place for creating, registering and retrieving Angular
+ * The `angular.module` is a global place for creating, registering and retrieving AngularJS
* modules.
- * All modules (angular core or 3rd party) that should be available to an application must be
+ * All modules (AngularJS core or 3rd party) that should be available to an application must be
* registered using this mechanism.
*
* Passing one argument retrieves an existing {@link angular.Module},
@@ -2067,6 +2312,9 @@ function setupModuleLoader(window) {
* @returns {angular.Module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
+
+ var info = {};
+
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
@@ -2079,9 +2327,9 @@ function setupModuleLoader(window) {
}
return ensure(modules, name, function() {
if (!requires) {
- throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
- "the module name or forgot to load it. If registering a module ensure that you " +
- "specify the dependencies as the second argument.", name);
+ throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' +
+ 'the module name or forgot to load it. If registering a module ensure that you ' +
+ 'specify the dependencies as the second argument.', name);
}
/** @type {!Array.<Array.<*>>} */
@@ -2103,6 +2351,45 @@ function setupModuleLoader(window) {
_runBlocks: runBlocks,
/**
+ * @ngdoc method
+ * @name angular.Module#info
+ * @module ng
+ *
+ * @param {Object=} info Information about the module
+ * @returns {Object|Module} The current info object for this module if called as a getter,
+ * or `this` if called as a setter.
+ *
+ * @description
+ * Read and write custom information about this module.
+ * For example you could put the version of the module in here.
+ *
+ * ```js
+ * angular.module('myModule', []).info({ version: '1.0.0' });
+ * ```
+ *
+ * The version could then be read back out by accessing the module elsewhere:
+ *
+ * ```
+ * var version = angular.module('myModule').info().version;
+ * ```
+ *
+ * You can also retrieve this information during runtime via the
+ * {@link $injector#modules `$injector.modules`} property:
+ *
+ * ```js
+ * var version = $injector.modules['myModule'].info().version;
+ * ```
+ */
+ info: function(value) {
+ if (isDefined(value)) {
+ if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value');
+ info = value;
+ return this;
+ }
+ return info;
+ },
+
+ /**
* @ngdoc property
* @name angular.Module#requires
* @module ng
@@ -2191,7 +2478,7 @@ function setupModuleLoader(window) {
* @description
* See {@link auto.$provide#decorator $provide.decorator()}.
*/
- decorator: invokeLaterAndSetModuleName('$provide', 'decorator'),
+ decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks),
/**
* @ngdoc method
@@ -2231,13 +2518,13 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#filter
* @module ng
- * @param {string} name Filter name - this must be a valid angular expression identifier
+ * @param {string} name Filter name - this must be a valid AngularJS expression identifier
* @param {Function} filterFactory Factory function for creating new instance of filter.
* @description
* See {@link ng.$filterProvider#register $filterProvider.register()}.
*
* <div class="alert alert-warning">
- * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
* Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
@@ -2274,7 +2561,8 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#component
* @module ng
- * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
+ * @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`),
+ * or an object map of components where the keys are the names and the values are the component definition objects.
* @param {Object} options Component definition object (a simplified
* {@link ng.$compile#directive-definition-object directive definition object})
*
@@ -2290,7 +2578,13 @@ function setupModuleLoader(window) {
* @param {Function} configFn Execute this function on module load. Useful for service
* configuration.
* @description
- * Use this method to register work which needs to be performed on module loading.
+ * Use this method to configure services by injecting their
+ * {@link angular.Module#provider `providers`}, e.g. for adding routes to the
+ * {@link ngRoute.$routeProvider $routeProvider}.
+ *
+ * Note that you can only inject {@link angular.Module#provider `providers`} and
+ * {@link angular.Module#constant `constants`} into this function.
+ *
* For more about how to configure services, see
* {@link providers#provider-recipe Provider Recipe}.
*/
@@ -2337,10 +2631,11 @@ function setupModuleLoader(window) {
* @param {string} method
* @returns {angular.Module}
*/
- function invokeLaterAndSetModuleName(provider, method) {
+ function invokeLaterAndSetModuleName(provider, method, queue) {
+ if (!queue) queue = invokeQueue;
return function(recipeName, factoryFunction) {
if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
- invokeQueue.push([provider, method, arguments]);
+ queue.push([provider, method, arguments]);
return moduleInstance;
};
}
@@ -2377,11 +2672,19 @@ function shallowCopy(src, dst) {
return dst || src;
}
-/* global toDebugString: true */
+/* exported toDebugString */
-function serializeObject(obj) {
+function serializeObject(obj, maxDepth) {
var seen = [];
+ // There is no direct way to stringify object until reaching a specific depth
+ // and a very deep object can cause a performance issue, so we copy the object
+ // based on this specific depth and then stringify it.
+ if (isValidObjectMaxDepth(maxDepth)) {
+ // This file is also included in `angular-loader`, so `copy()` might not always be available in
+ // the closure. Therefore, it is lazily retrieved as `angular.copy()` when needed.
+ obj = angular.copy(obj, null, maxDepth);
+ }
return JSON.stringify(obj, function(key, val) {
val = toJsonReplacer(key, val);
if (isObject(val)) {
@@ -2394,13 +2697,13 @@ function serializeObject(obj) {
});
}
-function toDebugString(obj) {
+function toDebugString(obj, maxDepth) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (isUndefined(obj)) {
return 'undefined';
} else if (typeof obj !== 'string') {
- return serializeObject(obj);
+ return serializeObject(obj, maxDepth);
}
return obj;
}
@@ -2412,11 +2715,10 @@ function toDebugString(obj) {
htmlAnchorDirective,
inputDirective,
- inputDirective,
+ hiddenInputBrowserCacheDirective,
formDirective,
scriptDirective,
selectDirective,
- styleDirective,
optionDirective,
ngBindDirective,
ngBindHtmlDirective,
@@ -2434,6 +2736,7 @@ function toDebugString(obj) {
ngInitDirective,
ngNonBindableDirective,
ngPluralizeDirective,
+ ngRefDirective,
ngRepeatDirective,
ngShowDirective,
ngStyleDirective,
@@ -2470,12 +2773,13 @@ function toDebugString(obj) {
$ControllerProvider,
$DateProvider,
$DocumentProvider,
+ $$IsDocumentHiddenProvider,
$ExceptionHandlerProvider,
$FilterProvider,
$$ForceReflowProvider,
$InterpolateProvider,
+ $$IntervalFactoryProvider,
$IntervalProvider,
- $$HashMapProvider,
$HttpProvider,
$HttpParamSerializerProvider,
$HttpParamSerializerJQLikeProvider,
@@ -2484,6 +2788,7 @@ function toDebugString(obj) {
$jsonpCallbacksProvider,
$LocationProvider,
$LogProvider,
+ $$MapProvider,
$ParseProvider,
$RootScopeProvider,
$QProvider,
@@ -2492,6 +2797,7 @@ function toDebugString(obj) {
$SceProvider,
$SceDelegateProvider,
$SnifferProvider,
+ $$TaskTrackerFactoryProvider,
$TemplateCacheProvider,
$TemplateRequestProvider,
$$TestabilityProvider,
@@ -2519,16 +2825,19 @@ function toDebugString(obj) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.5.8', // all of these placeholder strings will be replaced by grunt's
- major: 1, // package task
- minor: 5,
- dot: 8,
- codeName: 'arbitrary-fallbacks'
+ // These placeholder strings will be replaced by grunt's `build` task.
+ // They need to be double- or single-quoted.
+ full: '1.8.2',
+ major: '1',
+ minor: '8',
+ dot: '2',
+ codeName: 'meteoric-mining'
};
function publishExternalAPI(angular) {
extend(angular, {
+ 'errorHandlingConfig': errorHandlingConfig,
'bootstrap': bootstrap,
'copy': copy,
'extend': extend,
@@ -2552,13 +2861,17 @@ function publishExternalAPI(angular) {
'isArray': isArray,
'version': version,
'isDate': isDate,
- 'lowercase': lowercase,
- 'uppercase': uppercase,
'callbacks': {$$counter: 0},
'getTestability': getTestability,
+ 'reloadWithDebugInfo': reloadWithDebugInfo,
+ 'UNSAFE_restoreLegacyJqLiteXHTMLReplacement': UNSAFE_restoreLegacyJqLiteXHTMLReplacement,
'$$minErr': minErr,
'$$csp': csp,
- 'reloadWithDebugInfo': reloadWithDebugInfo
+ '$$encodeUriSegment': encodeUriSegment,
+ '$$encodeUriQuery': encodeUriQuery,
+ '$$lowercase': lowercase,
+ '$$stringify': stringify,
+ '$$uppercase': uppercase
});
angularModule = setupModuleLoader(window);
@@ -2577,7 +2890,6 @@ function publishExternalAPI(angular) {
form: formDirective,
script: scriptDirective,
select: selectDirective,
- style: styleDirective,
option: optionDirective,
ngBind: ngBindDirective,
ngBindHtml: ngBindHtmlDirective,
@@ -2594,6 +2906,7 @@ function publishExternalAPI(angular) {
ngInit: ngInitDirective,
ngNonBindable: ngNonBindableDirective,
ngPluralize: ngPluralizeDirective,
+ ngRef: ngRefDirective,
ngRepeat: ngRepeatDirective,
ngShow: ngShowDirective,
ngStyle: ngStyleDirective,
@@ -2617,7 +2930,8 @@ function publishExternalAPI(angular) {
ngModelOptions: ngModelOptionsDirective
}).
directive({
- ngInclude: ngIncludeFillContentDirective
+ ngInclude: ngIncludeFillContentDirective,
+ input: hiddenInputBrowserCacheDirective
}).
directive(ngAttributeAliasDirectives).
directive(ngEventDirectives);
@@ -2633,11 +2947,13 @@ function publishExternalAPI(angular) {
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
$document: $DocumentProvider,
+ $$isDocumentHidden: $$IsDocumentHiddenProvider,
$exceptionHandler: $ExceptionHandlerProvider,
$filter: $FilterProvider,
$$forceReflow: $$ForceReflowProvider,
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
+ $$intervalFactory: $$IntervalFactoryProvider,
$http: $HttpProvider,
$httpParamSerializer: $HttpParamSerializerProvider,
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
@@ -2653,6 +2969,7 @@ function publishExternalAPI(angular) {
$sce: $SceProvider,
$sceDelegate: $SceDelegateProvider,
$sniffer: $SnifferProvider,
+ $$taskTrackerFactory: $$TaskTrackerFactoryProvider,
$templateCache: $TemplateCacheProvider,
$templateRequest: $TemplateRequestProvider,
$$testability: $$TestabilityProvider,
@@ -2660,7 +2977,7 @@ function publishExternalAPI(angular) {
$window: $WindowProvider,
$$rAF: $$RAFProvider,
$$jqLite: $$jqLiteProvider,
- $$HashMap: $$HashMapProvider,
+ $$Map: $$MapProvider,
$$cookieReader: $$CookieReaderProvider
});
}
@@ -2678,11 +2995,10 @@ function publishExternalAPI(angular) {
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-/* global JQLitePrototype: true,
- addEventListenerFn: true,
- removeEventListenerFn: true,
+/* global
+ JQLitePrototype: true,
BOOLEAN_ATTR: true,
- ALIASED_ATTR: true,
+ ALIASED_ATTR: true
*/
//////////////////////////////////
@@ -2700,31 +3016,32 @@ function publishExternalAPI(angular) {
*
* If jQuery is available, `angular.element` is an alias for the
* [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element`
- * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or **jqLite**.
+ * delegates to AngularJS's built-in subset of jQuery, called "jQuery lite" or **jqLite**.
*
* jqLite is a tiny, API-compatible subset of jQuery that allows
- * Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most
+ * AngularJS to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most
* commonly needed functionality with the goal of having a very small footprint.
*
* To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the
* {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a
* specific version of jQuery if multiple versions exist on the page.
*
- * <div class="alert alert-info">**Note:** All element references in Angular are always wrapped with jQuery or
+ * <div class="alert alert-info">**Note:** All element references in AngularJS are always wrapped with jQuery or
* jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.</div>
*
* <div class="alert alert-warning">**Note:** Keep in mind that this function will not find elements
* by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)`
* or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.</div>
*
- * ## Angular's jqLite
+ * ## AngularJS's jqLite
* jqLite provides only the following jQuery methods:
*
* - [`addClass()`](http://api.jquery.com/addClass/) - Does not support a function as first argument
* - [`after()`](http://api.jquery.com/after/)
- * - [`append()`](http://api.jquery.com/append/)
+ * - [`append()`](http://api.jquery.com/append/) - Contrary to jQuery, this doesn't clone elements
+ * so will not work correctly when invoked on a jqLite object containing more than one DOM node
* - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters
- * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
+ * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData
* - [`children()`](http://api.jquery.com/children/) - Does not support selectors
* - [`clone()`](http://api.jquery.com/clone/)
* - [`contents()`](http://api.jquery.com/contents/)
@@ -2744,21 +3061,31 @@ function publishExternalAPI(angular) {
* - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors
* - [`prepend()`](http://api.jquery.com/prepend/)
* - [`prop()`](http://api.jquery.com/prop/)
- * - [`ready()`](http://api.jquery.com/ready/)
+ * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`)
* - [`remove()`](http://api.jquery.com/remove/)
- * - [`removeAttr()`](http://api.jquery.com/removeAttr/)
+ * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes
* - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument
* - [`removeData()`](http://api.jquery.com/removeData/)
* - [`replaceWith()`](http://api.jquery.com/replaceWith/)
* - [`text()`](http://api.jquery.com/text/)
* - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument
* - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers
- * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter
+ * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter
* - [`val()`](http://api.jquery.com/val/)
* - [`wrap()`](http://api.jquery.com/wrap/)
*
+ * jqLite also provides a method restoring pre-1.8 insecure treatment of XHTML-like tags.
+ * This legacy behavior turns input like `<div /><span />` to `<div></div><span></span>`
+ * instead of `<div><span></span></div>` like version 1.8 & newer do. To restore it, invoke:
+ * ```js
+ * angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement();
+ * ```
+ * Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read the
+ * [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details
+ * about the workarounds.
+ *
* ## jQuery/jqLite Extras
- * Angular also provides the following additional methods and events to both jQuery and jqLite:
+ * AngularJS also provides the following additional methods and events to both jQuery and jqLite:
*
* ### Events
* - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event
@@ -2791,13 +3118,7 @@ function publishExternalAPI(angular) {
JQLite.expando = 'ng339';
var jqCache = JQLite.cache = {},
- jqId = 1,
- addEventListenerFn = function(element, type, fn) {
- element.addEventListener(type, fn, false);
- },
- removeEventListenerFn = function(element, type, fn) {
- element.removeEventListener(type, fn, false);
- };
+ jqId = 1;
/*
* !!! This is an undocumented "private" function !!!
@@ -2810,22 +3131,31 @@ JQLite._data = function(node) {
function jqNextId() { return ++jqId; }
-var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
-var MOZ_HACK_REGEXP = /^moz([A-Z])/;
-var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"};
+var DASH_LOWERCASE_REGEXP = /-([a-z])/g;
+var MS_HACK_REGEXP = /^-ms-/;
+var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' };
var jqLiteMinErr = minErr('jqLite');
/**
- * Converts snake_case to camelCase.
- * Also there is special case for Moz prefix starting with upper case letter.
+ * Converts kebab-case to camelCase.
+ * There is also a special case for the ms prefix starting with a lowercase letter.
* @param name Name to normalize
*/
-function camelCase(name) {
- return name.
- replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
- return offset ? letter.toUpperCase() : letter;
- }).
- replace(MOZ_HACK_REGEXP, 'Moz$1');
+function cssKebabToCamel(name) {
+ return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-'));
+}
+
+function fnCamelCaseReplace(all, letter) {
+ return letter.toUpperCase();
+}
+
+/**
+ * Converts kebab-case to camelCase.
+ * @param name Name to normalize
+ */
+function kebabToCamel(name) {
+ return name
+ .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace);
}
var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;
@@ -2833,20 +3163,36 @@ var HTML_REGEXP = /<|&#?\w+;/;
var TAG_NAME_REGEXP = /<([\w:-]+)/;
var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi;
+// Table parts need to be wrapped with `<table>` or they're
+// stripped to their contents when put in a div.
+// XHTML parsers do not magically insert elements in the
+// same way that tag soup parsers do, so we cannot shorten
+// this by omitting <tbody> or other required elements.
var wrapMap = {
- 'option': [1, '<select multiple="multiple">', '</select>'],
-
- 'thead': [1, '<table>', '</table>'],
- 'col': [2, '<table><colgroup>', '</colgroup></table>'],
- 'tr': [2, '<table><tbody>', '</tbody></table>'],
- 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
- '_default': [0, "", ""]
+ thead: ['table'],
+ col: ['colgroup', 'table'],
+ tr: ['tbody', 'table'],
+ td: ['tr', 'tbody', 'table']
};
-wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
+// Support: IE <10 only
+// IE 9 requires an option wrapper & it needs to have the whole table structure
+// set up in advance; assigning `"<td></td>"` to `tr.innerHTML` doesn't work, etc.
+var wrapMapIE9 = {
+ option: [1, '<select multiple="multiple">', '</select>'],
+ _default: [0, '', '']
+};
+
+for (var key in wrapMap) {
+ var wrapMapValueClosing = wrapMap[key];
+ var wrapMapValue = wrapMapValueClosing.slice().reverse();
+ wrapMapIE9[key] = [wrapMapValue.length, '<' + wrapMapValue.join('><') + '>', '</' + wrapMapValueClosing.join('></') + '>'];
+}
+
+wrapMapIE9.optgroup = wrapMapIE9.option;
function jqLiteIsTextNode(html) {
return !HTML_REGEXP.test(html);
@@ -2866,14 +3212,8 @@ function jqLiteHasData(node) {
return false;
}
-function jqLiteCleanData(nodes) {
- for (var i = 0, ii = nodes.length; i < ii; i++) {
- jqLiteRemoveData(nodes[i]);
- }
-}
-
function jqLiteBuildFragment(html, context) {
- var tmp, tag, wrap,
+ var tmp, tag, wrap, finalHtml,
fragment = context.createDocumentFragment(),
nodes = [], i;
@@ -2882,26 +3222,43 @@ function jqLiteBuildFragment(html, context) {
nodes.push(context.createTextNode(html));
} else {
// Convert html into DOM nodes
- tmp = fragment.appendChild(context.createElement("div"));
- tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase();
- wrap = wrapMap[tag] || wrapMap._default;
- tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2];
+ tmp = fragment.appendChild(context.createElement('div'));
+ tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase();
+ finalHtml = JQLite.legacyXHTMLReplacement ?
+ html.replace(XHTML_TAG_REGEXP, '<$1></$2>') :
+ html;
- // Descend through wrappers to the right content
- i = wrap[0];
- while (i--) {
- tmp = tmp.lastChild;
+ if (msie < 10) {
+ wrap = wrapMapIE9[tag] || wrapMapIE9._default;
+ tmp.innerHTML = wrap[1] + finalHtml + wrap[2];
+
+ // Descend through wrappers to the right content
+ i = wrap[0];
+ while (i--) {
+ tmp = tmp.firstChild;
+ }
+ } else {
+ wrap = wrapMap[tag] || [];
+
+ // Create wrappers & descend into them
+ i = wrap.length;
+ while (--i > -1) {
+ tmp.appendChild(window.document.createElement(wrap[i]));
+ tmp = tmp.firstChild;
+ }
+
+ tmp.innerHTML = finalHtml;
}
nodes = concat(nodes, tmp.childNodes);
tmp = fragment.firstChild;
- tmp.textContent = "";
+ tmp.textContent = '';
}
// Remove wrapper from fragment
- fragment.textContent = "";
- fragment.innerHTML = ""; // Clear inner HTML
+ fragment.textContent = '';
+ fragment.innerHTML = ''; // Clear inner HTML
forEach(nodes, function(node) {
fragment.appendChild(node);
});
@@ -2936,10 +3293,9 @@ function jqLiteWrapNode(node, wrapper) {
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
-var jqLiteContains = window.Node.prototype.contains || function(arg) {
- // jshint bitwise: false
+var jqLiteContains = window.Node.prototype.contains || /** @this */ function(arg) {
+ // eslint-disable-next-line no-bitwise
return !!(this.compareDocumentPosition(arg) & 16);
- // jshint bitwise: true
};
/////////////////////////////////////////////
@@ -2955,7 +3311,7 @@ function JQLite(element) {
argIsString = true;
}
if (!(this instanceof JQLite)) {
- if (argIsString && element.charAt(0) != '<') {
+ if (argIsString && element.charAt(0) !== '<') {
throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element');
}
return new JQLite(element);
@@ -2963,6 +3319,8 @@ function JQLite(element) {
if (argIsString) {
jqLiteAddNodes(this, jqLiteParseHTML(element));
+ } else if (isFunction(element)) {
+ jqLiteReady(element);
} else {
jqLiteAddNodes(this, element);
}
@@ -2973,13 +3331,32 @@ function jqLiteClone(element) {
}
function jqLiteDealoc(element, onlyDescendants) {
- if (!onlyDescendants) jqLiteRemoveData(element);
+ if (!onlyDescendants && jqLiteAcceptsData(element)) jqLite.cleanData([element]);
if (element.querySelectorAll) {
- var descendants = element.querySelectorAll('*');
- for (var i = 0, l = descendants.length; i < l; i++) {
- jqLiteRemoveData(descendants[i]);
- }
+ jqLite.cleanData(element.querySelectorAll('*'));
+ }
+}
+
+function isEmptyObject(obj) {
+ var name;
+
+ for (name in obj) {
+ return false;
+ }
+ return true;
+}
+
+function removeIfEmptyData(element) {
+ var expandoId = element.ng339;
+ var expandoStore = expandoId && jqCache[expandoId];
+
+ var events = expandoStore && expandoStore.events;
+ var data = expandoStore && expandoStore.data;
+
+ if ((!data || isEmptyObject(data)) && (!events || isEmptyObject(events))) {
+ delete jqCache[expandoId];
+ element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it
}
}
@@ -2995,7 +3372,7 @@ function jqLiteOff(element, type, fn, unsupported) {
if (!type) {
for (type in events) {
if (type !== '$destroy') {
- removeEventListenerFn(element, type, handle);
+ element.removeEventListener(type, handle);
}
delete events[type];
}
@@ -3007,7 +3384,7 @@ function jqLiteOff(element, type, fn, unsupported) {
arrayRemove(listenerFns || [], fn);
}
if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
- removeEventListenerFn(element, type, handle);
+ element.removeEventListener(type, handle);
delete events[type];
}
};
@@ -3019,6 +3396,8 @@ function jqLiteOff(element, type, fn, unsupported) {
}
});
}
+
+ removeIfEmptyData(element);
}
function jqLiteRemoveData(element, name) {
@@ -3028,17 +3407,11 @@ function jqLiteRemoveData(element, name) {
if (expandoStore) {
if (name) {
delete expandoStore.data[name];
- return;
+ } else {
+ expandoStore.data = {};
}
- if (expandoStore.handle) {
- if (expandoStore.events.$destroy) {
- expandoStore.handle({}, '$destroy');
- }
- jqLiteOff(element);
- }
- delete jqCache[expandoId];
- element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it
+ removeIfEmptyData(element);
}
}
@@ -3058,6 +3431,7 @@ function jqLiteExpandoStore(element, createIfNecessary) {
function jqLiteData(element, key, value) {
if (jqLiteAcceptsData(element)) {
+ var prop;
var isSimpleSetter = isDefined(value);
var isSimpleGetter = !isSimpleSetter && key && !isObject(key);
@@ -3066,16 +3440,18 @@ function jqLiteData(element, key, value) {
var data = expandoStore && expandoStore.data;
if (isSimpleSetter) { // data('key', value)
- data[key] = value;
+ data[kebabToCamel(key)] = value;
} else {
if (massGetter) { // data()
return data;
} else {
if (isSimpleGetter) { // data('key')
// don't force creation of expandoStore if it doesn't exist yet
- return data && data[key];
+ return data && data[kebabToCamel(key)];
} else { // mass-setter: data({key1: val1, key2: val2})
- extend(data, key);
+ for (prop in key) {
+ data[kebabToCamel(prop)] = key[prop];
+ }
}
}
}
@@ -3084,35 +3460,43 @@ function jqLiteData(element, key, value) {
function jqLiteHasClass(element, selector) {
if (!element.getAttribute) return false;
- return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " ").
- indexOf(" " + selector + " ") > -1);
+ return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' ').
+ indexOf(' ' + selector + ' ') > -1);
}
function jqLiteRemoveClass(element, cssClasses) {
if (cssClasses && element.setAttribute) {
+ var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ')
+ .replace(/[\n\t]/g, ' ');
+ var newClasses = existingClasses;
+
forEach(cssClasses.split(' '), function(cssClass) {
- element.setAttribute('class', trim(
- (" " + (element.getAttribute('class') || '') + " ")
- .replace(/[\n\t]/g, " ")
- .replace(" " + trim(cssClass) + " ", " "))
- );
+ cssClass = trim(cssClass);
+ newClasses = newClasses.replace(' ' + cssClass + ' ', ' ');
});
+
+ if (newClasses !== existingClasses) {
+ element.setAttribute('class', trim(newClasses));
+ }
}
}
function jqLiteAddClass(element, cssClasses) {
if (cssClasses && element.setAttribute) {
var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ')
- .replace(/[\n\t]/g, " ");
+ .replace(/[\n\t]/g, ' ');
+ var newClasses = existingClasses;
forEach(cssClasses.split(' '), function(cssClass) {
cssClass = trim(cssClass);
- if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) {
- existingClasses += cssClass + ' ';
+ if (newClasses.indexOf(' ' + cssClass + ' ') === -1) {
+ newClasses += cssClass + ' ';
}
});
- element.setAttribute('class', trim(existingClasses));
+ if (newClasses !== existingClasses) {
+ element.setAttribute('class', trim(newClasses));
+ }
}
}
@@ -3150,7 +3534,7 @@ function jqLiteController(element, name) {
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 == NODE_TYPE_DOCUMENT) {
+ if (element.nodeType === NODE_TYPE_DOCUMENT) {
element = element.documentElement;
}
var names = isArray(name) ? name : [name];
@@ -3194,30 +3578,32 @@ function jqLiteDocumentLoaded(action, win) {
}
}
+function jqLiteReady(fn) {
+ function trigger() {
+ window.document.removeEventListener('DOMContentLoaded', trigger);
+ window.removeEventListener('load', trigger);
+ fn();
+ }
+
+ // check if document is already loaded
+ if (window.document.readyState === 'complete') {
+ window.setTimeout(fn);
+ } else {
+ // We can not use jqLite since we are not done loading and jQuery could be loaded later.
+
+ // Works for modern browsers and IE9
+ window.document.addEventListener('DOMContentLoaded', trigger);
+
+ // Fallback to window.onload for others
+ window.addEventListener('load', trigger);
+ }
+}
+
//////////////////////////////////////////
// Functions which are declared directly.
//////////////////////////////////////////
var JQLitePrototype = JQLite.prototype = {
- ready: function(fn) {
- var fired = false;
-
- function trigger() {
- if (fired) return;
- fired = true;
- fn();
- }
-
- // check if document is already loaded
- if (window.document.readyState === 'complete') {
- window.setTimeout(trigger);
- } else {
- this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9
- // we can not use jqLite since we are not done loading and jQuery could be loaded later.
- // jshint -W064
- JQLite(window).on('load', trigger); // fallback to window.onload for others
- // jshint +W064
- }
- },
+ ready: jqLiteReady,
toString: function() {
var value = [];
forEach(this, function(e) { value.push('' + e);});
@@ -3252,7 +3638,8 @@ var ALIASED_ATTR = {
'ngMaxlength': 'maxlength',
'ngMin': 'min',
'ngMax': 'max',
- 'ngPattern': 'pattern'
+ 'ngPattern': 'pattern',
+ 'ngStep': 'step'
};
function getBooleanAttrName(element, name) {
@@ -3271,7 +3658,12 @@ forEach({
data: jqLiteData,
removeData: jqLiteRemoveData,
hasData: jqLiteHasData,
- cleanData: jqLiteCleanData
+ cleanData: function jqLiteCleanData(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ jqLiteRemoveData(nodes[i]);
+ jqLiteOff(nodes[i]);
+ }
+ }
}, function(fn, name) {
JQLite[name] = fn;
});
@@ -3303,7 +3695,7 @@ forEach({
hasClass: jqLiteHasClass,
css: function(element, name, value) {
- name = camelCase(name);
+ name = cssKebabToCamel(name);
if (isDefined(value)) {
element.style[name] = value;
@@ -3313,33 +3705,33 @@ forEach({
},
attr: function(element, name, value) {
+ var ret;
var nodeType = element.nodeType;
- if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) {
+ if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT ||
+ !element.getAttribute) {
return;
}
+
var lowercasedName = lowercase(name);
- if (BOOLEAN_ATTR[lowercasedName]) {
- if (isDefined(value)) {
- if (!!value) {
- element[name] = true;
- element.setAttribute(name, lowercasedName);
- } else {
- element[name] = false;
- element.removeAttribute(lowercasedName);
- }
+ var isBooleanAttr = BOOLEAN_ATTR[lowercasedName];
+
+ if (isDefined(value)) {
+ // setter
+
+ if (value === null || (value === false && isBooleanAttr)) {
+ element.removeAttribute(name);
} else {
- return (element[name] ||
- (element.attributes.getNamedItem(name) || noop).specified)
- ? lowercasedName
- : undefined;
- }
- } else if (isDefined(value)) {
- element.setAttribute(name, value);
- } else if (element.getAttribute) {
- // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code
- // some elements (e.g. Document) don't have get attribute, so return undefined
- var ret = element.getAttribute(name, 2);
- // normalize non-existing attributes to undefined (as jQuery)
+ element.setAttribute(name, isBooleanAttr ? lowercasedName : value);
+ }
+ } else {
+ // getter
+
+ ret = element.getAttribute(name);
+
+ if (isBooleanAttr && ret !== null) {
+ ret = lowercasedName;
+ }
+ // Normalize non-existing attributes to undefined (as jQuery).
return ret === null ? undefined : ret;
}
},
@@ -3374,7 +3766,7 @@ forEach({
result.push(option.value || option.text);
}
});
- return result.length === 0 ? null : result;
+ return result;
}
return element.value;
}
@@ -3402,7 +3794,7 @@ forEach({
// in a way that survives minification.
// jqLiteEmpty takes no arguments but is a setter.
if (fn !== jqLiteEmpty &&
- (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
+ (isUndefined((fn.length === 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
if (isObject(arg1)) {
// we are a write, but the object properties are the key/values
@@ -3544,7 +3936,7 @@ forEach({
eventFns = events[type] = [];
eventFns.specialHandlerWrapper = specialHandlerWrapper;
if (type !== '$destroy' && !noEventListener) {
- addEventListenerFn(element, type, handle);
+ element.addEventListener(type, handle);
}
}
@@ -3637,12 +4029,15 @@ forEach({
after: function(element, newElement) {
var index = element, parent = element.parentNode;
- newElement = new JQLite(newElement);
- for (var i = 0, ii = newElement.length; i < ii; i++) {
- var node = newElement[i];
- parent.insertBefore(node, index.nextSibling);
- index = node;
+ if (parent) {
+ newElement = new JQLite(newElement);
+
+ for (var i = 0, ii = newElement.length; i < ii; i++) {
+ var node = newElement[i];
+ parent.insertBefore(node, index.nextSibling);
+ index = node;
+ }
}
},
@@ -3736,14 +4131,15 @@ forEach({
}
return isDefined(value) ? value : this;
};
-
- // bind legacy bind/unbind to on/off
- JQLite.prototype.bind = JQLite.prototype.on;
- JQLite.prototype.unbind = JQLite.prototype.off;
});
+// bind legacy bind/unbind to on/off
+JQLite.prototype.bind = JQLite.prototype.on;
+JQLite.prototype.unbind = JQLite.prototype.off;
+
// Provider for private $$jqLite service
+/** @this */
function $$jqLiteProvider() {
this.$get = function $$jqLite() {
return extend(JQLite, {
@@ -3786,7 +4182,7 @@ function hashKey(obj, nextUidFn) {
}
var objType = typeof obj;
- if (objType == 'function' || (objType == 'object' && obj !== null)) {
+ if (objType === 'function' || (objType === 'object' && obj !== null)) {
key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)();
} else {
key = objType + ':' + obj;
@@ -3795,50 +4191,74 @@ function hashKey(obj, nextUidFn) {
return key;
}
-/**
- * HashMap which can use objects as keys
- */
-function HashMap(array, isolatedUid) {
- if (isolatedUid) {
- var uid = 0;
- this.nextUid = function() {
- return ++uid;
- };
- }
- forEach(array, this.put, this);
+// A minimal ES2015 Map implementation.
+// Should be bug/feature equivalent to the native implementations of supported browsers
+// (for the features required in Angular).
+// See https://kangax.github.io/compat-table/es6/#test-Map
+var nanKey = Object.create(null);
+function NgMapShim() {
+ this._keys = [];
+ this._values = [];
+ this._lastKey = NaN;
+ this._lastIndex = -1;
}
-HashMap.prototype = {
- /**
- * Store key value pair
- * @param key key to store can be any type
- * @param value value to store can be any type
- */
- put: function(key, value) {
- this[hashKey(key, this.nextUid)] = value;
+NgMapShim.prototype = {
+ _idx: function(key) {
+ if (key !== this._lastKey) {
+ this._lastKey = key;
+ this._lastIndex = this._keys.indexOf(key);
+ }
+ return this._lastIndex;
+ },
+ _transformKey: function(key) {
+ return isNumberNaN(key) ? nanKey : key;
},
-
- /**
- * @param key
- * @returns {Object} the value for the key
- */
get: function(key) {
- return this[hashKey(key, this.nextUid)];
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx !== -1) {
+ return this._values[idx];
+ }
},
+ has: function(key) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ return idx !== -1;
+ },
+ set: function(key, value) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx === -1) {
+ idx = this._lastIndex = this._keys.length;
+ }
+ this._keys[idx] = key;
+ this._values[idx] = value;
- /**
- * Remove the key/value pair
- * @param key
- */
- remove: function(key) {
- var value = this[key = hashKey(key, this.nextUid)];
- delete this[key];
- return value;
+ // Support: IE11
+ // Do not `return this` to simulate the partial IE11 implementation
+ },
+ delete: function(key) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx === -1) {
+ return false;
+ }
+ this._keys.splice(idx, 1);
+ this._values.splice(idx, 1);
+ this._lastKey = NaN;
+ this._lastIndex = -1;
+ return true;
}
};
-var $$HashMapProvider = [function() {
+// For now, always use `NgMapShim`, even if `window.Map` is available. Some native implementations
+// are still buggy (often in subtle ways) and can cause hard-to-debug failures. When native `Map`
+// implementations get more stable, we can reconsider switching to `window.Map` (when available).
+var NgMap = NgMapShim;
+
+var $$MapProvider = [/** @this */function() {
this.$get = [function() {
- return HashMap;
+ return NgMap;
}];
}];
@@ -3872,8 +4292,8 @@ var $$HashMapProvider = [function() {
* });
* ```
*
- * Sometimes you want to get access to the injector of a currently running Angular app
- * from outside Angular. Perhaps, you want to inject and compile some markup after the
+ * Sometimes you want to get access to the injector of a currently running AngularJS app
+ * from outside AngularJS. Perhaps, you want to inject and compile some markup after the
* application has been bootstrapped. You can do this using the extra `injector()` added
* to JQuery/jqLite elements. See {@link angular.element}.
*
@@ -3905,19 +4325,15 @@ var $$HashMapProvider = [function() {
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/
-var ARROW_ARG = /^([^\(]+?)=>/;
-var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
+var ARROW_ARG = /^([^(]+?)=>/;
+var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
function stringifyFn(fn) {
- // Support: Chrome 50-51 only
- // Creating a new string by adding `' '` at the end, to hack around some bug in Chrome v50/51
- // (See https://github.com/angular/angular.js/issues/14487.)
- // TODO (gkalpak): Remove workaround when Chrome v52 is released
- return Function.prototype.toString.call(fn) + ' ';
+ return Function.prototype.toString.call(fn);
}
function extractArgs(fn) {
@@ -3993,7 +4409,7 @@ function annotate(fn, strictDi, name) {
* })).toBe($injector);
* ```
*
- * # Injection Function Annotation
+ * ## Injection Function Annotation
*
* JavaScript does not have annotations, and annotations are needed for dependency injection. The
* following are all valid ways of annotating function with injection arguments and are equivalent.
@@ -4011,7 +4427,7 @@ function annotate(fn, strictDi, name) {
* $injector.invoke(['serviceA', function(serviceA){}]);
* ```
*
- * ## Inference
+ * ### Inference
*
* In JavaScript calling `toString()` on a function returns the function definition. The definition
* can then be parsed and the function arguments can be extracted. This method of discovering
@@ -4019,14 +4435,36 @@ function annotate(fn, strictDi, name) {
* *NOTE:* This does not work with minification, and obfuscation tools since these tools change the
* argument names.
*
- * ## `$inject` Annotation
+ * ### `$inject` Annotation
* By adding an `$inject` property onto a function the injection parameters can be specified.
*
- * ## Inline
+ * ### Inline
* As an array of injection names, where the last item in the array is the function to call.
*/
/**
+ * @ngdoc property
+ * @name $injector#modules
+ * @type {Object}
+ * @description
+ * A hash containing all the modules that have been loaded into the
+ * $injector.
+ *
+ * You can use this property to find out information about a module via the
+ * {@link angular.Module#info `myModule.info(...)`} method.
+ *
+ * For example:
+ *
+ * ```
+ * var info = $injector.modules['ngAnimate'].info();
+ * ```
+ *
+ * **Do not use this property to attempt to modify the modules after the application
+ * has been bootstrapped.**
+ */
+
+
+/**
* @ngdoc method
* @name $injector#get
*
@@ -4088,7 +4526,7 @@ function annotate(fn, strictDi, name) {
* function is invoked. There are three ways in which the function can be annotated with the needed
* dependencies.
*
- * # Argument names
+ * #### Argument names
*
* The simplest form is to extract the dependencies from the arguments of the function. This is done
* by converting the function into a string using `toString()` method and extracting the argument
@@ -4108,7 +4546,7 @@ function annotate(fn, strictDi, name) {
* This method does not work with code minification / obfuscation. For this reason the following
* annotation strategies are supported.
*
- * # The `$inject` property
+ * #### The `$inject` property
*
* If a function has an `$inject` property and its value is an array of strings, then the strings
* represent names of services to be injected into the function.
@@ -4124,7 +4562,7 @@ function annotate(fn, strictDi, name) {
* expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
* ```
*
- * # The array notation
+ * #### The array notation
*
* It is often desirable to inline Injected functions and that's when setting the `$inject` property
* is very inconvenient. In these situations using the array notation to specify the dependencies in
@@ -4161,8 +4599,45 @@ function annotate(fn, strictDi, name) {
*
* @returns {Array.<string>} The names of the services which the function requires.
*/
-
-
+/**
+ * @ngdoc method
+ * @name $injector#loadNewModules
+ *
+ * @description
+ *
+ * **This is a dangerous API, which you use at your own risk!**
+ *
+ * Add the specified modules to the current injector.
+ *
+ * This method will add each of the injectables to the injector and execute all of the config and run
+ * blocks for each module passed to the method.
+ *
+ * If a module has already been loaded into the injector then it will not be loaded again.
+ *
+ * * The application developer is responsible for loading the code containing the modules; and for
+ * ensuring that lazy scripts are not downloaded and executed more often that desired.
+ * * Previously compiled HTML will not be affected by newly loaded directives, filters and components.
+ * * Modules cannot be unloaded.
+ *
+ * You can use {@link $injector#modules `$injector.modules`} to check whether a module has been loaded
+ * into the injector, which may indicate whether the script has been executed already.
+ *
+ * @example
+ * Here is an example of loading a bundle of modules, with a utility method called `getScript`:
+ *
+ * ```javascript
+ * app.factory('loadModule', function($injector) {
+ * return function loadModule(moduleName, bundleUrl) {
+ * return getScript(bundleUrl).then(function() { $injector.loadNewModules([moduleName]); });
+ * };
+ * })
+ * ```
+ *
+ * @param {Array<String|Function|Array>=} mods an array of modules to load into the application.
+ * Each item in the array should be the name of a predefined module or a (DI annotated)
+ * function that will be invoked by the injector as a `config` block.
+ * See: {@link angular.module modules}
+ */
/**
@@ -4175,7 +4650,7 @@ function annotate(fn, strictDi, name) {
* with the {@link auto.$injector $injector}. Many of these functions are also exposed on
* {@link angular.Module}.
*
- * An Angular **service** is a singleton object created by a **service factory**. These **service
+ * An AngularJS **service** is a singleton object created by a **service factory**. These **service
* factories** are functions which, in turn, are created by a **service provider**.
* The **service providers** are constructor functions. When instantiated they must contain a
* property called `$get`, which holds the **service factory** function.
@@ -4227,6 +4702,9 @@ function annotate(fn, strictDi, name) {
* which lets you specify whether the {@link ng.$log $log} service will log debug messages to the
* console or not.
*
+ * It is possible to inject other providers into the provider function,
+ * but the injected provider must have been defined before the one that requires it.
+ *
* @param {string} name The name of the instance. NOTE: the provider will be available under `name +
'Provider'` key.
* @param {(Object|function())} provider If the provider is:
@@ -4403,7 +4881,7 @@ function annotate(fn, strictDi, name) {
*
* Value services are similar to constant services, except that they cannot be injected into a
* module configuration function (see {@link angular.Module#config}) but they can be overridden by
- * an Angular {@link auto.$provide#decorator decorator}.
+ * an AngularJS {@link auto.$provide#decorator decorator}.
*
* @param {string} name The name of the instance.
* @param {*} value The value.
@@ -4434,7 +4912,7 @@ function annotate(fn, strictDi, name) {
*
* But unlike {@link auto.$provide#value value}, a constant can be
* injected into a module configuration function (see {@link angular.Module#config}) and it cannot
- * be overridden by an Angular {@link auto.$provide#decorator decorator}.
+ * be overridden by an AngularJS {@link auto.$provide#decorator decorator}.
*
* @param {string} name The name of the constant.
* @param {*} value The constant value.
@@ -4492,7 +4970,7 @@ function createInjector(modulesToLoad, strictDi) {
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
- loadedModules = new HashMap([], true),
+ loadedModules = new NgMap(),
providerCache = {
$provide: {
provider: supportObject(provider),
@@ -4508,7 +4986,7 @@ function createInjector(modulesToLoad, strictDi) {
if (angular.isString(caller)) {
path.push(caller);
}
- throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
+ throw $injectorMinErr('unpr', 'Unknown provider: {0}', path.join(' <- '));
})),
instanceCache = {},
protoInstanceInjector =
@@ -4520,11 +4998,17 @@ function createInjector(modulesToLoad, strictDi) {
instanceInjector = protoInstanceInjector;
providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
+ instanceInjector.modules = providerInjector.modules = createMap();
var runBlocks = loadModules(modulesToLoad);
instanceInjector = protoInstanceInjector.get('$injector');
instanceInjector.strictDi = strictDi;
forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); });
+ instanceInjector.loadNewModules = function(mods) {
+ forEach(loadModules(mods), function(fn) { if (fn) instanceInjector.invoke(fn); });
+ };
+
+
return instanceInjector;
////////////////////////////////////
@@ -4547,16 +5031,16 @@ function createInjector(modulesToLoad, strictDi) {
provider_ = providerInjector.instantiate(provider_);
}
if (!provider_.$get) {
- throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
+ throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name);
}
- return providerCache[name + providerSuffix] = provider_;
+ return (providerCache[name + providerSuffix] = provider_);
}
function enforceReturnValue(name, factory) {
- return function enforcedReturnValue() {
+ return /** @this */ function enforcedReturnValue() {
var result = instanceInjector.invoke(factory, this);
if (isUndefined(result)) {
- throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name);
+ throw $injectorMinErr('undef', 'Provider \'{0}\' must return a value from $get factory method.', name);
}
return result;
};
@@ -4600,7 +5084,7 @@ function createInjector(modulesToLoad, strictDi) {
var runBlocks = [], moduleFn;
forEach(modulesToLoad, function(module) {
if (loadedModules.get(module)) return;
- loadedModules.put(module, true);
+ loadedModules.set(module, true);
function runInvokeQueue(queue) {
var i, ii;
@@ -4615,6 +5099,7 @@ function createInjector(modulesToLoad, strictDi) {
try {
if (isString(module)) {
moduleFn = angularModule(module);
+ instanceInjector.modules[module] = moduleFn;
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
runInvokeQueue(moduleFn._invokeQueue);
runInvokeQueue(moduleFn._configBlocks);
@@ -4629,15 +5114,15 @@ function createInjector(modulesToLoad, strictDi) {
if (isArray(module)) {
module = module[module.length - 1];
}
- if (e.message && e.stack && e.stack.indexOf(e.message) == -1) {
+ if (e.message && e.stack && e.stack.indexOf(e.message) === -1) {
// Safari & FF's stack traces don't contain error.message content
// unlike those of Chrome and IE
// So if stack doesn't contain message, we create a new string that contains both.
// Since error.stack is read-only in Safari, I'm overriding e and not e.stack here.
- /* jshint -W022 */
+ // eslint-disable-next-line no-ex-assign
e = e.message + '\n' + e.stack;
}
- throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}",
+ throw $injectorMinErr('modulerr', 'Failed to instantiate module {0} due to:\n{1}',
module, e.stack || e.message || e);
}
});
@@ -4661,7 +5146,8 @@ function createInjector(modulesToLoad, strictDi) {
try {
path.unshift(serviceName);
cache[serviceName] = INSTANTIATING;
- return cache[serviceName] = factory(serviceName, caller);
+ cache[serviceName] = factory(serviceName, caller);
+ return cache[serviceName];
} catch (err) {
if (cache[serviceName] === INSTANTIATING) {
delete cache[serviceName];
@@ -4691,14 +5177,16 @@ function createInjector(modulesToLoad, strictDi) {
}
function isClass(func) {
+ // Support: IE 9-11 only
// IE 9-11 do not support classes and IE9 leaks with the code below.
- if (msie <= 11) {
+ if (msie || typeof func !== 'function') {
return false;
}
- // Support: Edge 12-13 only
- // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/
- return typeof func === 'function'
- && /^(?:class\b|constructor\()/.test(stringifyFn(func));
+ var result = func.$$ngIsClass;
+ if (!isBoolean(result)) {
+ result = func.$$ngIsClass = /^class\b/.test(stringifyFn(func));
+ }
+ return result;
}
function invoke(fn, self, locals, serviceName) {
@@ -4751,6 +5239,7 @@ createInjector.$$annotate = annotate;
/**
* @ngdoc provider
* @name $anchorScrollProvider
+ * @this
*
* @description
* Use `$anchorScrollProvider` to disable automatic scrolling whenever
@@ -4822,7 +5311,7 @@ function $AnchorScrollProvider() {
* </div>
*
* @example
- <example module="anchorScrollExample">
+ <example module="anchorScrollExample" name="anchor-scroll">
<file name="index.html">
<div id="scrollArea" ng-controller="ScrollController">
<a ng-click="gotoBottom()">Go to bottom</a>
@@ -4832,7 +5321,7 @@ function $AnchorScrollProvider() {
<file name="script.js">
angular.module('anchorScrollExample', [])
.controller('ScrollController', ['$scope', '$location', '$anchorScroll',
- function ($scope, $location, $anchorScroll) {
+ function($scope, $location, $anchorScroll) {
$scope.gotoBottom = function() {
// set the location.hash to the id of
// the element you wish to scroll to.
@@ -4861,7 +5350,7 @@ function $AnchorScrollProvider() {
* See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details.
*
* @example
- <example module="anchorScrollOffsetExample">
+ <example module="anchorScrollOffsetExample" name="anchor-scroll-offset">
<file name="index.html">
<div class="fixed-header" ng-controller="headerCtrl">
<a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
@@ -4878,7 +5367,7 @@ function $AnchorScrollProvider() {
$anchorScroll.yOffset = 50; // always scroll by 50 extra pixels
}])
.controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
- function ($anchorScroll, $location, $scope) {
+ function($anchorScroll, $location, $scope) {
$scope.gotoAnchor = function(x) {
var newHash = 'anchor' + x;
if ($location.hash() !== newHash) {
@@ -4985,7 +5474,8 @@ function $AnchorScrollProvider() {
}
function scroll(hash) {
- hash = isString(hash) ? hash : $location.hash();
+ // Allow numeric hashes
+ hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash();
var elm;
// empty hash, scroll to the top of the page
@@ -4997,7 +5487,7 @@ function $AnchorScrollProvider() {
// first anchor with given name :-D
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm);
- // no element and hash == 'top', scroll to the top of the page
+ // no element and hash === 'top', scroll to the top of the page
else if (hash === 'top') scrollTo(null);
}
@@ -5072,14 +5562,14 @@ function prepareAnimateOptions(options) {
: {};
}
-var $$CoreAnimateJsProvider = function() {
+var $$CoreAnimateJsProvider = /** @this */ function() {
this.$get = noop;
};
// this is prefixed with Core since it conflicts with
// the animateQueueProvider defined in ngAnimate/animateQueue.js
-var $$CoreAnimateQueueProvider = function() {
- var postDigestQueue = new HashMap();
+var $$CoreAnimateQueueProvider = /** @this */ function() {
+ var postDigestQueue = new NgMap();
var postDigestElements = [];
this.$get = ['$$AnimateRunner', '$rootScope',
@@ -5091,17 +5581,23 @@ var $$CoreAnimateQueueProvider = function() {
pin: noop,
push: function(element, event, options, domOperation) {
- domOperation && domOperation();
+ if (domOperation) {
+ domOperation();
+ }
options = options || {};
- options.from && element.css(options.from);
- options.to && element.css(options.to);
+ if (options.from) {
+ element.css(options.from);
+ }
+ if (options.to) {
+ element.css(options.to);
+ }
if (options.addClass || options.removeClass) {
addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
}
- var runner = new $$AnimateRunner(); // jshint ignore:line
+ var runner = new $$AnimateRunner();
// since there are no animations to run the runner needs to be
// notified that the animation call is complete.
@@ -5145,10 +5641,14 @@ var $$CoreAnimateQueueProvider = function() {
});
forEach(element, function(elm) {
- toAdd && jqLiteAddClass(elm, toAdd);
- toRemove && jqLiteRemoveClass(elm, toRemove);
+ if (toAdd) {
+ jqLiteAddClass(elm, toAdd);
+ }
+ if (toRemove) {
+ jqLiteRemoveClass(elm, toRemove);
+ }
});
- postDigestQueue.remove(element);
+ postDigestQueue.delete(element);
}
});
postDigestElements.length = 0;
@@ -5163,7 +5663,7 @@ var $$CoreAnimateQueueProvider = function() {
if (classesAdded || classesRemoved) {
- postDigestQueue.put(element, data);
+ postDigestQueue.set(element, data);
postDigestElements.push(element);
if (postDigestElements.length === 1) {
@@ -5186,8 +5686,10 @@ var $$CoreAnimateQueueProvider = function() {
*
* To see the functional implementation check out `src/ngAnimate/animate.js`.
*/
-var $AnimateProvider = ['$provide', function($provide) {
+var $AnimateProvider = ['$provide', /** @this */ function($provide) {
var provider = this;
+ var classNameFilter = null;
+ var customFilter = null;
this.$$registeredAnimations = Object.create(null);
@@ -5232,7 +5734,7 @@ var $AnimateProvider = ['$provide', function($provide) {
*/
this.register = function(name, factory) {
if (name && name.charAt(0) !== '.') {
- throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name);
+ throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name);
}
var key = name + '-animation';
@@ -5242,6 +5744,51 @@ var $AnimateProvider = ['$provide', function($provide) {
/**
* @ngdoc method
+ * @name $animateProvider#customFilter
+ *
+ * @description
+ * Sets and/or returns the custom filter function that is used to "filter" animations, i.e.
+ * determine if an animation is allowed or not. When no filter is specified (the default), no
+ * animation will be blocked. Setting the `customFilter` value will only allow animations for
+ * which the filter function's return value is truthy.
+ *
+ * This allows to easily create arbitrarily complex rules for filtering animations, such as
+ * allowing specific events only, or enabling animations on specific subtrees of the DOM, etc.
+ * Filtering animations can also boost performance for low-powered devices, as well as
+ * applications containing a lot of structural operations.
+ *
+ * <div class="alert alert-success">
+ * **Best Practice:**
+ * Keep the filtering function as lean as possible, because it will be called for each DOM
+ * action (e.g. insertion, removal, class change) performed by "animation-aware" directives.
+ * See {@link guide/animations#which-directives-support-animations- here} for a list of built-in
+ * directives that support animations.
+ * Performing computationally expensive or time-consuming operations on each call of the
+ * filtering function can make your animations sluggish.
+ * </div>
+ *
+ * **Note:** If present, `customFilter` will be checked before
+ * {@link $animateProvider#classNameFilter classNameFilter}.
+ *
+ * @param {Function=} filterFn - The filter function which will be used to filter all animations.
+ * If a falsy value is returned, no animation will be performed. The function will be called
+ * with the following arguments:
+ * - **node** `{DOMElement}` - The DOM element to be animated.
+ * - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass`
+ * etc).
+ * - **options** `{Object}` - A collection of options/styles used for the animation.
+ * @return {Function} The current filter function or `null` if there is none set.
+ */
+ this.customFilter = function(filterFn) {
+ if (arguments.length === 1) {
+ customFilter = isFunction(filterFn) ? filterFn : null;
+ }
+
+ return customFilter;
+ };
+
+ /**
+ * @ngdoc method
* @name $animateProvider#classNameFilter
*
* @description
@@ -5251,21 +5798,26 @@ var $AnimateProvider = ['$provide', function($provide) {
* When setting the `classNameFilter` value, animations will only be performed on elements
* that successfully match the filter expression. This in turn can boost performance
* for low-powered devices as well as applications containing a lot of structural operations.
+ *
+ * **Note:** If present, `classNameFilter` will be checked after
+ * {@link $animateProvider#customFilter customFilter}. If `customFilter` is present and returns
+ * false, `classNameFilter` will not be checked.
+ *
* @param {RegExp=} expression The className expression which will be checked against all animations
* @return {RegExp} The current CSS className expression value. If null then there is no expression value
*/
this.classNameFilter = function(expression) {
if (arguments.length === 1) {
- this.$$classNameFilter = (expression instanceof RegExp) ? expression : null;
- if (this.$$classNameFilter) {
- var reservedRegex = new RegExp("(\\s+|\\/)" + NG_ANIMATE_CLASSNAME + "(\\s+|\\/)");
- if (reservedRegex.test(this.$$classNameFilter.toString())) {
- throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
-
+ classNameFilter = (expression instanceof RegExp) ? expression : null;
+ if (classNameFilter) {
+ var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]');
+ if (reservedRegex.test(classNameFilter.toString())) {
+ classNameFilter = null;
+ throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
}
}
}
- return this.$$classNameFilter;
+ return classNameFilter;
};
this.$get = ['$$animateQueue', function($$animateQueue) {
@@ -5279,7 +5831,11 @@ var $AnimateProvider = ['$provide', function($provide) {
afterElement = null;
}
}
- afterElement ? afterElement.after(element) : parentElement.prepend(element);
+ if (afterElement) {
+ afterElement.after(element);
+ } else {
+ parentElement.prepend(element);
+ }
}
/**
@@ -5322,14 +5878,39 @@ var $AnimateProvider = ['$provide', function($provide) {
* );
* ```
*
+ * <div class="alert alert-warning">
+ * **Note**: Generally, the events that are fired correspond 1:1 to `$animate` method names,
+ * e.g. {@link ng.$animate#addClass addClass()} will fire `addClass`, and {@link ng.ngClass}
+ * will fire `addClass` if classes are added, and `removeClass` if classes are removed.
+ * However, there are two exceptions:
+ *
+ * <ul>
+ * <li>if both an {@link ng.$animate#addClass addClass()} and a
+ * {@link ng.$animate#removeClass removeClass()} action are performed during the same
+ * animation, the event fired will be `setClass`. This is true even for `ngClass`.</li>
+ * <li>an {@link ng.$animate#animate animate()} call that adds and removes classes will fire
+ * the `setClass` event, but if it either removes or adds classes,
+ * it will fire `animate` instead.</li>
+ * </ul>
+ *
+ * </div>
+ *
* @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...)
* @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself
* as well as among its children
- * @param {Function} callback the callback function that will be fired when the listener is triggered
+ * @param {Function} callback the callback function that will be fired when the listener is triggered.
*
* The arguments present in the callback function are:
* * `element` - The captured DOM element that the animation was fired on.
* * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends).
+ * * `data` - an object with these properties:
+ * * addClass - `{string|null}` - space-separated CSS classes to add to the element
+ * * removeClass - `{string|null}` - space-separated CSS classes to remove from the element
+ * * from - `{Object|null}` - CSS properties & values at the beginning of the animation
+ * * to - `{Object|null}` - CSS properties & values at the end of the animation
+ *
+ * Note that the callback does not trigger a scope digest. Wrap your call into a
+ * {@link $rootScope.Scope#$apply scope.$apply} to propagate changes to the scope.
*/
on: $$animateQueue.on,
@@ -5369,7 +5950,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* @name $animate#pin
* @kind function
* @description Associates the provided element with a host parent element to allow the element to be animated even if it exists
- * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the
+ * outside of the DOM structure of the AngularJS application. By doing so, any animation triggered via `$animate` can be issued on the
* element despite being outside the realm of the application or within another application. Say for example if the application
* was bootstrapped on an element that is somewhere inside of the `<body>` tag, but we wanted to allow for an element to be situated
* as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind
@@ -5417,12 +5998,78 @@ var $AnimateProvider = ['$provide', function($provide) {
* @ngdoc method
* @name $animate#cancel
* @kind function
- * @description Cancels the provided animation.
+ * @description Cancels the provided animation and applies the end state of the animation.
+ * Note that this does not cancel the underlying operation, e.g. the setting of classes or
+ * adding the element to the DOM.
+ *
+ * @param {animationRunner} animationRunner An animation runner returned by an $animate function.
*
- * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
+ * @example
+ <example module="animationExample" deps="angular-animate.js" animations="true" name="animate-cancel">
+ <file name="app.js">
+ angular.module('animationExample', ['ngAnimate']).component('cancelExample', {
+ templateUrl: 'template.html',
+ controller: function($element, $animate) {
+ this.runner = null;
+
+ this.addClass = function() {
+ this.runner = $animate.addClass($element.find('div'), 'red');
+ var ctrl = this;
+ this.runner.finally(function() {
+ ctrl.runner = null;
+ });
+ };
+
+ this.removeClass = function() {
+ this.runner = $animate.removeClass($element.find('div'), 'red');
+ var ctrl = this;
+ this.runner.finally(function() {
+ ctrl.runner = null;
+ });
+ };
+
+ this.cancel = function() {
+ $animate.cancel(this.runner);
+ };
+ }
+ });
+ </file>
+ <file name="template.html">
+ <p>
+ <button id="add" ng-click="$ctrl.addClass()">Add</button>
+ <button ng-click="$ctrl.removeClass()">Remove</button>
+ <br>
+ <button id="cancel" ng-click="$ctrl.cancel()" ng-disabled="!$ctrl.runner">Cancel</button>
+ <br>
+ <div id="target">CSS-Animated Text</div>
+ </p>
+ </file>
+ <file name="index.html">
+ <cancel-example></cancel-example>
+ </file>
+ <file name="style.css">
+ .red-add, .red-remove {
+ transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
+ }
+
+ .red,
+ .red-add.red-add-active {
+ color: #FF0000;
+ font-size: 40px;
+ }
+
+ .red-remove.red-remove-active {
+ font-size: 10px;
+ color: black;
+ }
+
+ </file>
+ </example>
*/
cancel: function(runner) {
- runner.end && runner.end();
+ if (runner.cancel) {
+ runner.cancel();
+ }
},
/**
@@ -5447,7 +6094,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
enter: function(element, parent, after, options) {
parent = parent && jqLite(parent);
@@ -5479,7 +6126,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
move: function(element, parent, after, options) {
parent = parent && jqLite(parent);
@@ -5506,7 +6153,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
leave: function(element, options) {
return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
@@ -5531,12 +6178,11 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {object=} options an optional collection of options/styles that will be applied to the element.
* The object can have the following properties:
*
- * - **addClass** - `{string}` - space-separated CSS classes to add to element
- * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} animationRunner the animation runner
*/
addClass: function(element, className, options) {
options = prepareAnimateOptions(options);
@@ -5563,10 +6209,9 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* - **addClass** - `{string}` - space-separated CSS classes to add to element
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
- * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
removeClass: function(element, className, options) {
options = prepareAnimateOptions(options);
@@ -5593,11 +6238,11 @@ var $AnimateProvider = ['$provide', function($provide) {
* The object can have the following properties:
*
* - **addClass** - `{string}` - space-separated CSS classes to add to element
- * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
setClass: function(element, add, remove, options) {
options = prepareAnimateOptions(options);
@@ -5613,7 +6258,7 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element.
* If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take
- * on the provided styles. For example, if a transition animation is set for the given classNamem, then the provided `from` and
+ * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and
* `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding
* style in `to`, the style in `from` is applied immediately, and no animation is run.
* If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate`
@@ -5644,7 +6289,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
animate: function(element, from, to, className, options) {
options = prepareAnimateOptions(options);
@@ -5659,7 +6304,7 @@ var $AnimateProvider = ['$provide', function($provide) {
}];
}];
-var $$AnimateAsyncRunFactoryProvider = function() {
+var $$AnimateAsyncRunFactoryProvider = /** @this */ function() {
this.$get = ['$$rAF', function($$rAF) {
var waitQueue = [];
@@ -5680,15 +6325,19 @@ var $$AnimateAsyncRunFactoryProvider = function() {
passed = true;
});
return function(callback) {
- passed ? callback() : waitForTick(callback);
+ if (passed) {
+ callback();
+ } else {
+ waitForTick(callback);
+ }
};
};
}];
};
-var $$AnimateRunnerFactoryProvider = function() {
- this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout',
- function($q, $sniffer, $$animateAsyncRun, $document, $timeout) {
+var $$AnimateRunnerFactoryProvider = /** @this */ function() {
+ this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout',
+ function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) {
var INITIAL_STATE = 0;
var DONE_PENDING_STATE = 1;
@@ -5740,11 +6389,7 @@ var $$AnimateRunnerFactoryProvider = function() {
this._doneCallbacks = [];
this._tick = function(fn) {
- var doc = $document[0];
-
- // the document may not be ready or attached
- // to the module for some internal tests
- if (doc && doc.hidden) {
+ if ($$isDocumentHidden()) {
timeoutTick(fn);
} else {
rafTick(fn);
@@ -5773,7 +6418,11 @@ var $$AnimateRunnerFactoryProvider = function() {
var self = this;
this.promise = $q(function(resolve, reject) {
self.done(function(status) {
- status === false ? reject() : resolve();
+ if (status === false) {
+ reject();
+ } else {
+ resolve();
+ }
});
});
}
@@ -5847,6 +6496,7 @@ var $$AnimateRunnerFactoryProvider = function() {
* @ngdoc service
* @name $animateCss
* @kind object
+ * @this
*
* @description
* This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included,
@@ -5879,7 +6529,6 @@ var $CoreAnimateCssProvider = function() {
options.from = null;
}
- /* jshint newcap: false */
var closed, runner = new $$AnimateRunner();
return {
start: run,
@@ -5917,6858 +6566,7 @@ var $CoreAnimateCssProvider = function() {
/* global stripHash: true */
-/**
- * ! This is a private undocumented service !
- *
- * @name $browser
- * @requires $log
- * @description
- * This object has two goals:
- *
- * - hide all the global state in the browser caused by the window object
- * - abstract away all the browser specific features and inconsistencies
- *
- * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser`
- * service, which can be used for convenient testing of the application without the interaction with
- * the real browser apis.
- */
-/**
- * @param {object} window The global window object.
- * @param {object} document jQuery wrapped document.
- * @param {object} $log window.console or an object with the same interface.
- * @param {object} $sniffer $sniffer service
- */
-function Browser(window, document, $log, $sniffer) {
- var self = this,
- location = window.location,
- history = window.history,
- setTimeout = window.setTimeout,
- clearTimeout = window.clearTimeout,
- pendingDeferIds = {};
-
- self.isMock = false;
-
- var outstandingRequestCount = 0;
- var outstandingRequestCallbacks = [];
-
- // TODO(vojta): remove this temporary api
- self.$$completeOutstandingRequest = completeOutstandingRequest;
- self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
-
- /**
- * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks`
- * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
- */
- function completeOutstandingRequest(fn) {
- try {
- fn.apply(null, sliceArgs(arguments, 1));
- } finally {
- outstandingRequestCount--;
- if (outstandingRequestCount === 0) {
- while (outstandingRequestCallbacks.length) {
- try {
- outstandingRequestCallbacks.pop()();
- } catch (e) {
- $log.error(e);
- }
- }
- }
- }
- }
-
- function getHash(url) {
- var index = url.indexOf('#');
- return index === -1 ? '' : url.substr(index);
- }
-
- /**
- * @private
- * Note: this method is used only by scenario runner
- * TODO(vojta): prefix this method with $$ ?
- * @param {function()} callback Function that will be called when no outstanding request
- */
- self.notifyWhenNoOutstandingRequests = function(callback) {
- if (outstandingRequestCount === 0) {
- callback();
- } else {
- outstandingRequestCallbacks.push(callback);
- }
- };
-
- //////////////////////////////////////////////////////////////
- // URL API
- //////////////////////////////////////////////////////////////
-
- var cachedState, lastHistoryState,
- lastBrowserUrl = location.href,
- baseElement = document.find('base'),
- pendingLocation = null,
- getCurrentState = !$sniffer.history ? noop : function getCurrentState() {
- try {
- return history.state;
- } catch (e) {
- // MSIE can reportedly throw when there is no state (UNCONFIRMED).
- }
- };
-
- cacheState();
- lastHistoryState = cachedState;
-
- /**
- * @name $browser#url
- *
- * @description
- * GETTER:
- * Without any argument, this method just returns current value of location.href.
- *
- * SETTER:
- * With at least one argument, this method sets url to new value.
- * If html5 history api supported, pushState/replaceState is used, otherwise
- * location.href/location.replace is used.
- * Returns its own instance to allow chaining
- *
- * 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 {object=} state object to use with pushState/replaceState
- */
- 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) {
- var sameState = lastHistoryState === state;
-
- // 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 || sameState)) {
- return self;
- }
- var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
- lastBrowserUrl = url;
- lastHistoryState = state;
- // 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 || !sameState)) {
- history[replace ? 'replaceState' : 'pushState'](state, '', url);
- cacheState();
- // Do the assignment again so that those two variables are referentially identical.
- lastHistoryState = cachedState;
- } else {
- if (!sameBase) {
- pendingLocation = url;
- }
- if (replace) {
- location.replace(url);
- } else if (!sameBase) {
- location.href = url;
- } else {
- location.hash = getHash(url);
- }
- if (location.href !== url) {
- pendingLocation = url;
- }
- }
- if (pendingLocation) {
- pendingLocation = url;
- }
- return self;
- // getter
- } else {
- // - pendingLocation is needed as browsers don't allow to read out
- // the new location.href if a reload happened or if there is a bug like in iOS 9 (see
- // https://openradar.appspot.com/22186109).
- // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
- return pendingLocation || 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 cachedState;
- };
-
- var urlChangeListeners = [],
- urlChangeInit = false;
-
- function cacheStateAndFireUrlChange() {
- pendingLocation = null;
- cacheState();
- fireUrlChange();
- }
-
- // This variable should be used *only* inside the cacheState function.
- var lastCachedState = null;
- function cacheState() {
- // This should be the only place in $browser where `history.state` is read.
- cachedState = getCurrentState();
- cachedState = isUndefined(cachedState) ? null : cachedState;
-
- // Prevent callbacks fo fire twice if both hashchange & popstate were fired.
- if (equals(cachedState, lastCachedState)) {
- cachedState = lastCachedState;
- }
- lastCachedState = cachedState;
- }
-
- function fireUrlChange() {
- if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
- return;
- }
-
- lastBrowserUrl = self.url();
- lastHistoryState = cachedState;
- forEach(urlChangeListeners, function(listener) {
- listener(self.url(), cachedState);
- });
- }
-
- /**
- * @name $browser#onUrlChange
- *
- * @description
- * Register callback function that will be called, when url changes.
- *
- * It's only called when the url is changed from outside of angular:
- * - user types different url into address bar
- * - user clicks on history (forward/back) button
- * - user clicks on a link
- *
- * It's not called when url is changed by $browser.url() method
- *
- * The listener gets called with new url as parameter.
- *
- * NOTE: this api is intended for use only by the $location service. Please use the
- * {@link ng.$location $location service} to monitor url changes in angular apps.
- *
- * @param {function(string)} listener Listener function to be called when url changes.
- * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous.
- */
- self.onUrlChange = function(callback) {
- // TODO(vojta): refactor to use node's syntax for events
- if (!urlChangeInit) {
- // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera)
- // don't fire popstate when user change the address bar and don't fire hashchange when url
- // changed by push/replaceState
-
- // html5 history api - popstate event
- if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange);
- // hashchange event
- jqLite(window).on('hashchange', cacheStateAndFireUrlChange);
-
- urlChangeInit = true;
- }
-
- urlChangeListeners.push(callback);
- return callback;
- };
-
- /**
- * @private
- * Remove popstate and hashchange handler from window.
- *
- * NOTE: this api is intended for use only by $rootScope.
- */
- self.$$applicationDestroyed = function() {
- jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange);
- };
-
- /**
- * Checks whether the url has changed outside of Angular.
- * Needs to be exported to be able to check for changes that have been done in sync,
- * as hashchange/popstate events fire in async.
- */
- self.$$checkUrlChange = fireUrlChange;
-
- //////////////////////////////////////////////////////////////
- // Misc API
- //////////////////////////////////////////////////////////////
-
- /**
- * @name $browser#baseHref
- *
- * @description
- * Returns current <base href>
- * (always relative - without domain)
- *
- * @returns {string} The current base href
- */
- self.baseHref = function() {
- var href = baseElement.attr('href');
- return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : '';
- };
-
- /**
- * @name $browser#defer
- * @param {function()} fn A function, who's execution should be deferred.
- * @param {number=} [delay=0] of milliseconds to defer the function execution.
- * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`.
- *
- * @description
- * Executes a fn asynchronously via `setTimeout(fn, delay)`.
- *
- * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using
- * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed
- * via `$browser.defer.flush()`.
- *
- */
- self.defer = function(fn, delay) {
- var timeoutId;
- outstandingRequestCount++;
- timeoutId = setTimeout(function() {
- delete pendingDeferIds[timeoutId];
- completeOutstandingRequest(fn);
- }, delay || 0);
- pendingDeferIds[timeoutId] = true;
- return timeoutId;
- };
-
-
- /**
- * @name $browser#defer.cancel
- *
- * @description
- * Cancels a deferred task identified with `deferId`.
- *
- * @param {*} deferId Token returned by the `$browser.defer` function.
- * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
- * canceled.
- */
- self.defer.cancel = function(deferId) {
- if (pendingDeferIds[deferId]) {
- delete pendingDeferIds[deferId];
- clearTimeout(deferId);
- completeOutstandingRequest(noop);
- return true;
- }
- return false;
- };
-
-}
-
-function $BrowserProvider() {
- this.$get = ['$window', '$log', '$sniffer', '$document',
- function($window, $log, $sniffer, $document) {
- return new Browser($window, $document, $log, $sniffer);
- }];
-}
-
-/**
- * @ngdoc service
- * @name $cacheFactory
- *
- * @description
- * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to
- * them.
- *
- * ```js
- *
- * var cache = $cacheFactory('cacheId');
- * expect($cacheFactory.get('cacheId')).toBe(cache);
- * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined();
- *
- * cache.put("key", "value");
- * cache.put("another key", "another value");
- *
- * // We've specified no options on creation
- * expect(cache.info()).toEqual({id: 'cacheId', size: 2});
- *
- * ```
- *
- *
- * @param {string} cacheId Name or id of the newly created cache.
- * @param {object=} options Options object that specifies the cache behavior. Properties:
- *
- * - `{number=}` `capacity` — turns the cache into LRU cache.
- *
- * @returns {object} Newly created cache object with the following set of methods:
- *
- * - `{object}` `info()` — Returns id, size, and options of cache.
- * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns
- * it.
- * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss.
- * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache.
- * - `{void}` `removeAll()` — Removes all cached values.
- * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory.
- *
- * @example
- <example module="cacheExampleApp">
- <file name="index.html">
- <div ng-controller="CacheController">
- <input ng-model="newCacheKey" placeholder="Key">
- <input ng-model="newCacheValue" placeholder="Value">
- <button ng-click="put(newCacheKey, newCacheValue)">Cache</button>
-
- <p ng-if="keys.length">Cached Values</p>
- <div ng-repeat="key in keys">
- <span ng-bind="key"></span>
- <span>: </span>
- <b ng-bind="cache.get(key)"></b>
- </div>
-
- <p>Cache Info</p>
- <div ng-repeat="(key, value) in cache.info()">
- <span ng-bind="key"></span>
- <span>: </span>
- <b ng-bind="value"></b>
- </div>
- </div>
- </file>
- <file name="script.js">
- angular.module('cacheExampleApp', []).
- controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) {
- $scope.keys = [];
- $scope.cache = $cacheFactory('cacheId');
- $scope.put = function(key, value) {
- if (angular.isUndefined($scope.cache.get(key))) {
- $scope.keys.push(key);
- }
- $scope.cache.put(key, angular.isUndefined(value) ? null : value);
- };
- }]);
- </file>
- <file name="style.css">
- p {
- margin: 10px 0 3px;
- }
- </file>
- </example>
- */
-function $CacheFactoryProvider() {
-
- this.$get = function() {
- var caches = {};
-
- function cacheFactory(cacheId, options) {
- if (cacheId in caches) {
- throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId);
- }
-
- var size = 0,
- stats = extend({}, options, {id: cacheId}),
- data = createMap(),
- capacity = (options && options.capacity) || Number.MAX_VALUE,
- lruHash = createMap(),
- freshEnd = null,
- staleEnd = null;
-
- /**
- * @ngdoc type
- * @name $cacheFactory.Cache
- *
- * @description
- * A cache object used to store and retrieve data, primarily used by
- * {@link $http $http} and the {@link ng.directive:script script} directive to cache
- * templates and other data.
- *
- * ```js
- * angular.module('superCache')
- * .factory('superCache', ['$cacheFactory', function($cacheFactory) {
- * return $cacheFactory('super-cache');
- * }]);
- * ```
- *
- * Example test:
- *
- * ```js
- * it('should behave like a cache', inject(function(superCache) {
- * superCache.put('key', 'value');
- * superCache.put('another key', 'another value');
- *
- * expect(superCache.info()).toEqual({
- * id: 'super-cache',
- * size: 2
- * });
- *
- * superCache.remove('another key');
- * expect(superCache.get('another key')).toBeUndefined();
- *
- * superCache.removeAll();
- * expect(superCache.info()).toEqual({
- * id: 'super-cache',
- * size: 0
- * });
- * }));
- * ```
- */
- return caches[cacheId] = {
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#put
- * @kind function
- *
- * @description
- * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be
- * retrieved later, and incrementing the size of the cache if the key was not already
- * present in the cache. If behaving like an LRU cache, it will also remove stale
- * entries from the set.
- *
- * It will not insert undefined values into the cache.
- *
- * @param {string} key the key under which the cached data is stored.
- * @param {*} value the value to store alongside the key. If it is undefined, the key
- * will not be stored.
- * @returns {*} the value stored.
- */
- put: function(key, value) {
- if (isUndefined(value)) return;
- if (capacity < Number.MAX_VALUE) {
- var lruEntry = lruHash[key] || (lruHash[key] = {key: key});
-
- refresh(lruEntry);
- }
-
- if (!(key in data)) size++;
- data[key] = value;
-
- if (size > capacity) {
- this.remove(staleEnd.key);
- }
-
- return value;
- },
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#get
- * @kind function
- *
- * @description
- * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object.
- *
- * @param {string} key the key of the data to be retrieved
- * @returns {*} the value stored.
- */
- get: function(key) {
- if (capacity < Number.MAX_VALUE) {
- var lruEntry = lruHash[key];
-
- if (!lruEntry) return;
-
- refresh(lruEntry);
- }
-
- return data[key];
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#remove
- * @kind function
- *
- * @description
- * Removes an entry from the {@link $cacheFactory.Cache Cache} object.
- *
- * @param {string} key the key of the entry to be removed
- */
- remove: function(key) {
- if (capacity < Number.MAX_VALUE) {
- var lruEntry = lruHash[key];
-
- if (!lruEntry) return;
-
- if (lruEntry == freshEnd) freshEnd = lruEntry.p;
- if (lruEntry == staleEnd) staleEnd = lruEntry.n;
- link(lruEntry.n,lruEntry.p);
-
- delete lruHash[key];
- }
-
- if (!(key in data)) return;
-
- delete data[key];
- size--;
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#removeAll
- * @kind function
- *
- * @description
- * Clears the cache object of any entries.
- */
- removeAll: function() {
- data = createMap();
- size = 0;
- lruHash = createMap();
- freshEnd = staleEnd = null;
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#destroy
- * @kind function
- *
- * @description
- * Destroys the {@link $cacheFactory.Cache Cache} object entirely,
- * removing it from the {@link $cacheFactory $cacheFactory} set.
- */
- destroy: function() {
- data = null;
- stats = null;
- lruHash = null;
- delete caches[cacheId];
- },
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory.Cache#info
- * @kind function
- *
- * @description
- * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}.
- *
- * @returns {object} an object with the following properties:
- * <ul>
- * <li>**id**: the id of the cache instance</li>
- * <li>**size**: the number of entries kept in the cache instance</li>
- * <li>**...**: any additional properties from the options object when creating the
- * cache.</li>
- * </ul>
- */
- info: function() {
- return extend({}, stats, {size: size});
- }
- };
-
-
- /**
- * makes the `entry` the freshEnd of the LRU linked list
- */
- function refresh(entry) {
- if (entry != freshEnd) {
- if (!staleEnd) {
- staleEnd = entry;
- } else if (staleEnd == entry) {
- staleEnd = entry.n;
- }
-
- link(entry.n, entry.p);
- link(entry, freshEnd);
- freshEnd = entry;
- freshEnd.n = null;
- }
- }
-
-
- /**
- * bidirectionally links two entries of the LRU linked list
- */
- function link(nextEntry, prevEntry) {
- if (nextEntry != prevEntry) {
- if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify
- if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify
- }
- }
- }
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory#info
- *
- * @description
- * Get information about all the caches that have been created
- *
- * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info`
- */
- cacheFactory.info = function() {
- var info = {};
- forEach(caches, function(cache, cacheId) {
- info[cacheId] = cache.info();
- });
- return info;
- };
-
-
- /**
- * @ngdoc method
- * @name $cacheFactory#get
- *
- * @description
- * Get access to a cache object by the `cacheId` used when it was created.
- *
- * @param {string} cacheId Name or id of a cache to access.
- * @returns {object} Cache object identified by the cacheId or undefined if no such cache.
- */
- cacheFactory.get = function(cacheId) {
- return caches[cacheId];
- };
-
-
- return cacheFactory;
- };
-}
-
-/**
- * @ngdoc service
- * @name $templateCache
- *
- * @description
- * The first time a template is used, it is loaded in the template cache for quick retrieval. You
- * can load templates directly into the cache in a `script` tag, or by consuming the
- * `$templateCache` service directly.
- *
- * Adding via the `script` tag:
- *
- * ```html
- * <script type="text/ng-template" id="templateId.html">
- * <p>This is the content of the template</p>
- * </script>
- * ```
- *
- * **Note:** the `script` tag containing the template does not need to be included in the `head` of
- * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE,
- * element with ng-app attribute), otherwise the template will be ignored.
- *
- * Adding via the `$templateCache` service:
- *
- * ```js
- * var myApp = angular.module('myApp', []);
- * myApp.run(function($templateCache) {
- * $templateCache.put('templateId.html', 'This is the content of the template');
- * });
- * ```
- *
- * To retrieve the template later, simply use it in your HTML:
- * ```html
- * <div ng-include=" 'templateId.html' "></div>
- * ```
- *
- * or get it via Javascript:
- * ```js
- * $templateCache.get('templateId.html')
- * ```
- *
- * See {@link ng.$cacheFactory $cacheFactory}.
- *
- */
-function $TemplateCacheProvider() {
- this.$get = ['$cacheFactory', function($cacheFactory) {
- return $cacheFactory('templates');
- }];
-}
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Any commits to this file should be reviewed with security in mind. *
- * Changes to this file can potentially create security vulnerabilities. *
- * An approval from 2 Core members with history of modifying *
- * this file is required. *
- * *
- * Does the change somehow allow for arbitrary javascript to be executed? *
- * Or allows for someone to change the prototype of built-in objects? *
- * Or gives undesired access to variables likes document or window? *
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
- *
- * DOM-related variables:
- *
- * - "node" - DOM Node
- * - "element" - DOM Element or Node
- * - "$node" or "$element" - jqLite-wrapped node or element
- *
- *
- * Compiler related stuff:
- *
- * - "linkFn" - linking fn of a single directive
- * - "nodeLinkFn" - function that aggregates all linking fns for a particular node
- * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node
- * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList)
- */
-
-
-/**
- * @ngdoc service
- * @name $compile
- * @kind function
- *
- * @description
- * Compiles an HTML string or DOM into a template and produces a template function, which
- * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together.
- *
- * The compilation is a process of walking the DOM tree and matching DOM elements to
- * {@link ng.$compileProvider#directive directives}.
- *
- * <div class="alert alert-warning">
- * **Note:** This document is an in-depth reference of all directive options.
- * For a gentle introduction to directives with examples of common use cases,
- * see the {@link guide/directive directive guide}.
- * </div>
- *
- * ## Comprehensive Directive API
- *
- * There are many different options for a directive.
- *
- * The difference resides in the return value of the factory function.
- * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)}
- * that defines the directive properties, or just the `postLink` function (all other properties will have
- * the default values).
- *
- * <div class="alert alert-success">
- * **Best Practice:** It's recommended to use the "directive definition object" form.
- * </div>
- *
- * Here's an example directive declared with a Directive Definition Object:
- *
- * ```js
- * var myModule = angular.module(...);
- *
- * myModule.directive('directiveName', function factory(injectables) {
- * var directiveDefinitionObject = {
- * priority: 0,
- * template: '<div></div>', // or // function(tElement, tAttrs) { ... },
- * // or
- * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
- * transclude: false,
- * restrict: 'A',
- * templateNamespace: 'html',
- * scope: false,
- * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
- * controllerAs: 'stringIdentifier',
- * bindToController: false,
- * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
- * compile: function compile(tElement, tAttrs, transclude) {
- * return {
- * pre: function preLink(scope, iElement, iAttrs, controller) { ... },
- * post: function postLink(scope, iElement, iAttrs, controller) { ... }
- * }
- * // or
- * // return function postLink( ... ) { ... }
- * },
- * // or
- * // link: {
- * // pre: function preLink(scope, iElement, iAttrs, controller) { ... },
- * // post: function postLink(scope, iElement, iAttrs, controller) { ... }
- * // }
- * // or
- * // link: function postLink( ... ) { ... }
- * };
- * return directiveDefinitionObject;
- * });
- * ```
- *
- * <div class="alert alert-warning">
- * **Note:** Any unspecified options will use the default value. You can see the default values below.
- * </div>
- *
- * Therefore the above can be simplified as:
- *
- * ```js
- * var myModule = angular.module(...);
- *
- * myModule.directive('directiveName', function factory(injectables) {
- * var directiveDefinitionObject = {
- * link: function postLink(scope, iElement, iAttrs) { ... }
- * };
- * return directiveDefinitionObject;
- * // or
- * // return function postLink(scope, iElement, iAttrs) { ... }
- * });
- * ```
- *
- * ### Life-cycle hooks
- * Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the
- * directive:
- * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
- * had their bindings initialized (and before the pre &amp; post linking functions for the directives on
- * this element). This is a good place to put initialization code for your controller.
- * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
- * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
- * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a
- * component such as cloning the bound value to prevent accidental mutation of the outer value.
- * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on
- * changes. Any actions that you wish to take in response to the changes that you detect must be
- * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook
- * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not
- * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments;
- * if detecting changes, you must store the previous value(s) for comparison to the current values.
- * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
- * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
- * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
- * components will have their `$onDestroy()` hook called before child components.
- * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
- * function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
- * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
- * they are waiting for their template to load asynchronously and their own compilation and linking has been
- * suspended until that occurs.
- *
- * #### Comparison with Angular 2 life-cycle hooks
- * Angular 2 also uses life-cycle hooks for its components. While the Angular 1 life-cycle hooks are similar there are
- * some differences that you should be aware of, especially when it comes to moving your code from Angular 1 to Angular 2:
- *
- * * Angular 1 hooks are prefixed with `$`, such as `$onInit`. Angular 2 hooks are prefixed with `ng`, such as `ngOnInit`.
- * * Angular 1 hooks can be defined on the controller prototype or added to the controller inside its constructor.
- * In Angular 2 you can only define hooks on the prototype of the Component class.
- * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in Angular 1 than you would to
- * `ngDoCheck` in Angular 2
- * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be
- * propagated throughout the application.
- * Angular 2 does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an
- * error or do nothing depending upon the state of `enableProdMode()`.
- *
- * #### Life-cycle hook examples
- *
- * This example shows how you can check for mutations to a Date object even though the identity of the object
- * has not changed.
- *
- * <example name="doCheckDateExample" module="do-check-module">
- * <file name="app.js">
- * angular.module('do-check-module', [])
- * .component('app', {
- * template:
- * 'Month: <input ng-model="$ctrl.month" ng-change="$ctrl.updateDate()">' +
- * 'Date: {{ $ctrl.date }}' +
- * '<test date="$ctrl.date"></test>',
- * controller: function() {
- * this.date = new Date();
- * this.month = this.date.getMonth();
- * this.updateDate = function() {
- * this.date.setMonth(this.month);
- * };
- * }
- * })
- * .component('test', {
- * bindings: { date: '<' },
- * template:
- * '<pre>{{ $ctrl.log | json }}</pre>',
- * controller: function() {
- * var previousValue;
- * this.log = [];
- * this.$doCheck = function() {
- * var currentValue = this.date && this.date.valueOf();
- * if (previousValue !== currentValue) {
- * this.log.push('doCheck: date mutated: ' + this.date);
- * previousValue = currentValue;
- * }
- * };
- * }
- * });
- * </file>
- * <file name="index.html">
- * <app></app>
- * </file>
- * </example>
- *
- * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the
- * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large
- * arrays or objects can have a negative impact on your application performance)
- *
- * <example name="doCheckArrayExample" module="do-check-module">
- * <file name="index.html">
- * <div ng-init="items = []">
- * <button ng-click="items.push(items.length)">Add Item</button>
- * <button ng-click="items = []">Reset Items</button>
- * <pre>{{ items }}</pre>
- * <test items="items"></test>
- * </div>
- * </file>
- * <file name="app.js">
- * angular.module('do-check-module', [])
- * .component('test', {
- * bindings: { items: '<' },
- * template:
- * '<pre>{{ $ctrl.log | json }}</pre>',
- * controller: function() {
- * this.log = [];
- *
- * this.$doCheck = function() {
- * if (this.items_ref !== this.items) {
- * this.log.push('doCheck: items changed');
- * this.items_ref = this.items;
- * }
- * if (!angular.equals(this.items_clone, this.items)) {
- * this.log.push('doCheck: items mutated');
- * this.items_clone = angular.copy(this.items);
- * }
- * };
- * }
- * });
- * </file>
- * </example>
- *
- *
- * ### Directive Definition Object
- *
- * The directive definition object provides instructions to the {@link ng.$compile
- * compiler}. The attributes are:
- *
- * #### `multiElement`
- * When this property is set to true, the HTML compiler will collect DOM nodes between
- * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
- * together as the directive elements. It is recommended that this feature be used on directives
- * which are not strictly behavioral (such as {@link ngClick}), and which
- * do not manipulate or replace child nodes (such as {@link ngInclude}).
- *
- * #### `priority`
- * When there are multiple directives defined on a single DOM element, sometimes it
- * is necessary to specify the order in which the directives are applied. The `priority` is used
- * to sort the directives before their `compile` functions get called. Priority is defined as a
- * number. Directives with greater numerical `priority` are compiled first. Pre-link functions
- * are also run in priority order, but post-link functions are run in reverse order. The order
- * of directives with the same priority is undefined. The default priority is `0`.
- *
- * #### `terminal`
- * If set to true then the current `priority` will be the last set of directives
- * which will execute (any directives at the current priority will still execute
- * as the order of execution on same `priority` is undefined). Note that expressions
- * and other directives used in the directive's template will also be excluded from execution.
- *
- * #### `scope`
- * The scope property can be `true`, an object or a falsy value:
- *
- * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope.
- *
- * * **`true`:** A new child scope that prototypically inherits from its parent will be created for
- * the directive's element. If multiple directives on the same element request a new scope,
- * only one new scope is created. The new scope rule does not apply for the root of the template
- * since the root of the template always gets a new scope.
- *
- * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The
- * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent
- * scope. This is useful when creating reusable components, which should not accidentally read or modify
- * data in the parent scope.
- *
- * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the
- * directive's element. These local properties are useful for aliasing values for templates. The keys in
- * the object hash map to the name of the property on the isolate scope; the values define how the property
- * is bound to the parent scope, via matching attributes on the directive's element:
- *
- * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is
- * always a string since DOM attributes are strings. If no `attr` name is specified then the
- * attribute name is assumed to be the same as the local name. Given `<my-component
- * my-attr="hello {{name}}">` and the isolate scope definition `scope: { localName:'@myAttr' }`,
- * the directive's scope property `localName` will reflect the interpolated value of `hello
- * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's
- * scope. The `name` is read from the parent scope (not the directive's scope).
- *
- * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression
- * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope.
- * If no `attr` name is specified then the attribute name is assumed to be the same as the local
- * name. Given `<my-component my-attr="parentModel">` and the isolate scope definition `scope: {
- * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the
- * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in
- * `localModel` and vice versa. Optional attributes should be marked as such with a question mark:
- * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't
- * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`})
- * will be thrown upon discovering changes to the local value, since it will be impossible to sync
- * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`}
- * method is used for tracking changes, and the equality check is based on object identity.
- * However, if an object literal or an array literal is passed as the binding expression, the
- * equality check is done by value (using the {@link angular.equals} function). It's also possible
- * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection
- * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional).
- *
- * * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an
- * expression passed via the attribute `attr`. The expression is evaluated in the context of the
- * parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the
- * local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`.
- *
- * For example, given `<my-component my-attr="parentModel">` and directive definition of
- * `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the
- * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
- * in `localModel`, but changes in `localModel` will not reflect in `parentModel`. There are however
- * two caveats:
- * 1. one-way binding does not copy the value from the parent to the isolate scope, it simply
- * sets the same value. That means if your bound value is an object, changes to its properties
- * in the isolated scope will be reflected in the parent scope (because both reference the same object).
- * 2. one-way binding watches changes to the **identity** of the parent value. That means the
- * {@link ng.$rootScope.Scope#$watch `$watch`} on the parent value only fires if the reference
- * to the value has changed. In most cases, this should not be of concern, but can be important
- * to know if you one-way bind to an object, and then replace that object in the isolated scope.
- * If you now change a property of the object in your parent scope, the change will not be
- * propagated to the isolated scope, because the identity of the object on the parent scope
- * has not changed. Instead you must assign a new object.
- *
- * One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings
- * back to the parent. However, it does not make this completely impossible.
- *
- * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If
- * no `attr` name is specified then the attribute name is assumed to be the same as the local name.
- * Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: {
- * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for
- * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope
- * via an expression to the parent scope. This can be done by passing a map of local variable names
- * and values into the expression wrapper fn. For example, if the expression is `increment(amount)`
- * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`.
- *
- * In general it's possible to apply more than one directive to one element, but there might be limitations
- * depending on the type of scope required by the directives. The following points will help explain these limitations.
- * For simplicity only two directives are taken into account, but it is also applicable for several directives:
- *
- * * **no scope** + **no scope** => Two directives which don't require their own scope will use their parent's scope
- * * **child scope** + **no scope** => Both directives will share one single child scope
- * * **child scope** + **child scope** => Both directives will share one single child scope
- * * **isolated scope** + **no scope** => The isolated directive will use it's own created isolated scope. The other directive will use
- * its parent's scope
- * * **isolated scope** + **child scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives cannot
- * be applied to the same element.
- * * **isolated scope** + **isolated scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives
- * cannot be applied to the same element.
- *
- *
- * #### `bindToController`
- * This property is used to bind scope properties directly to the controller. It can be either
- * `true` or an object hash with the same format as the `scope` property. Additionally, a controller
- * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller
- * definition: `controller: 'myCtrl as myAlias'`.
- *
- * When an isolate scope is used for a directive (see above), `bindToController: true` will
- * allow a component to have its properties bound to the controller, rather than to scope.
- *
- * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller
- * properties. You can access these bindings once they have been initialized by providing a controller method called
- * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings
- * initialized.
- *
- * <div class="alert alert-warning">
- * **Deprecation warning:** although bindings for non-ES6 class controllers are currently
- * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization
- * code that relies upon bindings inside a `$onInit` method on the controller, instead.
- * </div>
- *
- * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property.
- * This will set up the scope bindings to the controller directly. Note that `scope` can still be used
- * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate
- * scope (useful for component directives).
- *
- * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`.
- *
- *
- * #### `controller`
- * Controller constructor function. The controller is instantiated before the
- * pre-linking phase and can be accessed by other directives (see
- * `require` attribute). This allows the directives to communicate with each other and augment
- * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals:
- *
- * * `$scope` - Current scope associated with the element
- * * `$element` - Current element
- * * `$attrs` - Current attributes object for the element
- * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
- * `function([scope], cloneLinkingFn, futureParentElement, slotName)`:
- * * `scope`: (optional) override the scope.
- * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content.
- * * `futureParentElement` (optional):
- * * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
- * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
- * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
- * and when the `cloneLinkinFn` is passed,
- * as those elements need to created and cloned in a special way when they are defined outside their
- * usual containers (e.g. like `<svg>`).
- * * See also the `directive.templateNamespace` property.
- * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`)
- * then the default translusion is provided.
- * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns
- * `true` if the specified slot contains content (i.e. one or more DOM nodes).
- *
- * #### `require`
- * Require another directive and inject its controller as the fourth argument to the linking function. The
- * `require` property can be a string, an array or an object:
- * * a **string** containing the name of the directive to pass to the linking function
- * * an **array** containing the names of directives to pass to the linking function. The argument passed to the
- * linking function will be an array of controllers in the same order as the names in the `require` property
- * * an **object** whose property values are the names of the directives to pass to the linking function. The argument
- * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding
- * controllers.
- *
- * If the `require` property is an object and `bindToController` is truthy, then the required controllers are
- * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers
- * have been constructed but before `$onInit` is called.
- * If the name of the required controller is the same as the local name (the key), the name can be
- * omitted. For example, `{parentDir: '^^'}` is equivalent to `{parentDir: '^^parentDir'}`.
- * See the {@link $compileProvider#component} helper for an example of how this can be used.
- * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is
- * raised (unless no link function is specified and the required controllers are not being bound to the directive
- * controller, in which case error checking is skipped). 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`
- * Identifier name for a reference to the controller in the directive's scope.
- * This allows the controller to be referenced from the directive template. This is especially
- * useful when a directive is used as component, i.e. with an `isolate` scope. It's also possible
- * to use it in a directive without an `isolate` / `new` scope, but you need to be aware that the
- * `controllerAs` reference might overwrite a property that already exists on the parent scope.
- *
- *
- * #### `restrict`
- * String of subset of `EACM` which restricts the directive to a specific directive
- * declaration style. If omitted, the defaults (elements and attributes) are used.
- *
- * * `E` - Element name (default): `<my-directive></my-directive>`
- * * `A` - Attribute (default): `<div my-directive="exp"></div>`
- * * `C` - Class: `<div class="my-directive: exp;"></div>`
- * * `M` - Comment: `<!-- directive: my-directive exp -->`
- *
- *
- * #### `templateNamespace`
- * String representing the document type used by the markup in the template.
- * AngularJS needs this information as those elements need to be created and cloned
- * in a special way when they are defined outside their usual containers like `<svg>` and `<math>`.
- *
- * * `html` - All root nodes in the template are HTML. Root nodes may also be
- * top-level elements such as `<svg>` or `<math>`.
- * * `svg` - The root nodes in the template are SVG elements (excluding `<math>`).
- * * `math` - The root nodes in the template are MathML elements (excluding `<svg>`).
- *
- * If no `templateNamespace` is specified, then the namespace is considered to be `html`.
- *
- * #### `template`
- * HTML markup that may:
- * * Replace the contents of the directive's element (default).
- * * Replace the directive's element itself (if `replace` is true - DEPRECATED).
- * * Wrap the contents of the directive's element (if `transclude` is true).
- *
- * Value may be:
- *
- * * A string. For example `<div red-on-hover>{{delete_str}}</div>`.
- * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile`
- * function api below) and returns a string value.
- *
- *
- * #### `templateUrl`
- * This is similar to `template` but the template is loaded from the specified URL, asynchronously.
- *
- * Because template loading is asynchronous the compiler will suspend compilation of directives on that element
- * for later when the template has been resolved. In the meantime it will continue to compile and link
- * sibling and parent elements as though this element had not contained any directives.
- *
- * The compiler does not suspend the entire compilation to wait for templates to be loaded because this
- * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the
- * case when only one deeply nested directive has `templateUrl`.
- *
- * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache}
- *
- * You can specify `templateUrl` as a string representing the URL or as a function which takes two
- * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns
- * a string value representing the url. In either case, the template URL is passed through {@link
- * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}.
- *
- *
- * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0)
- * specify what the template should replace. Defaults to `false`.
- *
- * * `true` - the template will replace the directive's element.
- * * `false` - the template will replace the contents of the directive's element.
- *
- * The replacement process migrates all of the attributes / classes from the old element to the new
- * one. See the {@link guide/directive#template-expanding-directive
- * Directives Guide} for an example.
- *
- * 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`
- * 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.
- *
- *
- * #### `compile`
- *
- * ```js
- * function compile(tElement, tAttrs, transclude) { ... }
- * ```
- *
- * The compile function deals with transforming the template DOM. Since most directives do not do
- * template transformation, it is not used often. The compile function takes the following arguments:
- *
- * * `tElement` - template element - The element where the directive has been declared. It is
- * safe to do template transformation on the element and child elements only.
- *
- * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared
- * between all directive compile functions.
- *
- * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)`
- *
- * <div class="alert alert-warning">
- * **Note:** The template instance and the link instance may be different objects if the template has
- * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that
- * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration
- * should be done in a linking function rather than in a compile function.
- * </div>
-
- * <div class="alert alert-warning">
- * **Note:** The compile function cannot handle directives that recursively use themselves in their
- * own templates or compile functions. Compiling these directives results in an infinite loop and
- * stack overflow errors.
- *
- * This can be avoided by manually using $compile in the postLink function to imperatively compile
- * a directive's template instead of relying on automatic template compilation via `template` or
- * `templateUrl` declaration or manual compilation inside the compile function.
- * </div>
- *
- * <div class="alert alert-danger">
- * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it
- * e.g. does not know about the right outer scope. Please use the transclude function that is passed
- * to the link function instead.
- * </div>
-
- * A compile function can have a return value which can be either a function or an object.
- *
- * * returning a (post-link) function - is equivalent to registering the linking function via the
- * `link` property of the config object when the compile function is empty.
- *
- * * returning an object with function(s) registered via `pre` and `post` properties - allows you to
- * control when a linking function should be called during the linking phase. See info about
- * pre-linking and post-linking functions below.
- *
- *
- * #### `link`
- * This property is used only if the `compile` property is not defined.
- *
- * ```js
- * function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
- * ```
- *
- * The link function is responsible for registering DOM listeners as well as updating the DOM. It is
- * executed after the template has been cloned. This is where most of the directive logic will be
- * put.
- *
- * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the
- * directive for registering {@link ng.$rootScope.Scope#$watch watches}.
- *
- * * `iElement` - instance element - The element where the directive is to be used. It is safe to
- * manipulate the children of the element only in `postLink` function since the children have
- * already been linked.
- *
- * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared
- * between all directive linking functions.
- *
- * * `controller` - the directive's required controller instance(s) - Instances are shared
- * among all directives, which allows the directives to use the controllers as a communication
- * channel. The exact value depends on the directive's `require` property:
- * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one
- * * `string`: the controller instance
- * * `array`: array of controller instances
- *
- * If a required controller cannot be found, and it is optional, the instance is `null`,
- * otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown.
- *
- * Note that you can also require the directive's own controller - it will be made available like
- * any other controller.
- *
- * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
- * This is the same as the `$transclude` parameter of directive controllers,
- * see {@link ng.$compile#-controller- the controller section for details}.
- * `function([scope], cloneLinkingFn, futureParentElement)`.
- *
- * #### Pre-linking function
- *
- * Executed before the child elements are linked. Not safe to do DOM transformation since the
- * compiler linking function will fail to locate the correct elements for linking.
- *
- * #### Post-linking function
- *
- * Executed after the child elements are linked.
- *
- * Note that child elements that contain `templateUrl` directives will not have been compiled
- * and linked since they are waiting for their template to load asynchronously and their own
- * 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.
- *
- *
- * ### Transclusion
- *
- * Transclusion is the process of extracting a collection of DOM elements 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>
- *
- * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
- * directive's element, the entire element or multiple parts of the element contents:
- *
- * * `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.
- * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
- *
- * **Mult-slot transclusion** is declared by providing an object for the `transclude` property.
- *
- * This object is a map where the keys are the name of the slot to fill and the value is an element selector
- * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`)
- * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc).
- *
- * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
- *
- * If the element selector is prefixed with a `?` then that slot is optional.
- *
- * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `<my-custom-element>` elements to
- * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive.
- *
- * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements
- * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call
- * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and
- * injectable into the directive's controller.
- *
- *
- * #### 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 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 transclude 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 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 transcluded 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.
- *
- * * *Accessing normalized attribute names:* Directives like 'ngBind' can be expressed in many ways:
- * 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. The attributes object allows for normalized access
- * to the attributes.
- *
- * * *Directive inter-communication:* All directives share the same instance of the attributes
- * object which allows the directives to use the attributes object as inter directive
- * communication.
- *
- * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object
- * allowing other directives to read the interpolated value.
- *
- * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes
- * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also
- * the only way to easily get the actual value because during the linking phase the interpolation
- * hasn't been evaluated yet and so the value is at this time set to `undefined`.
- *
- * ```js
- * function linkingFn(scope, elm, attrs, ctrl) {
- * // get the attribute value
- * console.log(attrs.ngModel);
- *
- * // change the attribute
- * attrs.$set('ngModel', 'new value');
- *
- * // observe changes to interpolated attribute
- * attrs.$observe('ngModel', function(value) {
- * console.log('ngModel has changed value to ' + value);
- * });
- * }
- * ```
- *
- * ## Example
- *
- * <div class="alert alert-warning">
- * **Note**: Typically directives are registered with `module.directive`. The example below is
- * to illustrate how `$compile` works.
- * </div>
- *
- <example module="compileExample">
- <file name="index.html">
- <script>
- angular.module('compileExample', [], function($compileProvider) {
- // configure new 'compile' directive by passing a directive
- // factory function. The factory function injects the '$compile'
- $compileProvider.directive('compile', function($compile) {
- // directive factory creates a link function
- return function(scope, element, attrs) {
- scope.$watch(
- function(scope) {
- // watch the 'compile' expression for changes
- return scope.$eval(attrs.compile);
- },
- function(value) {
- // when the 'compile' expression changes
- // assign it into the current DOM
- element.html(value);
-
- // compile the new DOM and link it to the current
- // scope.
- // NOTE: we only compile .childNodes so that
- // we don't get into infinite loop compiling ourselves
- $compile(element.contents())(scope);
- }
- );
- };
- });
- })
- .controller('GreeterController', ['$scope', function($scope) {
- $scope.name = 'Angular';
- $scope.html = 'Hello {{name}}';
- }]);
- </script>
- <div ng-controller="GreeterController">
- <input ng-model="name"> <br/>
- <textarea ng-model="html"></textarea> <br/>
- <div compile="html"></div>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should auto compile', function() {
- var textarea = $('textarea');
- var output = $('div[compile]');
- // The initial state reads 'Hello Angular'.
- expect(output.getText()).toBe('Hello Angular');
- textarea.clear();
- textarea.sendKeys('{{name}}!');
- expect(output.getText()).toBe('Angular!');
- });
- </file>
- </example>
-
- *
- *
- * @param {string|DOMElement} element Element or HTML string to compile into a template function.
- * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED.
- *
- * <div class="alert alert-danger">
- * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it
- * e.g. will not use the right outer scope. Please pass the transclude function as a
- * `parentBoundTranscludeFn` to the link function instead.
- * </div>
- *
- * @param {number} maxPriority only apply directives lower than given priority (Only effects the
- * root element(s), not their children)
- * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template
- * (a DOM element/tree) to a scope. Where:
- *
- * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
- * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
- * `template` and call the `cloneAttachFn` function allowing the caller to attach the
- * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
- * called as: <br/> `cloneAttachFn(clonedElement, scope)` where:
- *
- * * `clonedElement` - is a clone of the original `element` passed into the compiler.
- * * `scope` - is the current scope with which the linking function is working with.
- *
- * * `options` - An optional object hash with linking options. If `options` is provided, then the following
- * keys may be used to control linking behavior:
- *
- * * `parentBoundTranscludeFn` - the transclude function made available to
- * directives; if given, it will be passed through to the link functions of
- * directives found in `element` during compilation.
- * * `transcludeControllers` - an object hash with keys that map controller names
- * to a hash with the key `instance`, which maps to the controller instance;
- * if given, it will make the controllers available to directives on the compileNode:
- * ```
- * {
- * parent: {
- * instance: parentControllerInstance
- * }
- * }
- * ```
- * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add
- * the cloned elements; only needed for transcludes that are allowed to contain non html
- * elements (e.g. SVG elements). See also the directive.controller property.
- *
- * Calling the linking function returns the element of the template. It is either the original
- * element passed in, or the clone of the element if the `cloneAttachFn` is provided.
- *
- * After linking the view is not updated until after a call to $digest which typically is done by
- * Angular automatically.
- *
- * If you need access to the bound view, there are two ways to do it:
- *
- * - If you are not asking the linking function to clone the template, create the DOM element(s)
- * before you send them to the compiler and keep this reference around.
- * ```js
- * var element = $compile('<p>{{total}}</p>')(scope);
- * ```
- *
- * - if on the other hand, you need the element to be cloned, the view reference from the original
- * example would not point to the clone, but rather to the original template that was cloned. In
- * this case, you can access the clone via the cloneAttachFn:
- * ```js
- * var templateElement = angular.element('<p>{{total}}</p>'),
- * scope = ....;
- *
- * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) {
- * //attach the clone to DOM document at the right place
- * });
- *
- * //now we have reference to the cloned DOM via `clonedElement`
- * ```
- *
- *
- * For information on how the compiler works, see the
- * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
- */
-
-var $compileMinErr = minErr('$compile');
-
-function UNINITIALIZED_VALUE() {}
-var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
-
-/**
- * @ngdoc provider
- * @name $compileProvider
- *
- * @description
- */
-$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider'];
-function $CompileProvider($provide, $$sanitizeUriProvider) {
- var hasDirectives = {},
- Suffix = 'Directive',
- COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/,
- CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/,
- 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)$/;
- var bindingCache = createMap();
-
- function parseIsolateBindings(scope, directiveName, isController) {
- var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/;
-
- var bindings = createMap();
-
- forEach(scope, function(definition, scopeName) {
- if (definition in bindingCache) {
- bindings[scopeName] = bindingCache[definition];
- return;
- }
- var match = definition.match(LOCAL_REGEXP);
-
- if (!match) {
- throw $compileMinErr('iscp',
- "Invalid {3} for directive '{0}'." +
- " Definition: {... {1}: '{2}' ...}",
- directiveName, scopeName, definition,
- (isController ? "controller bindings definition" :
- "isolate scope definition"));
- }
-
- bindings[scopeName] = {
- mode: match[1][0],
- collection: match[2] === '*',
- optional: match[3] === '?',
- attrName: match[4] || scopeName
- };
- if (match[4]) {
- bindingCache[definition] = bindings[scopeName];
- }
- });
-
- return bindings;
- }
-
- function parseDirectiveBindings(directive, directiveName) {
- var bindings = {
- isolateScope: null,
- bindToController: null
- };
- if (isObject(directive.scope)) {
- if (directive.bindToController === true) {
- bindings.bindToController = parseIsolateBindings(directive.scope,
- directiveName, true);
- bindings.isolateScope = {};
- } else {
- bindings.isolateScope = parseIsolateBindings(directive.scope,
- directiveName, false);
- }
- }
- if (isObject(directive.bindToController)) {
- bindings.bindToController =
- parseIsolateBindings(directive.bindToController, directiveName, true);
- }
- if (isObject(bindings.bindToController)) {
- var controller = directive.controller;
- var controllerAs = directive.controllerAs;
- if (!controller) {
- // There is no controller, there may or may not be a controllerAs property
- throw $compileMinErr('noctrl',
- "Cannot bind to controller without directive '{0}'s controller.",
- directiveName);
- } else if (!identifierForController(controller, controllerAs)) {
- // There is a controller, but no identifier or controllerAs property
- throw $compileMinErr('noident',
- "Cannot bind to controller without identifier for directive '{0}'.",
- directiveName);
- }
- }
- return bindings;
- }
-
- function assertValidDirectiveName(name) {
- var letter = name.charAt(0);
- if (!letter || letter !== lowercase(letter)) {
- throw $compileMinErr('baddir', "Directive/Component name '{0}' is invalid. The first character must be a lowercase letter", name);
- }
- if (name !== name.trim()) {
- throw $compileMinErr('baddir',
- "Directive/Component name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
- name);
- }
- }
-
- function getDirectiveRequire(directive) {
- var require = directive.require || (directive.controller && directive.name);
-
- if (!isArray(require) && isObject(require)) {
- forEach(require, function(value, key) {
- var match = value.match(REQUIRE_PREFIX_REGEXP);
- var name = value.substring(match[0].length);
- if (!name) require[key] = match[0] + key;
- });
- }
-
- return require;
- }
-
- /**
- * @ngdoc method
- * @name $compileProvider#directive
- * @kind function
- *
- * @description
- * Register a new directive with the compiler.
- *
- * @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which
- * will match as <code>ng-bind</code>), or an object map of directives where the keys are the
- * names and the values are the factories.
- * @param {Function|Array} directiveFactory An injectable directive factory function. See the
- * {@link guide/directive directive guide} and the {@link $compile compile API} for more info.
- * @returns {ng.$compileProvider} Self for chaining.
- */
- this.directive = function registerDirective(name, directiveFactory) {
- assertNotHasOwnProperty(name, 'directive');
- if (isString(name)) {
- assertValidDirectiveName(name);
- assertArg(directiveFactory, 'directiveFactory');
- if (!hasDirectives.hasOwnProperty(name)) {
- hasDirectives[name] = [];
- $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
- function($injector, $exceptionHandler) {
- var directives = [];
- forEach(hasDirectives[name], function(directiveFactory, index) {
- try {
- var directive = $injector.invoke(directiveFactory);
- if (isFunction(directive)) {
- directive = { compile: valueFn(directive) };
- } else if (!directive.compile && directive.link) {
- directive.compile = valueFn(directive.link);
- }
- directive.priority = directive.priority || 0;
- directive.index = index;
- directive.name = directive.name || name;
- directive.require = getDirectiveRequire(directive);
- directive.restrict = directive.restrict || 'EA';
- directive.$$moduleName = directiveFactory.$$moduleName;
- directives.push(directive);
- } catch (e) {
- $exceptionHandler(e);
- }
- });
- return directives;
- }]);
- }
- hasDirectives[name].push(directiveFactory);
- } else {
- forEach(name, reverseParams(registerDirective));
- }
- return this;
- };
-
- /**
- * @ngdoc method
- * @name $compileProvider#component
- * @module ng
- * @param {string} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`)
- * @param {Object} options Component definition object (a simplified
- * {@link ng.$compile#directive-definition-object directive definition object}),
- * with the following properties (all optional):
- *
- * - `controller` – `{(string|function()=}` – controller constructor function that should be
- * associated with newly created scope or the name of a {@link ng.$compile#-controller-
- * registered controller} if passed as a string. An empty `noop` function by default.
- * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope.
- * If present, the controller will be published to scope under the `controllerAs` name.
- * If not present, this will default to be `$ctrl`.
- * - `template` – `{string=|function()=}` – html template as a string or a function that
- * returns an html template as a string which should be used as the contents of this component.
- * Empty string by default.
- *
- * If `template` is a function, then it is {@link auto.$injector#invoke injected} with
- * the following locals:
- *
- * - `$element` - Current element
- * - `$attrs` - Current attributes object for the element
- *
- * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
- * template that should be used as the contents of this component.
- *
- * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with
- * the following locals:
- *
- * - `$element` - Current element
- * - `$attrs` - Current attributes object for the element
- *
- * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties.
- * Component properties are always bound to the component controller and not to the scope.
- * See {@link ng.$compile#-bindtocontroller- `bindToController`}.
- * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled.
- * Disabled by default.
- * - `require` - `{Object<string, string>=}` - requires the controllers of other directives and binds them to
- * this component's controller. The object keys specify the property names under which the required
- * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}.
- * - `$...` – additional properties to attach to the directive factory function and the controller
- * constructor function. (This is used by the component router to annotate)
- *
- * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls.
- * @description
- * Register a **component definition** with the compiler. This is a shorthand for registering a special
- * type of directive, which represents a self-contained UI component in your application. Such components
- * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`).
- *
- * Component definitions are very simple and do not require as much configuration as defining general
- * directives. Component definitions usually consist only of a template and a controller backing it.
- *
- * In order to make the definition easier, components enforce best practices like use of `controllerAs`,
- * `bindToController`. They always have **isolate scope** and are restricted to elements.
- *
- * Here are a few examples of how you would usually define components:
- *
- * ```js
- * var myMod = angular.module(...);
- * myMod.component('myComp', {
- * template: '<div>My name is {{$ctrl.name}}</div>',
- * controller: function() {
- * this.name = 'shahar';
- * }
- * });
- *
- * myMod.component('myComp', {
- * template: '<div>My name is {{$ctrl.name}}</div>',
- * bindings: {name: '@'}
- * });
- *
- * myMod.component('myComp', {
- * templateUrl: 'views/my-comp.html',
- * controller: 'MyCtrl',
- * controllerAs: 'ctrl',
- * bindings: {name: '@'}
- * });
- *
- * ```
- * For more examples, and an in-depth guide, see the {@link guide/component component guide}.
- *
- * <br />
- * See also {@link ng.$compileProvider#directive $compileProvider.directive()}.
- */
- this.component = function registerComponent(name, options) {
- var controller = options.controller || function() {};
-
- function factory($injector) {
- function makeInjectable(fn) {
- if (isFunction(fn) || isArray(fn)) {
- return function(tElement, tAttrs) {
- return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
- };
- } else {
- return fn;
- }
- }
-
- var template = (!options.template && !options.templateUrl ? '' : options.template);
- var ddo = {
- controller: controller,
- controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
- template: makeInjectable(template),
- templateUrl: makeInjectable(options.templateUrl),
- transclude: options.transclude,
- scope: {},
- bindToController: options.bindings || {},
- restrict: 'E',
- require: options.require
- };
-
- // Copy annotations (starting with $) over to the DDO
- forEach(options, function(val, key) {
- if (key.charAt(0) === '$') ddo[key] = val;
- });
-
- return ddo;
- }
-
- // TODO(pete) remove the following `forEach` before we release 1.6.0
- // The component-router@0.2.0 looks for the annotations on the controller constructor
- // Nothing in Angular looks for annotations on the factory function but we can't remove
- // it from 1.5.x yet.
-
- // Copy any annotation properties (starting with $) over to the factory and controller constructor functions
- // These could be used by libraries such as the new component router
- forEach(options, function(val, key) {
- if (key.charAt(0) === '$') {
- factory[key] = val;
- // Don't try to copy over annotations to named controller
- if (isFunction(controller)) controller[key] = val;
- }
- });
-
- factory.$inject = ['$injector'];
-
- return this.directive(name, factory);
- };
-
-
- /**
- * @ngdoc method
- * @name $compileProvider#aHrefSanitizationWhitelist
- * @kind function
- *
- * @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
- * urls during a[href] sanitization.
- *
- * The sanitization is a security measure aimed at preventing XSS attacks via html links.
- *
- * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
- *
- * @param {RegExp=} regexp New regexp to whitelist urls with.
- * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
- * chaining otherwise.
- */
- this.aHrefSanitizationWhitelist = function(regexp) {
- if (isDefined(regexp)) {
- $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp);
- return this;
- } else {
- return $$sanitizeUriProvider.aHrefSanitizationWhitelist();
- }
- };
-
-
- /**
- * @ngdoc method
- * @name $compileProvider#imgSrcSanitizationWhitelist
- * @kind function
- *
- * @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
- * urls during img[src] sanitization.
- *
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
- *
- * Any url about to be assigned to img[src] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
- *
- * @param {RegExp=} regexp New regexp to whitelist urls with.
- * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
- * chaining otherwise.
- */
- this.imgSrcSanitizationWhitelist = function(regexp) {
- if (isDefined(regexp)) {
- $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp);
- return this;
- } else {
- return $$sanitizeUriProvider.imgSrcSanitizationWhitelist();
- }
- };
-
- /**
- * @ngdoc method
- * @name $compileProvider#debugInfoEnabled
- *
- * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the
- * current debugInfoEnabled state
- * @returns {*} current value if used as getter or itself (chaining) if used as setter
- *
- * @kind function
- *
- * @description
- * Call this method to enable/disable various debug runtime information in the compiler such as adding
- * binding information and a reference to the current scope on to DOM elements.
- * If enabled, the compiler will add the following to DOM elements that have been bound to the scope
- * * `ng-binding` CSS class
- * * `$binding` data property containing an array of the binding expressions
- *
- * You may want to disable this in production for a significant performance boost. See
- * {@link guide/production#disabling-debug-data Disabling Debug Data} for more.
- *
- * The default value is true.
- */
- var debugInfoEnabled = true;
- this.debugInfoEnabled = function(enabled) {
- if (isDefined(enabled)) {
- debugInfoEnabled = enabled;
- return this;
- }
- return debugInfoEnabled;
- };
-
-
- var TTL = 10;
- /**
- * @ngdoc method
- * @name $compileProvider#onChangesTtl
- * @description
- *
- * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
- * assuming that the model is unstable.
- *
- * The current default is 10 iterations.
- *
- * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
- * in several iterations of calls to these hooks. However if an application needs more than the default 10
- * iterations to stabilize then you should investigate what is causing the model to continuously change during
- * the `$onChanges` hook execution.
- *
- * Increasing the TTL could have performance implications, so you should not change it without proper justification.
- *
- * @param {number} limit The number of `$onChanges` hook iterations.
- * @returns {number|object} the current limit (or `this` if called as a setter for chaining)
- */
- this.onChangesTtl = function(value) {
- if (arguments.length) {
- TTL = value;
- return this;
- }
- return TTL;
- };
-
- this.$get = [
- '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
- '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
- function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
- $controller, $rootScope, $sce, $animate, $$sanitizeUri) {
-
- var SIMPLE_ATTR_NAME = /^\w/;
- var specialAttrHolder = window.document.createElement('div');
-
-
-
- var onChangesTtl = TTL;
- // The onChanges hooks should all be run together in a single digest
- // When changes occur, the call to trigger their hooks will be added to this queue
- var onChangesQueue;
-
- // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
- function flushOnChangesQueue() {
- try {
- if (!(--onChangesTtl)) {
- // We have hit the TTL limit so reset everything
- onChangesQueue = undefined;
- throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL);
- }
- // We must run this hook in an apply since the $$postDigest runs outside apply
- $rootScope.$apply(function() {
- var errors = [];
- for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
- try {
- onChangesQueue[i]();
- } catch (e) {
- errors.push(e);
- }
- }
- // Reset the queue to trigger a new schedule next time there is a change
- onChangesQueue = undefined;
- if (errors.length) {
- throw errors;
- }
- });
- } finally {
- onChangesTtl++;
- }
- }
-
-
- function Attributes(element, attributesToCopy) {
- if (attributesToCopy) {
- var keys = Object.keys(attributesToCopy);
- var i, l, key;
-
- for (i = 0, l = keys.length; i < l; i++) {
- key = keys[i];
- this[key] = attributesToCopy[key];
- }
- } else {
- this.$attr = {};
- }
-
- this.$$element = element;
- }
-
- Attributes.prototype = {
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$normalize
- * @kind function
- *
- * @description
- * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or
- * `data-`) to its normalized, camelCase form.
- *
- * Also there is special case for Moz prefix starting with upper case letter.
- *
- * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
- *
- * @param {string} name Name to normalize
- */
- $normalize: directiveNormalize,
-
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$addClass
- * @kind function
- *
- * @description
- * Adds the CSS class value specified by the classVal parameter to the element. If animations
- * are enabled then an animation will be triggered for the class addition.
- *
- * @param {string} classVal The className value that will be added to the element
- */
- $addClass: function(classVal) {
- if (classVal && classVal.length > 0) {
- $animate.addClass(this.$$element, classVal);
- }
- },
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$removeClass
- * @kind function
- *
- * @description
- * Removes the CSS class value specified by the classVal parameter from the element. If
- * animations are enabled then an animation will be triggered for the class removal.
- *
- * @param {string} classVal The className value that will be removed from the element
- */
- $removeClass: function(classVal) {
- if (classVal && classVal.length > 0) {
- $animate.removeClass(this.$$element, classVal);
- }
- },
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$updateClass
- * @kind function
- *
- * @description
- * Adds and removes the appropriate CSS class values to the element based on the difference
- * between the new and old CSS class values (specified as newClasses and oldClasses).
- *
- * @param {string} newClasses The current CSS className value
- * @param {string} oldClasses The former CSS className value
- */
- $updateClass: function(newClasses, oldClasses) {
- var toAdd = tokenDifference(newClasses, oldClasses);
- if (toAdd && toAdd.length) {
- $animate.addClass(this.$$element, toAdd);
- }
-
- var toRemove = tokenDifference(oldClasses, newClasses);
- if (toRemove && toRemove.length) {
- $animate.removeClass(this.$$element, toRemove);
- }
- },
-
- /**
- * Set a normalized attribute on the element in a way such that all directives
- * can share the attribute. This function properly handles boolean attributes.
- * @param {string} key Normalized key. (ie ngAttribute)
- * @param {string|boolean} value The value to set. If `null` attribute will be deleted.
- * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
- * Defaults to true.
- * @param {string=} attrName Optional none normalized name. Defaults to key.
- */
- $set: function(key, value, writeAttr, attrName) {
- // TODO: decide whether or not to throw an error if "class"
- //is set through this function since it may cause $updateClass to
- //become unstable.
-
- var node = this.$$element[0],
- booleanKey = getBooleanAttrName(node, key),
- aliasedKey = getAliasedAttrName(key),
- observer = key,
- nodeName;
-
- if (booleanKey) {
- this.$$element.prop(key, value);
- attrName = booleanKey;
- } else if (aliasedKey) {
- this[aliasedKey] = value;
- observer = aliasedKey;
- }
-
- this[key] = value;
-
- // translate normalized key to actual key
- if (attrName) {
- this.$attr[key] = attrName;
- } else {
- attrName = this.$attr[key];
- if (!attrName) {
- this.$attr[key] = attrName = snake_case(key, '-');
- }
- }
-
- nodeName = nodeName_(this.$$element);
-
- if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
- (nodeName === 'img' && key === 'src')) {
- // sanitize a[href] and img[src] values
- this[key] = value = $$sanitizeUri(value, key === 'src');
- } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
- // 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 || isUndefined(value)) {
- this.$$element.removeAttr(attrName);
- } else {
- if (SIMPLE_ATTR_NAME.test(attrName)) {
- this.$$element.attr(attrName, value);
- } else {
- setSpecialAttr(this.$$element[0], attrName, value);
- }
- }
- }
-
- // fire observers
- var $$observers = this.$$observers;
- $$observers && forEach($$observers[observer], function(fn) {
- try {
- fn(value);
- } catch (e) {
- $exceptionHandler(e);
- }
- });
- },
-
-
- /**
- * @ngdoc method
- * @name $compile.directive.Attributes#$observe
- * @kind function
- *
- * @description
- * Observes an interpolated attribute.
- *
- * The observer function will be invoked once during the next `$digest` following
- * compilation. The observer is then invoked whenever the interpolated value
- * 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/interpolation#how-text-and-attribute-bindings-work Interpolation
- * guide} for more info.
- * @returns {function()} Returns a deregistration function for this observer.
- */
- $observe: function(key, fn) {
- var attrs = this,
- $$observers = (attrs.$$observers || (attrs.$$observers = createMap())),
- listeners = ($$observers[key] || ($$observers[key] = []));
-
- listeners.push(fn);
- $rootScope.$evalAsync(function() {
- if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) {
- // no one registered attribute interpolation function, so lets call it manually
- fn(attrs[key]);
- }
- });
-
- return function() {
- arrayRemove(listeners, fn);
- };
- }
- };
-
- function setSpecialAttr(element, attrName, value) {
- // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute`
- // so we have to jump through some hoops to get such an attribute
- // https://github.com/angular/angular.js/pull/13318
- specialAttrHolder.innerHTML = "<span " + attrName + ">";
- var attributes = specialAttrHolder.firstChild.attributes;
- var attribute = attributes[0];
- // We have to remove the attribute from its container element before we can add it to the destination element
- attributes.removeNamedItem(attribute.name);
- attribute.value = value;
- element.attributes.setNamedItem(attribute);
- }
-
- function safeAddClass($element, className) {
- try {
- $element.addClass(className);
- } catch (e) {
- // ignore, since it means that we are trying to set class on
- // SVG element, where class name is read-only.
- }
- }
-
-
- var startSymbol = $interpolate.startSymbol(),
- endSymbol = $interpolate.endSymbol(),
- denormalizeTemplate = (startSymbol == '{{' && endSymbol == '}}')
- ? identity
- : function denormalizeTemplate(template) {
- return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
- },
- NG_ATTR_BINDING = /^ngAttr[A-Z]/;
- var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
-
- compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
- var bindings = $element.data('$binding') || [];
-
- if (isArray(binding)) {
- bindings = bindings.concat(binding);
- } else {
- bindings.push(binding);
- }
-
- $element.data('$binding', bindings);
- } : noop;
-
- compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) {
- safeAddClass($element, 'ng-binding');
- } : noop;
-
- compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) {
- var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
- $element.data(dataName, scope);
- } : noop;
-
- compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) {
- safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope');
- } : noop;
-
- compile.$$createComment = function(directiveName, comment) {
- var content = '';
- if (debugInfoEnabled) {
- content = ' ' + (directiveName || '') + ': ';
- if (comment) content += comment + ' ';
- }
- return window.document.createComment(content);
- };
-
- return compile;
-
- //================================
-
- function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,
- previousCompileContext) {
- if (!($compileNodes instanceof jqLite)) {
- // jquery always rewraps, whereas we need to preserve the original selector so that we can
- // modify it.
- $compileNodes = jqLite($compileNodes);
- }
-
- var NOT_EMPTY = /\S+/;
-
- // We can not compile top level text elements since text nodes can be merged and we will
- // not be able to attach scope data to them, so we will wrap them in <span>
- for (var i = 0, len = $compileNodes.length; i < len; i++) {
- var domNode = $compileNodes[i];
-
- if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) {
- jqLiteWrapNode(domNode, $compileNodes[i] = window.document.createElement('span'));
- }
- }
-
- var compositeLinkFn =
- compileNodes($compileNodes, transcludeFn, $compileNodes,
- maxPriority, ignoreDirective, previousCompileContext);
- compile.$$addScopeClass($compileNodes);
- var namespace = null;
- return function publicLinkFn(scope, cloneConnectFn, options) {
- assertArg(scope, 'scope');
-
- if (previousCompileContext && previousCompileContext.needsNewScope) {
- // A parent directive did a replace and a directive on this element asked
- // for transclusion, which caused us to lose a layer of element on which
- // we could hold the new transclusion scope, so we will create it manually
- // here.
- scope = scope.$parent.$new();
- }
-
- options = options || {};
- var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
- transcludeControllers = options.transcludeControllers,
- futureParentElement = options.futureParentElement;
-
- // When `parentBoundTranscludeFn` is passed, it is a
- // `controllersBoundTransclude` function (it was previously passed
- // as `transclude` to directive.link) so we must unwrap it to get
- // its `boundTranscludeFn`
- if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) {
- parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude;
- }
-
- if (!namespace) {
- namespace = detectNamespaceForChildElements(futureParentElement);
- }
- 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;
- }
-
- if (transcludeControllers) {
- for (var controllerName in transcludeControllers) {
- $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance);
- }
- }
-
- compile.$$addScopeInfo($linkNode, scope);
-
- if (cloneConnectFn) cloneConnectFn($linkNode, scope);
- if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
- return $linkNode;
- };
- }
-
- function detectNamespaceForChildElements(parentElement) {
- // TODO: Make this detect MathML as well...
- var node = parentElement && parentElement[0];
- if (!node) {
- return 'html';
- } else {
- return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html';
- }
- }
-
- /**
- * Compile function matches each node in nodeList against the directives. Once all directives
- * for a particular node are collected their compile functions are executed. The compile
- * functions return values - the linking functions - are combined into a composite linking
- * function, which is the a linking function for the node.
- *
- * @param {NodeList} nodeList an array of nodes or NodeList to compile
- * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the
- * scope argument is auto-generated to the new child of the transcluded parent scope.
- * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then
- * the rootElement must be set the jqLite collection of the compile root. This is
- * needed so that the jqLite collection items can be replaced with widgets.
- * @param {number=} maxPriority Max directive priority.
- * @returns {Function} A composite linking function of all of the matched directives or null.
- */
- function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,
- previousCompileContext) {
- var linkFns = [],
- attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;
-
- for (var i = 0; i < nodeList.length; i++) {
- attrs = new Attributes();
-
- // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
- directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,
- ignoreDirective);
-
- nodeLinkFn = (directives.length)
- ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement,
- null, [], [], previousCompileContext)
- : null;
-
- if (nodeLinkFn && nodeLinkFn.scope) {
- compile.$$addScopeClass(attrs.$$element);
- }
-
- childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
- !(childNodes = nodeList[i].childNodes) ||
- !childNodes.length)
- ? null
- : compileNodes(childNodes,
- nodeLinkFn ? (
- (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
- && nodeLinkFn.transclude) : transcludeFn);
-
- if (nodeLinkFn || childLinkFn) {
- linkFns.push(i, nodeLinkFn, childLinkFn);
- linkFnFound = true;
- nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn;
- }
-
- //use the previous context only for the first element in the virtual group
- previousCompileContext = null;
- }
-
- // return a linking function if we have found anything, null otherwise
- return linkFnFound ? compositeLinkFn : null;
-
- function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
- var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn;
- var stableNodeList;
-
-
- if (nodeLinkFnFound) {
- // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our
- // offsets don't get screwed up
- var nodeListLength = nodeList.length;
- stableNodeList = new Array(nodeListLength);
-
- // create a sparse array by only copying the elements which have a linkFn
- for (i = 0; i < linkFns.length; i+=3) {
- idx = linkFns[i];
- stableNodeList[idx] = nodeList[idx];
- }
- } else {
- stableNodeList = nodeList;
- }
-
- for (i = 0, ii = linkFns.length; i < ii;) {
- node = stableNodeList[linkFns[i++]];
- nodeLinkFn = linkFns[i++];
- childLinkFn = linkFns[i++];
-
- if (nodeLinkFn) {
- if (nodeLinkFn.scope) {
- childScope = scope.$new();
- compile.$$addScopeInfo(jqLite(node), childScope);
- } else {
- childScope = scope;
- }
-
- if (nodeLinkFn.transcludeOnThisElement) {
- childBoundTranscludeFn = createBoundTranscludeFn(
- scope, nodeLinkFn.transclude, parentBoundTranscludeFn);
-
- } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
- childBoundTranscludeFn = parentBoundTranscludeFn;
-
- } else if (!parentBoundTranscludeFn && transcludeFn) {
- childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn);
-
- } else {
- childBoundTranscludeFn = null;
- }
-
- nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
-
- } else if (childLinkFn) {
- childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
- }
- }
- }
- }
-
- function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
- function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
-
- if (!transcludedScope) {
- transcludedScope = scope.$new(false, containingScope);
- transcludedScope.$$transcluded = true;
- }
-
- return transcludeFn(transcludedScope, cloneFn, {
- parentBoundTranscludeFn: previousBoundTranscludeFn,
- transcludeControllers: controllers,
- futureParentElement: futureParentElement
- });
- }
-
- // We need to attach the transclusion slots onto the `boundTranscludeFn`
- // so that they are available inside the `controllersBoundTransclude` function
- var boundSlots = boundTranscludeFn.$$slots = createMap();
- for (var slotName in transcludeFn.$$slots) {
- if (transcludeFn.$$slots[slotName]) {
- boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
- } else {
- boundSlots[slotName] = null;
- }
- }
-
- return boundTranscludeFn;
- }
-
- /**
- * Looks for directives on the given node and adds them to the directive collection which is
- * sorted.
- *
- * @param node Node to search.
- * @param directives An array to which the directives are added to. This array is sorted before
- * the function returns.
- * @param attrs The shared attrs object which is used to populate the normalized attributes.
- * @param {number=} maxPriority Max directive priority.
- */
- function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
- var nodeType = node.nodeType,
- attrsMap = attrs.$attr,
- match,
- className;
-
- switch (nodeType) {
- case NODE_TYPE_ELEMENT: /* Element */
- // use the node name: <directive>
- addDirective(directives,
- directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective);
-
- // iterate over the attributes
- for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
- j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
- var attrStartName = false;
- var attrEndName = false;
-
- attr = nAttrs[j];
- name = attr.name;
- value = trim(attr.value);
-
- // support ngAttr attribute binding
- ngAttrName = directiveNormalize(name);
- if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
- name = name.replace(PREFIX_REGEXP, '')
- .substr(8).replace(/_(.)/g, function(match, letter) {
- return letter.toUpperCase();
- });
- }
-
- var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
- if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
- 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);
- }
-
- // use class as directive
- className = node.className;
- if (isObject(className)) {
- // Maybe SVGAnimatedString
- className = className.animVal;
- }
- if (isString(className) && className !== '') {
- while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
- nName = directiveNormalize(match[2]);
- if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) {
- attrs[nName] = trim(match[3]);
- }
- className = className.substr(match.index + match[0].length);
- }
- }
- break;
- case NODE_TYPE_TEXT: /* Text Node */
- if (msie === 11) {
- // Workaround for #11781
- while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) {
- node.nodeValue = node.nodeValue + node.nextSibling.nodeValue;
- node.parentNode.removeChild(node.nextSibling);
- }
- }
- addTextInterpolateDirective(directives, node.nodeValue);
- break;
- case NODE_TYPE_COMMENT: /* Comment */
- collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective);
- break;
- }
-
- directives.sort(byPriority);
- return directives;
- }
-
- function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
- // function created because of performance, try/catch disables
- // the optimization of the whole function #14848
- try {
- var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
- if (match) {
- var nName = directiveNormalize(match[1]);
- if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
- attrs[nName] = trim(match[2]);
- }
- }
- } catch (e) {
- // turns out that under some circumstances IE9 throws errors when one attempts to read
- // comment's node value.
- // Just ignore it and continue. (Can't seem to reproduce in test case.)
- }
- }
-
- /**
- * Given a node with an directive-start it collects all of the siblings until it finds
- * directive-end.
- * @param node
- * @param attrStart
- * @param attrEnd
- * @returns {*}
- */
- function groupScan(node, attrStart, attrEnd) {
- var nodes = [];
- var depth = 0;
- if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
- do {
- if (!node) {
- throw $compileMinErr('uterdir',
- "Unterminated attribute, found '{0}' but no matching '{1}' found.",
- attrStart, attrEnd);
- }
- if (node.nodeType == NODE_TYPE_ELEMENT) {
- if (node.hasAttribute(attrStart)) depth++;
- if (node.hasAttribute(attrEnd)) depth--;
- }
- nodes.push(node);
- node = node.nextSibling;
- } while (depth > 0);
- } else {
- nodes.push(node);
- }
-
- return jqLite(nodes);
- }
-
- /**
- * Wrapper for linking function which converts normal linking function into a grouped
- * linking function.
- * @param linkFn
- * @param attrStart
- * @param attrEnd
- * @returns {Function}
- */
- function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
- return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) {
- element = groupScan(element[0], attrStart, attrEnd);
- return linkFn(scope, element, attrs, controllers, transcludeFn);
- };
- }
-
- /**
- * A function generator that is used to support both eager and lazy compilation
- * linking function.
- * @param eager
- * @param $compileNodes
- * @param transcludeFn
- * @param maxPriority
- * @param ignoreDirective
- * @param previousCompileContext
- * @returns {Function}
- */
- function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
- var compiled;
-
- if (eager) {
- return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
- }
- return function lazyCompilation() {
- if (!compiled) {
- compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
-
- // Null out all of these references in order to make them eligible for garbage collection
- // since this is a potentially long lived closure
- $compileNodes = transcludeFn = previousCompileContext = null;
- }
- return compiled.apply(this, arguments);
- };
- }
-
- /**
- * Once the directives have been collected, their compile functions are executed. This method
- * is responsible for inlining directive templates as well as terminating the application
- * of the directives if the terminal directive has been reached.
- *
- * @param {Array} directives Array of collected directives to execute their compile function.
- * this needs to be pre-sorted by priority order.
- * @param {Node} compileNode The raw DOM node to apply the compile functions to
- * @param {Object} templateAttrs The shared attribute function
- * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the
- * scope argument is auto-generated to the new
- * child of the transcluded parent scope.
- * @param {JQLite} jqCollection If we are working on the root of the compile tree then this
- * argument has the root jqLite array so that we can replace nodes
- * on it.
- * @param {Object=} originalReplaceDirective An optional directive that will be ignored when
- * compiling the transclusion.
- * @param {Array.<Function>} preLinkFns
- * @param {Array.<Function>} postLinkFns
- * @param {Object} previousCompileContext Context used for previous compilation of the current
- * node
- * @returns {Function} linkFn
- */
- function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn,
- jqCollection, originalReplaceDirective, preLinkFns, postLinkFns,
- previousCompileContext) {
- previousCompileContext = previousCompileContext || {};
-
- var terminalPriority = -Number.MAX_VALUE,
- newScopeDirective = previousCompileContext.newScopeDirective,
- controllerDirectives = previousCompileContext.controllerDirectives,
- newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
- templateDirective = previousCompileContext.templateDirective,
- nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective,
- hasTranscludeDirective = false,
- hasTemplate = false,
- hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective,
- $compileNode = templateAttrs.$$element = jqLite(compileNode),
- directive,
- directiveName,
- $template,
- replaceDirective = originalReplaceDirective,
- childTranscludeFn = transcludeFn,
- linkFn,
- didScanForMultipleTransclusion = false,
- mightHaveMultipleTransclusionError = false,
- directiveValue;
-
- // executes all directives on the current element
- for (var i = 0, ii = directives.length; i < ii; i++) {
- directive = directives[i];
- var attrStart = directive.$$start;
- var attrEnd = directive.$$end;
-
- // collect multiblock sections
- if (attrStart) {
- $compileNode = groupScan(compileNode, attrStart, attrEnd);
- }
- $template = undefined;
-
- if (terminalPriority > directive.priority) {
- break; // prevent further processing of directives
- }
-
- if (directiveValue = directive.scope) {
-
- // skip the check for directives with async templates, we'll check the derived sync
- // directive when the template arrives
- if (!directive.templateUrl) {
- if (isObject(directiveValue)) {
- // This directive is trying to add an isolated scope.
- // Check that there is no scope of any kind already
- assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective,
- directive, $compileNode);
- newIsolateScopeDirective = directive;
- } else {
- // This directive is trying to add a child scope.
- // Check that there is no isolated scope already
- assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive,
- $compileNode);
- }
- }
-
- newScopeDirective = newScopeDirective || directive;
- }
-
- directiveName = directive.name;
-
- // If we encounter a condition that can result in transclusion on the directive,
- // then scan ahead in the remaining directives for others that may cause a multiple
- // transclusion error to be thrown during the compilation process. If a matching directive
- // is found, then we know that when we encounter a transcluded directive, we need to eagerly
- // compile the `transclude` function rather than doing it lazily in order to throw
- // exceptions at the correct time
- if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template))
- || (directive.transclude && !directive.$$tlb))) {
- var candidateDirective;
-
- for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) {
- if ((candidateDirective.transclude && !candidateDirective.$$tlb)
- || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) {
- mightHaveMultipleTransclusionError = true;
- break;
- }
- }
-
- didScanForMultipleTransclusion = true;
- }
-
- if (!directive.templateUrl && directive.controller) {
- directiveValue = directive.controller;
- controllerDirectives = controllerDirectives || createMap();
- assertNoDuplicate("'" + directiveName + "' controller",
- controllerDirectives[directiveName], directive, $compileNode);
- controllerDirectives[directiveName] = directive;
- }
-
- if (directiveValue = directive.transclude) {
- hasTranscludeDirective = true;
-
- // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion.
- // This option should only be used by directives that know how to safely handle element transclusion,
- // where the transcluded nodes are added or replaced after linking.
- if (!directive.$$tlb) {
- assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode);
- nonTlbTranscludeDirective = directive;
- }
-
- if (directiveValue == 'element') {
- hasElementTranscludeDirective = true;
- terminalPriority = directive.priority;
- $template = $compileNode;
- $compileNode = templateAttrs.$$element =
- jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName]));
- compileNode = $compileNode[0];
- replaceWith(jqCollection, sliceArgs($template), compileNode);
-
- // Support: Chrome < 50
- // https://github.com/angular/angular.js/issues/14041
-
- // In the versions of V8 prior to Chrome 50, the document fragment that is created
- // in the `replaceWith` function is improperly garbage collected despite still
- // being referenced by the `parentNode` property of all of the child nodes. By adding
- // a reference to the fragment via a different property, we can avoid that incorrect
- // behavior.
- // TODO: remove this line after Chrome 50 has been released
- $template[0].$$parentNode = $template[0].parentNode;
-
- childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
- replaceDirective && replaceDirective.name, {
- // Don't pass in:
- // - controllerDirectives - otherwise we'll create duplicates controllers
- // - newIsolateScopeDirective or templateDirective - combining templates with
- // element transclusion doesn't make sense.
- //
- // We need only nonTlbTranscludeDirective so that we prevent putting transclusion
- // on the same element more than once.
- nonTlbTranscludeDirective: nonTlbTranscludeDirective
- });
- } else {
-
- var slots = createMap();
-
- $template = jqLite(jqLiteClone(compileNode)).contents();
-
- if (isObject(directiveValue)) {
-
- // We have transclusion slots,
- // collect them up, compile them and store their transclusion functions
- $template = [];
-
- var slotMap = createMap();
- var filledSlots = createMap();
-
- // Parse the element selectors
- forEach(directiveValue, function(elementSelector, slotName) {
- // If an element selector starts with a ? then it is optional
- var optional = (elementSelector.charAt(0) === '?');
- elementSelector = optional ? elementSelector.substring(1) : elementSelector;
-
- slotMap[elementSelector] = slotName;
-
- // We explicitly assign `null` since this implies that a slot was defined but not filled.
- // Later when calling boundTransclusion functions with a slot name we only error if the
- // slot is `undefined`
- slots[slotName] = null;
-
- // filledSlots contains `true` for all slots that are either optional or have been
- // filled. This is used to check that we have not missed any required slots
- filledSlots[slotName] = optional;
- });
-
- // Add the matching elements into their slot
- forEach($compileNode.contents(), function(node) {
- var slotName = slotMap[directiveNormalize(nodeName_(node))];
- if (slotName) {
- filledSlots[slotName] = true;
- slots[slotName] = slots[slotName] || [];
- slots[slotName].push(node);
- } else {
- $template.push(node);
- }
- });
-
- // Check for required slots that were not filled
- forEach(filledSlots, function(filled, slotName) {
- if (!filled) {
- throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName);
- }
- });
-
- for (var slotName in slots) {
- if (slots[slotName]) {
- // Only define a transclusion function if the slot was filled
- slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
- }
- }
- }
-
- $compileNode.empty(); // clear contents
- childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined,
- undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
- childTranscludeFn.$$slots = slots;
- }
- }
-
- if (directive.template) {
- hasTemplate = true;
- assertNoDuplicate('template', templateDirective, directive, $compileNode);
- templateDirective = directive;
-
- directiveValue = (isFunction(directive.template))
- ? directive.template($compileNode, templateAttrs)
- : directive.template;
-
- directiveValue = denormalizeTemplate(directiveValue);
-
- if (directive.replace) {
- replaceDirective = directive;
- if (jqLiteIsTextNode(directiveValue)) {
- $template = [];
- } else {
- $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue)));
- }
- compileNode = $template[0];
-
- if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
- throw $compileMinErr('tplrt',
- "Template for directive '{0}' must have exactly one root element. {1}",
- directiveName, '');
- }
-
- replaceWith(jqCollection, $compileNode, compileNode);
-
- var newTemplateAttrs = {$attr: {}};
-
- // combine directives from the original node and from the template:
- // - take the array of directives for this element
- // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed)
- // - collect directives from the template and sort them by priority
- // - combine directives as: processed + template + unprocessed
- var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
- var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));
-
- if (newIsolateScopeDirective || newScopeDirective) {
- // The original directive caused the current element to be replaced but this element
- // also needs to have a new scope, so we need to tell the template directives
- // that they would need to get their scope from further up, if they require transclusion
- markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective);
- }
- directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
- mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
-
- ii = directives.length;
- } else {
- $compileNode.html(directiveValue);
- }
- }
-
- if (directive.templateUrl) {
- hasTemplate = true;
- assertNoDuplicate('template', templateDirective, directive, $compileNode);
- templateDirective = directive;
-
- if (directive.replace) {
- replaceDirective = directive;
- }
-
- /* jshint -W021 */
- nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
- /* jshint +W021 */
- templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
- controllerDirectives: controllerDirectives,
- newScopeDirective: (newScopeDirective !== directive) && newScopeDirective,
- newIsolateScopeDirective: newIsolateScopeDirective,
- templateDirective: templateDirective,
- nonTlbTranscludeDirective: nonTlbTranscludeDirective
- });
- ii = directives.length;
- } else if (directive.compile) {
- try {
- linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
- var context = directive.$$originalDirective || directive;
- if (isFunction(linkFn)) {
- addLinkFns(null, bind(context, linkFn), attrStart, attrEnd);
- } else if (linkFn) {
- addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd);
- }
- } catch (e) {
- $exceptionHandler(e, startingTag($compileNode));
- }
- }
-
- if (directive.terminal) {
- nodeLinkFn.terminal = true;
- terminalPriority = Math.max(terminalPriority, directive.priority);
- }
-
- }
-
- nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true;
- nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective;
- nodeLinkFn.templateOnThisElement = hasTemplate;
- nodeLinkFn.transclude = childTranscludeFn;
-
- previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective;
-
- // might be normal or delayed nodeLinkFn depending on if templateUrl is present
- return nodeLinkFn;
-
- ////////////////////
-
- function addLinkFns(pre, post, attrStart, attrEnd) {
- if (pre) {
- if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);
- pre.require = directive.require;
- pre.directiveName = directiveName;
- if (newIsolateScopeDirective === directive || directive.$$isolateScope) {
- pre = cloneAndAnnotateFn(pre, {isolateScope: true});
- }
- preLinkFns.push(pre);
- }
- if (post) {
- if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);
- post.require = directive.require;
- post.directiveName = directiveName;
- if (newIsolateScopeDirective === directive || directive.$$isolateScope) {
- post = cloneAndAnnotateFn(post, {isolateScope: true});
- }
- postLinkFns.push(post);
- }
- }
-
- function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
- var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
- attrs, scopeBindingInfo;
-
- if (compileNode === linkNode) {
- attrs = templateAttrs;
- $element = templateAttrs.$$element;
- } else {
- $element = jqLite(linkNode);
- attrs = new Attributes($element, templateAttrs);
- }
-
- controllerScope = scope;
- if (newIsolateScopeDirective) {
- isolateScope = scope.$new(true);
- } else if (newScopeDirective) {
- controllerScope = scope.$parent;
- }
-
- if (boundTranscludeFn) {
- // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn`
- // is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
- transcludeFn = controllersBoundTransclude;
- transcludeFn.$$boundTransclude = boundTranscludeFn;
- // expose the slots on the `$transclude` function
- transcludeFn.isSlotFilled = function(slotName) {
- return !!boundTranscludeFn.$$slots[slotName];
- };
- }
-
- if (controllerDirectives) {
- elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective);
- }
-
- if (newIsolateScopeDirective) {
- // Initialize isolate scope bindings for new isolate scope directive.
- compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective ||
- templateDirective === newIsolateScopeDirective.$$originalDirective)));
- compile.$$addScopeClass($element, true);
- isolateScope.$$isolateBindings =
- newIsolateScopeDirective.$$isolateBindings;
- scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope,
- isolateScope.$$isolateBindings,
- newIsolateScopeDirective);
- if (scopeBindingInfo.removeWatches) {
- isolateScope.$on('$destroy', scopeBindingInfo.removeWatches);
- }
- }
-
- // Initialize bindToController bindings
- for (var name in elementControllers) {
- var controllerDirective = controllerDirectives[name];
- var controller = elementControllers[name];
- var bindings = controllerDirective.$$bindings.bindToController;
-
- if (controller.identifier && bindings) {
- controller.bindingInfo =
- initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
- } else {
- controller.bindingInfo = {};
- }
-
- var controllerResult = controller();
- if (controllerResult !== controller.instance) {
- // If the controller constructor has a return value, overwrite the instance
- // from setupControllers
- controller.instance = controllerResult;
- $element.data('$' + controllerDirective.name + 'Controller', controllerResult);
- controller.bindingInfo.removeWatches && controller.bindingInfo.removeWatches();
- controller.bindingInfo =
- initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
- }
- }
-
- // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy
- forEach(controllerDirectives, function(controllerDirective, name) {
- var require = controllerDirective.require;
- if (controllerDirective.bindToController && !isArray(require) && isObject(require)) {
- extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers));
- }
- });
-
- // Handle the init and destroy lifecycle hooks on all controllers that have them
- forEach(elementControllers, function(controller) {
- var controllerInstance = controller.instance;
- if (isFunction(controllerInstance.$onChanges)) {
- try {
- controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
- } catch (e) {
- $exceptionHandler(e);
- }
- }
- if (isFunction(controllerInstance.$onInit)) {
- try {
- controllerInstance.$onInit();
- } catch (e) {
- $exceptionHandler(e);
- }
- }
- if (isFunction(controllerInstance.$doCheck)) {
- controllerScope.$watch(function() { controllerInstance.$doCheck(); });
- controllerInstance.$doCheck();
- }
- if (isFunction(controllerInstance.$onDestroy)) {
- controllerScope.$on('$destroy', function callOnDestroyHook() {
- controllerInstance.$onDestroy();
- });
- }
- });
-
- // PRELINKING
- for (i = 0, ii = preLinkFns.length; i < ii; i++) {
- linkFn = preLinkFns[i];
- invokeLinkFn(linkFn,
- linkFn.isolateScope ? isolateScope : scope,
- $element,
- attrs,
- linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
- transcludeFn
- );
- }
-
- // RECURSION
- // We only pass the isolate scope, if the isolate directive has a template,
- // otherwise the child elements do not belong to the isolate directive.
- var scopeToChild = scope;
- if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {
- scopeToChild = isolateScope;
- }
- childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
-
- // POSTLINKING
- for (i = postLinkFns.length - 1; i >= 0; i--) {
- linkFn = postLinkFns[i];
- invokeLinkFn(linkFn,
- linkFn.isolateScope ? isolateScope : scope,
- $element,
- attrs,
- linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
- transcludeFn
- );
- }
-
- // Trigger $postLink lifecycle hooks
- forEach(elementControllers, function(controller) {
- var controllerInstance = controller.instance;
- if (isFunction(controllerInstance.$postLink)) {
- controllerInstance.$postLink();
- }
- });
-
- // This is the function that is injected as `$transclude`.
- // Note: all arguments are optional!
- function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
- var transcludeControllers;
- // No scope passed in:
- if (!isScope(scope)) {
- slotName = futureParentElement;
- futureParentElement = cloneAttachFn;
- cloneAttachFn = scope;
- scope = undefined;
- }
-
- if (hasElementTranscludeDirective) {
- transcludeControllers = elementControllers;
- }
- if (!futureParentElement) {
- futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
- }
- if (slotName) {
- // slotTranscludeFn can be one of three things:
- // * a transclude function - a filled slot
- // * `null` - an optional slot that was not filled
- // * `undefined` - a slot that was not declared (i.e. invalid)
- var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
- if (slotTranscludeFn) {
- return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
- } else if (isUndefined(slotTranscludeFn)) {
- throw $compileMinErr('noslot',
- 'No parent directive that requires a transclusion with slot name "{0}". ' +
- 'Element: {1}',
- slotName, startingTag($element));
- }
- } else {
- return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
- }
- }
- }
- }
-
- function getControllers(directiveName, require, $element, elementControllers) {
- var value;
-
- if (isString(require)) {
- var match = require.match(REQUIRE_PREFIX_REGEXP);
- var name = require.substring(match[0].length);
- var inheritType = match[1] || match[3];
- var optional = match[2] === '?';
-
- //If only parents then start at the parent element
- if (inheritType === '^^') {
- $element = $element.parent();
- //Otherwise attempt getting the controller from elementControllers in case
- //the element is transcluded (and has no data) and to avoid .data if possible
- } else {
- value = elementControllers && elementControllers[name];
- value = value && value.instance;
- }
-
- if (!value) {
- var dataName = '$' + name + 'Controller';
- value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
- }
-
- if (!value && !optional) {
- throw $compileMinErr('ctreq',
- "Controller '{0}', required by directive '{1}', can't be found!",
- name, directiveName);
- }
- } else if (isArray(require)) {
- value = [];
- for (var i = 0, ii = require.length; i < ii; i++) {
- value[i] = getControllers(directiveName, require[i], $element, elementControllers);
- }
- } else if (isObject(require)) {
- value = {};
- forEach(require, function(controller, property) {
- value[property] = getControllers(directiveName, controller, $element, elementControllers);
- });
- }
-
- return value || null;
- }
-
- function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) {
- var elementControllers = createMap();
- for (var controllerKey in controllerDirectives) {
- var directive = controllerDirectives[controllerKey];
- var locals = {
- $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
- $element: $element,
- $attrs: attrs,
- $transclude: transcludeFn
- };
-
- var controller = directive.controller;
- if (controller == '@') {
- controller = attrs[directive.name];
- }
-
- var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
-
- // For directives with element transclusion the element is a comment.
- // In this case .data will not attach any data.
- // Instead, we save the controllers for the element in a local hash and attach to .data
- // later, once we have the actual element.
- elementControllers[directive.name] = controllerInstance;
- $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
- }
- return elementControllers;
- }
-
- // Depending upon the context in which a directive finds itself it might need to have a new isolated
- // or child scope created. For instance:
- // * if the directive has been pulled into a template because another directive with a higher priority
- // asked for element transclusion
- // * if the directive itself asks for transclusion but it is at the root of a template and the original
- // element was replaced. See https://github.com/angular/angular.js/issues/12936
- function markDirectiveScope(directives, isolateScope, newScope) {
- for (var j = 0, jj = directives.length; j < jj; j++) {
- directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope});
- }
- }
-
- /**
- * looks up the directive and decorates it with exception handling and proper parameters. We
- * call this the boundDirective.
- *
- * @param {string} name name of the directive to look up.
- * @param {string} location The directive must be found in specific format.
- * String containing any of theses characters:
- *
- * * `E`: element name
- * * `A': attribute
- * * `C`: class
- * * `M`: comment
- * @returns {boolean} true if directive was added.
- */
- function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName,
- endAttrName) {
- if (name === ignoreDirective) return null;
- var match = null;
- if (hasDirectives.hasOwnProperty(name)) {
- for (var directive, directives = $injector.get(name + Suffix),
- i = 0, ii = directives.length; i < ii; i++) {
- try {
- directive = directives[i];
- if ((isUndefined(maxPriority) || maxPriority > directive.priority) &&
- directive.restrict.indexOf(location) != -1) {
- if (startAttrName) {
- directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
- }
- if (!directive.$$bindings) {
- var bindings = directive.$$bindings =
- parseDirectiveBindings(directive, directive.name);
- if (isObject(bindings.isolateScope)) {
- directive.$$isolateBindings = bindings.isolateScope;
- }
- }
- tDirectives.push(directive);
- match = directive;
- }
- } catch (e) { $exceptionHandler(e); }
- }
- }
- return match;
- }
-
-
- /**
- * looks up the directive and returns true if it is a multi-element directive,
- * and therefore requires DOM nodes between -start and -end markers to be grouped
- * together.
- *
- * @param {string} name name of the directive to look up.
- * @returns true if directive was registered as multi-element.
- */
- function directiveIsMultiElement(name) {
- if (hasDirectives.hasOwnProperty(name)) {
- for (var directive, directives = $injector.get(name + Suffix),
- i = 0, ii = directives.length; i < ii; i++) {
- directive = directives[i];
- if (directive.multiElement) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * When the element is replaced with HTML template then the new attributes
- * on the template need to be merged with the existing attributes in the DOM.
- * The desired effect is to have both of the attributes present.
- *
- * @param {object} dst destination attributes (original DOM)
- * @param {object} src source attributes (from the directive template)
- */
- function mergeTemplateAttributes(dst, src) {
- var srcAttr = src.$attr,
- dstAttr = dst.$attr,
- $element = dst.$$element;
-
- // reapply the old attributes to the new element
- forEach(dst, function(value, key) {
- if (key.charAt(0) != '$') {
- if (src[key] && src[key] !== value) {
- value += (key === 'style' ? ';' : ' ') + src[key];
- }
- dst.$set(key, value, true, srcAttr[key]);
- }
- });
-
- // copy the new attributes on the old attrs object
- forEach(src, function(value, key) {
- // Check if we already set this attribute in the loop above.
- // `dst` will never contain hasOwnProperty as DOM parser won't let it.
- // You will get an "InvalidCharacterError: DOM Exception 5" error if you
- // have an attribute like "has-own-property" or "data-has-own-property", etc.
- if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') {
- dst[key] = value;
-
- if (key !== 'class' && key !== 'style') {
- dstAttr[key] = srcAttr[key];
- }
- }
- });
- }
-
-
- function compileTemplateUrl(directives, $compileNode, tAttrs,
- $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) {
- var linkQueue = [],
- afterTemplateNodeLinkFn,
- afterTemplateChildLinkFn,
- beforeTemplateCompileNode = $compileNode[0],
- origAsyncDirective = directives.shift(),
- derivedSyncDirective = inherit(origAsyncDirective, {
- templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective
- }),
- templateUrl = (isFunction(origAsyncDirective.templateUrl))
- ? origAsyncDirective.templateUrl($compileNode, tAttrs)
- : origAsyncDirective.templateUrl,
- templateNamespace = origAsyncDirective.templateNamespace;
-
- $compileNode.empty();
-
- $templateRequest(templateUrl)
- .then(function(content) {
- var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn;
-
- content = denormalizeTemplate(content);
-
- if (origAsyncDirective.replace) {
- if (jqLiteIsTextNode(content)) {
- $template = [];
- } else {
- $template = removeComments(wrapTemplate(templateNamespace, trim(content)));
- }
- compileNode = $template[0];
-
- 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);
- }
-
- tempTemplateAttrs = {$attr: {}};
- replaceWith($rootElement, $compileNode, compileNode);
- var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs);
-
- if (isObject(origAsyncDirective.scope)) {
- // the original directive that caused the template to be loaded async required
- // an isolate scope
- markDirectiveScope(templateDirectives, true);
- }
- directives = templateDirectives.concat(directives);
- mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
- } else {
- compileNode = beforeTemplateCompileNode;
- $compileNode.html(content);
- }
-
- directives.unshift(derivedSyncDirective);
-
- afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs,
- childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns,
- previousCompileContext);
- forEach($rootElement, function(node, i) {
- if (node == compileNode) {
- $rootElement[i] = $compileNode[0];
- }
- });
- afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
-
- while (linkQueue.length) {
- var scope = linkQueue.shift(),
- 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)) {
- // it was cloned therefore we have to clone as well.
- linkNode = jqLiteClone(compileNode);
- }
- replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
-
- // Copy in CSS classes from original node
- safeAddClass(jqLite(linkNode), oldClasses);
- }
- if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
- childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
- } else {
- childBoundTranscludeFn = boundTranscludeFn;
- }
- afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement,
- childBoundTranscludeFn);
- }
- linkQueue = null;
- });
-
- return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
- var childBoundTranscludeFn = boundTranscludeFn;
- if (scope.$$destroyed) return;
- if (linkQueue) {
- linkQueue.push(scope,
- node,
- rootElement,
- childBoundTranscludeFn);
- } else {
- if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
- childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
- }
- afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
- }
- };
- }
-
-
- /**
- * Sorting function for bound directives.
- */
- function byPriority(a, b) {
- var diff = b.priority - a.priority;
- if (diff !== 0) return diff;
- if (a.name !== b.name) return (a.name < b.name) ? -1 : 1;
- return a.index - b.index;
- }
-
- function assertNoDuplicate(what, previousDirective, directive, element) {
-
- function wrapModuleNameIfDefined(moduleName) {
- return moduleName ?
- (' (module: ' + moduleName + ')') :
- '';
- }
-
- if (previousDirective) {
- throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}',
- previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName),
- directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element));
- }
- }
-
-
- function addTextInterpolateDirective(directives, text) {
- var interpolateFn = $interpolate(text, true);
- if (interpolateFn) {
- directives.push({
- priority: 0,
- compile: function textInterpolateCompileFn(templateNode) {
- var templateNodeParent = templateNode.parent(),
- hasCompileParent = !!templateNodeParent.length;
-
- // When transcluding a template that has bindings in the root
- // we don't have a parent and thus need to add the class during linking fn.
- if (hasCompileParent) compile.$$addBindingClass(templateNodeParent);
-
- return function textInterpolateLinkFn(scope, node) {
- var parent = node.parent();
- if (!hasCompileParent) compile.$$addBindingClass(parent);
- compile.$$addBindingInfo(parent, interpolateFn.expressions);
- scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
- node[0].nodeValue = value;
- });
- };
- }
- });
- }
- }
-
-
- function wrapTemplate(type, template) {
- type = lowercase(type || 'html');
- switch (type) {
- case 'svg':
- case 'math':
- var wrapper = window.document.createElement('div');
- wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>';
- return wrapper.childNodes[0].childNodes;
- default:
- return template;
- }
- }
-
-
- function getTrustedContext(node, attrNormalizedName) {
- if (attrNormalizedName == "srcdoc") {
- return $sce.HTML;
- }
- var tag = nodeName_(node);
- // maction[xlink:href] can source SVG. It's not limited to <maction>.
- if (attrNormalizedName == "xlinkHref" ||
- (tag == "form" && attrNormalizedName == "action") ||
- (tag != "img" && (attrNormalizedName == "src" ||
- attrNormalizedName == "ngSrc"))) {
- return $sce.RESOURCE_URL;
- }
- }
-
-
- function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) {
- var trustedContext = getTrustedContext(node, name);
- allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing;
-
- var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing);
-
- // no interpolation found -> ignore
- if (!interpolateFn) return;
-
-
- if (name === "multiple" && nodeName_(node) === "select") {
- throw $compileMinErr("selmulti",
- "Binding to the 'multiple' attribute is not supported. Element: {0}",
- startingTag(node));
- }
-
- directives.push({
- priority: 100,
- compile: function() {
- return {
- pre: function attrInterpolatePreLinkFn(scope, element, attr) {
- var $$observers = (attr.$$observers || (attr.$$observers = createMap()));
-
- if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
- throw $compileMinErr('nodomevents',
- "Interpolations for HTML DOM event attributes are disallowed. Please use the " +
- "ng- versions (such as ng-click instead of onclick) instead.");
- }
-
- // If the attribute has changed since last $interpolate()ed
- var newValue = attr[name];
- if (newValue !== value) {
- // we need to interpolate again since the attribute value has been updated
- // (e.g. by another directive's compile function)
- // ensure unset/empty values make interpolateFn falsy
- interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing);
- value = newValue;
- }
-
- // if attribute was updated so that there is no interpolation going on we don't want to
- // register any observers
- if (!interpolateFn) return;
-
- // initialize attr object so that it's ready in case we need the value for isolate
- // scope initialization, otherwise the value would not be available from isolate
- // directive's linking fn during linking phase
- attr[name] = interpolateFn(scope);
-
- ($$observers[name] || ($$observers[name] = [])).$$inter = true;
- (attr.$$observers && attr.$$observers[name].$$scope || scope).
- $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
- //special case for class attribute addition + removal
- //so that class changes can tap into the animation
- //hooks provided by the $animate service. Be sure to
- //skip animations when the first digest occurs (when
- //both the new and the old values are the same) since
- //the CSS classes are the non-interpolated values
- if (name === 'class' && newValue != oldValue) {
- attr.$updateClass(newValue, oldValue);
- } else {
- attr.$set(name, newValue);
- }
- });
- }
- };
- }
- });
- }
-
-
- /**
- * This is a special jqLite.replaceWith, which can replace items which
- * have no parents, provided that the containing jqLite collection is provided.
- *
- * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
- * in the root of the tree.
- * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep
- * the shell, but replace its DOM node reference.
- * @param {Node} newNode The new DOM node.
- */
- function replaceWith($rootElement, elementsToRemove, newNode) {
- var firstElementToRemove = elementsToRemove[0],
- removeCount = elementsToRemove.length,
- parent = firstElementToRemove.parentNode,
- i, ii;
-
- if ($rootElement) {
- for (i = 0, ii = $rootElement.length; i < ii; i++) {
- if ($rootElement[i] == firstElementToRemove) {
- $rootElement[i++] = newNode;
- for (var j = i, j2 = j + removeCount - 1,
- jj = $rootElement.length;
- j < jj; j++, j2++) {
- if (j2 < jj) {
- $rootElement[j] = $rootElement[j2];
- } else {
- delete $rootElement[j];
- }
- }
- $rootElement.length -= removeCount - 1;
-
- // If the replaced element is also the jQuery .context then replace it
- // .context is a deprecated jQuery api, so we should set it only when jQuery set it
- // http://api.jquery.com/context/
- if ($rootElement.context === firstElementToRemove) {
- $rootElement.context = newNode;
- }
- break;
- }
- }
- }
-
- if (parent) {
- parent.replaceChild(newNode, firstElementToRemove);
- }
-
- // Append all the `elementsToRemove` to a fragment. This will...
- // - remove them from the DOM
- // - allow them to still be traversed with .nextSibling
- // - allow a single fragment.qSA to fetch all elements being removed
- var fragment = window.document.createDocumentFragment();
- for (i = 0; i < removeCount; i++) {
- fragment.appendChild(elementsToRemove[i]);
- }
-
- if (jqLite.hasData(firstElementToRemove)) {
- // Copy over user data (that includes Angular's $scope etc.). Don't copy private
- // data here because there's no public interface in jQuery to do that and copying over
- // event listeners (which is the main use of private data) wouldn't work anyway.
- jqLite.data(newNode, jqLite.data(firstElementToRemove));
-
- // Remove $destroy event listeners from `firstElementToRemove`
- jqLite(firstElementToRemove).off('$destroy');
- }
-
- // Cleanup any data/listeners on the elements and children.
- // This includes invoking the $destroy event on any elements with listeners.
- jqLite.cleanData(fragment.querySelectorAll('*'));
-
- // Update the jqLite collection to only contain the `newNode`
- for (i = 1; i < removeCount; i++) {
- delete elementsToRemove[i];
- }
- elementsToRemove[0] = newNode;
- elementsToRemove.length = 1;
- }
-
-
- function cloneAndAnnotateFn(fn, annotation) {
- return extend(function() { return fn.apply(null, arguments); }, fn, annotation);
- }
-
-
- function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) {
- try {
- linkFn(scope, $element, attrs, controllers, transcludeFn);
- } catch (e) {
- $exceptionHandler(e, startingTag($element));
- }
- }
-
-
- // Set up $watches for isolate scope and controller bindings. This process
- // only occurs for isolate scopes and new scopes with controllerAs.
- function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
- var removeWatchCollection = [];
- var initialChanges = {};
- var changes;
- forEach(bindings, function initializeBinding(definition, scopeName) {
- var attrName = definition.attrName,
- optional = definition.optional,
- mode = definition.mode, // @, =, <, or &
- lastValue,
- parentGet, parentSet, compare, removeWatch;
-
- switch (mode) {
-
- case '@':
- if (!optional && !hasOwnProperty.call(attrs, attrName)) {
- destination[scopeName] = attrs[attrName] = void 0;
- }
- attrs.$observe(attrName, function(value) {
- if (isString(value) || isBoolean(value)) {
- var oldValue = destination[scopeName];
- recordChanges(scopeName, value, oldValue);
- destination[scopeName] = value;
- }
- });
- attrs.$$observers[attrName].$$scope = scope;
- lastValue = attrs[attrName];
- if (isString(lastValue)) {
- // If the attribute has been provided then we trigger an interpolation to ensure
- // the value is there for use in the link fn
- destination[scopeName] = $interpolate(lastValue)(scope);
- } else if (isBoolean(lastValue)) {
- // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted
- // the value to boolean rather than a string, so we special case this situation
- destination[scopeName] = lastValue;
- }
- initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
- break;
-
- case '=':
- if (!hasOwnProperty.call(attrs, attrName)) {
- if (optional) break;
- attrs[attrName] = void 0;
- }
- if (optional && !attrs[attrName]) break;
-
- parentGet = $parse(attrs[attrName]);
- if (parentGet.literal) {
- compare = equals;
- } else {
- compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); };
- }
- parentSet = parentGet.assign || function() {
- // reset the change, or we will throw this exception on every $digest
- lastValue = destination[scopeName] = parentGet(scope);
- throw $compileMinErr('nonassign',
- "Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!",
- attrs[attrName], attrName, directive.name);
- };
- lastValue = destination[scopeName] = parentGet(scope);
- var parentValueWatch = function parentValueWatch(parentValue) {
- if (!compare(parentValue, destination[scopeName])) {
- // we are out of sync and need to copy
- if (!compare(parentValue, lastValue)) {
- // parent changed and it has precedence
- destination[scopeName] = parentValue;
- } else {
- // if the parent can be assigned then do so
- parentSet(scope, parentValue = destination[scopeName]);
- }
- }
- return lastValue = parentValue;
- };
- parentValueWatch.$stateful = true;
- if (definition.collection) {
- removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
- } else {
- removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
- }
- removeWatchCollection.push(removeWatch);
- break;
-
- case '<':
- if (!hasOwnProperty.call(attrs, attrName)) {
- if (optional) break;
- attrs[attrName] = void 0;
- }
- if (optional && !attrs[attrName]) break;
-
- parentGet = $parse(attrs[attrName]);
-
- var initialValue = destination[scopeName] = parentGet(scope);
- initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
-
- removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
- if (oldValue === newValue) {
- if (oldValue === initialValue) return;
- oldValue = initialValue;
- }
- recordChanges(scopeName, newValue, oldValue);
- destination[scopeName] = newValue;
- }, parentGet.literal);
-
- removeWatchCollection.push(removeWatch);
- break;
-
- case '&':
- // Don't assign Object.prototype method to scope
- parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
-
- // Don't assign noop to destination if expression is not valid
- if (parentGet === noop && optional) break;
-
- destination[scopeName] = function(locals) {
- return parentGet(scope, locals);
- };
- break;
- }
- });
-
- function recordChanges(key, currentValue, previousValue) {
- if (isFunction(destination.$onChanges) && currentValue !== previousValue) {
- // If we have not already scheduled the top level onChangesQueue handler then do so now
- if (!onChangesQueue) {
- scope.$$postDigest(flushOnChangesQueue);
- onChangesQueue = [];
- }
- // If we have not already queued a trigger of onChanges for this controller then do so now
- if (!changes) {
- changes = {};
- onChangesQueue.push(triggerOnChangesHook);
- }
- // If the has been a change on this property already then we need to reuse the previous value
- if (changes[key]) {
- previousValue = changes[key].previousValue;
- }
- // Store this change
- changes[key] = new SimpleChange(previousValue, currentValue);
- }
- }
-
- function triggerOnChangesHook() {
- destination.$onChanges(changes);
- // Now clear the changes so that we schedule onChanges when more changes arrive
- changes = undefined;
- }
-
- return {
- initialChanges: initialChanges,
- removeWatches: removeWatchCollection.length && function removeWatches() {
- for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
- removeWatchCollection[i]();
- }
- }
- };
- }
- }];
-}
-
-function SimpleChange(previous, current) {
- this.previousValue = previous;
- this.currentValue = current;
-}
-SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
-
-
-var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
-/**
- * Converts all accepted directives format into proper directive name.
- * @param name Name to normalize
- */
-function directiveNormalize(name) {
- return camelCase(name.replace(PREFIX_REGEXP, ''));
-}
-
-/**
- * @ngdoc type
- * @name $compile.directive.Attributes
- *
- * @description
- * A shared object between directive compile / linking functions which contains normalized DOM
- * element attributes. The values reflect current binding state `{{ }}`. The normalization is
- * needed since all of these are treated as equivalent in Angular:
- *
- * ```
- * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
- * ```
- */
-
-/**
- * @ngdoc property
- * @name $compile.directive.Attributes#$attr
- *
- * @description
- * A map of DOM element attribute names to the normalized name. This is
- * needed to do reverse lookup from normalized name back to actual name.
- */
-
-
-/**
- * @ngdoc method
- * @name $compile.directive.Attributes#$set
- * @kind function
- *
- * @description
- * Set DOM element attribute value.
- *
- *
- * @param {string} name Normalized element attribute name of the property to modify. The name is
- * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
- * property to the original name.
- * @param {string} value Value to set the attribute to. The value can be an interpolated string.
- */
-
-
-
-/**
- * Closure compiler type information
- */
-
-function nodesetLinkingFn(
- /* angular.Scope */ scope,
- /* NodeList */ nodeList,
- /* Element */ rootElement,
- /* function(Function) */ boundTranscludeFn
-) {}
-
-function directiveLinkingFn(
- /* nodesetLinkingFn */ nodesetLinkingFn,
- /* angular.Scope */ scope,
- /* Node */ node,
- /* Element */ rootElement,
- /* function(Function) */ boundTranscludeFn
-) {}
-
-function tokenDifference(str1, str2) {
- var values = '',
- tokens1 = str1.split(/\s+/),
- tokens2 = str2.split(/\s+/);
-
- outer:
- for (var i = 0; i < tokens1.length; i++) {
- var token = tokens1[i];
- for (var j = 0; j < tokens2.length; j++) {
- if (token == tokens2[j]) continue outer;
- }
- 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;
-}
-
-var $controllerMinErr = minErr('$controller');
-
-
-var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/;
-function identifierForController(controller, ident) {
- if (ident && isString(ident)) return ident;
- if (isString(controller)) {
- var match = CNTRL_REG.exec(controller);
- if (match) return match[3];
- }
-}
-
-
-/**
- * @ngdoc provider
- * @name $controllerProvider
- * @description
- * The {@link ng.$controller $controller service} is used by Angular to create new
- * controllers.
- *
- * This provider allows controller registration via the
- * {@link ng.$controllerProvider#register register} method.
- */
-function $ControllerProvider() {
- var controllers = {},
- globals = false;
-
- /**
- * @ngdoc method
- * @name $controllerProvider#has
- * @param {string} name Controller name to check.
- */
- this.has = function(name) {
- return controllers.hasOwnProperty(name);
- };
-
- /**
- * @ngdoc method
- * @name $controllerProvider#register
- * @param {string|Object} name Controller name, or an object map of controllers where the keys are
- * the names and the values are the constructors.
- * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI
- * annotations in the array notation).
- */
- this.register = function(name, constructor) {
- assertNotHasOwnProperty(name, 'controller');
- if (isObject(name)) {
- extend(controllers, name);
- } else {
- controllers[name] = constructor;
- }
- };
-
- /**
- * @ngdoc method
- * @name $controllerProvider#allowGlobals
- * @description If called, allows `$controller` to find controller constructors on `window`
- */
- this.allowGlobals = function() {
- globals = true;
- };
-
-
- this.$get = ['$injector', '$window', function($injector, $window) {
-
- /**
- * @ngdoc service
- * @name $controller
- * @requires $injector
- *
- * @param {Function|string} constructor If called with a function then it's considered to be the
- * controller constructor function. Otherwise it's considered to be a string which is used
- * to retrieve the controller constructor using the following steps:
- *
- * * check if a controller with given name is registered via `$controllerProvider`
- * * check if evaluating the string on the current scope returns a constructor
- * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
- * `window` object (not recommended)
- *
- * The string can use the `controller as property` syntax, where the controller instance is published
- * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
- * to work correctly.
- *
- * @param {Object} locals Injection locals for Controller.
- * @return {Object} Instance of given controller.
- *
- * @description
- * `$controller` service is responsible for instantiating controllers.
- *
- * It's just a simple call to {@link auto.$injector $injector}, but extracted into
- * a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
- */
- return function $controller(expression, locals, later, ident) {
- // PRIVATE API:
- // param `later` --- indicates that the controller's constructor is invoked at a later time.
- // If true, $controller will allocate the object with the correct
- // prototype chain, but will not invoke the controller until a returned
- // callback is invoked.
- // param `ident` --- An optional label which overrides the label parsed from the controller
- // expression, if any.
- var instance, match, constructor, identifier;
- later = later === true;
- if (ident && isString(ident)) {
- identifier = ident;
- }
-
- if (isString(expression)) {
- match = expression.match(CNTRL_REG);
- if (!match) {
- throw $controllerMinErr('ctrlfmt',
- "Badly formed controller string '{0}'. " +
- "Must match `__name__ as __id__` or `__name__`.", expression);
- }
- constructor = match[1],
- identifier = identifier || match[3];
- expression = controllers.hasOwnProperty(constructor)
- ? controllers[constructor]
- : getter(locals.$scope, constructor, true) ||
- (globals ? getter($window, constructor, true) : undefined);
-
- assertArgFn(expression, constructor, true);
- }
-
- if (later) {
- // Instantiate controller later:
- // This machinery is used to create an instance of the object before calling the
- // controller's constructor itself.
- //
- // This allows properties to be added to the controller before the constructor is
- // invoked. Primarily, this is used for isolate scope bindings in $compile.
- //
- // This feature is not intended for use by applications, and is thus not documented
- // publicly.
- // Object creation: http://jsperf.com/create-constructor/2
- var controllerPrototype = (isArray(expression) ?
- expression[expression.length - 1] : expression).prototype;
- instance = Object.create(controllerPrototype || null);
-
- if (identifier) {
- addIdentifier(locals, identifier, instance, constructor || expression.name);
- }
-
- var instantiate;
- return instantiate = extend(function $controllerInit() {
- var result = $injector.invoke(expression, instance, locals, constructor);
- if (result !== instance && (isObject(result) || isFunction(result))) {
- instance = result;
- if (identifier) {
- // If result changed, re-assign controllerAs value to scope.
- addIdentifier(locals, identifier, instance, constructor || expression.name);
- }
- }
- return instance;
- }, {
- instance: instance,
- identifier: identifier
- });
- }
-
- instance = $injector.instantiate(expression, locals, constructor);
-
- if (identifier) {
- addIdentifier(locals, identifier, instance, constructor || expression.name);
- }
-
- return instance;
- };
-
- function addIdentifier(locals, identifier, instance, name) {
- if (!(locals && isObject(locals.$scope))) {
- throw minErr('$controller')('noscp',
- "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
- name, identifier);
- }
-
- locals.$scope[identifier] = instance;
- }
- }];
-}
-
-/**
- * @ngdoc service
- * @name $document
- * @requires $window
- *
- * @description
- * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object.
- *
- * @example
- <example module="documentExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <p>$document title: <b ng-bind="title"></b></p>
- <p>window.document title: <b ng-bind="windowTitle"></b></p>
- </div>
- </file>
- <file name="script.js">
- angular.module('documentExample', [])
- .controller('ExampleController', ['$scope', '$document', function($scope, $document) {
- $scope.title = $document[0].title;
- $scope.windowTitle = angular.element(window.document)[0].title;
- }]);
- </file>
- </example>
- */
-function $DocumentProvider() {
- this.$get = ['$window', function(window) {
- return jqLite(window.document);
- }];
-}
-
-/**
- * @ngdoc service
- * @name $exceptionHandler
- * @requires ng.$log
- *
- * @description
- * Any uncaught exception in angular expressions is delegated to this service.
- * The default implementation simply delegates to `$log.error` which logs it into
- * the browser console.
- *
- * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by
- * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing.
- *
- * ## Example:
- *
- * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught
- * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead
- * of `$log.error()`.
- *
- * ```js
- * angular.
- * module('exceptionOverwrite', []).
- * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) {
- * return function myExceptionHandler(exception, cause) {
- * logErrorsToBackend(exception, cause);
- * $log.warn(exception, cause);
- * };
- * }]);
- * ```
- *
- * <hr />
- * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind`
- * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler}
- * (unless executed during a digest).
- *
- * If you wish, you can manually delegate exceptions, e.g.
- * `try { ... } catch(e) { $exceptionHandler(e); }`
- *
- * @param {Error} exception Exception associated with the error.
- * @param {string=} cause Optional information about the context in which
- * the error was thrown.
- *
- */
-function $ExceptionHandlerProvider() {
- this.$get = ['$log', function($log) {
- return function(exception, cause) {
- $log.error.apply($log, arguments);
- };
- }];
-}
-
-var $$ForceReflowProvider = function() {
- this.$get = ['$document', function($document) {
- return function(domNode) {
- //the line below will force the browser to perform a repaint so
- //that all the animated elements within the animation frame will
- //be properly updated and drawn on screen. This is required to
- //ensure that the preparation animation is properly flushed so that
- //the active state picks up from there. DO NOT REMOVE THIS LINE.
- //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH
- //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND
- //WILL TAKE YEARS AWAY FROM YOUR LIFE.
- if (domNode) {
- if (!domNode.nodeType && domNode instanceof jqLite) {
- domNode = domNode[0];
- }
- } else {
- domNode = $document[0].body;
- }
- return domNode.offsetWidth + 1;
- };
- }];
-};
-
-var APPLICATION_JSON = 'application/json';
-var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'};
-var JSON_START = /^\[|^\{(?!\{)/;
-var JSON_ENDS = {
- '[': /]$/,
- '{': /}$/
-};
-var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
-var $httpMinErr = minErr('$http');
-var $httpMinErrLegacyFn = function(method) {
- return function() {
- throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method);
- };
-};
-
-function serializeValue(v) {
- if (isObject(v)) {
- return isDate(v) ? v.toISOString() : toJson(v);
- }
- return v;
-}
-
-
-function $HttpParamSerializerProvider() {
- /**
- * @ngdoc service
- * @name $httpParamSerializer
- * @description
- *
- * Default {@link $http `$http`} params serializer that converts objects to strings
- * according to the following rules:
- *
- * * `{'foo': 'bar'}` results in `foo=bar`
- * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object)
- * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element)
- * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object)
- *
- * Note that serializer will sort the request parameters alphabetically.
- * */
-
- this.$get = function() {
- return function ngParamSerializer(params) {
- if (!params) return '';
- var parts = [];
- forEachSorted(params, function(value, key) {
- if (value === null || isUndefined(value)) return;
- if (isArray(value)) {
- forEach(value, function(v) {
- parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
- });
- } else {
- parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value)));
- }
- });
-
- return parts.join('&');
- };
- };
-}
-
-function $HttpParamSerializerJQLikeProvider() {
- /**
- * @ngdoc service
- * @name $httpParamSerializerJQLike
- * @description
- *
- * Alternative {@link $http `$http`} params serializer that follows
- * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic.
- * The serializer will also sort the params alphabetically.
- *
- * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property:
- *
- * ```js
- * $http({
- * url: myUrl,
- * method: 'GET',
- * params: myParams,
- * paramSerializer: '$httpParamSerializerJQLike'
- * });
- * ```
- *
- * It is also possible to set it as the default `paramSerializer` in the
- * {@link $httpProvider#defaults `$httpProvider`}.
- *
- * Additionally, you can inject the serializer and use it explicitly, for example to serialize
- * form data for submission:
- *
- * ```js
- * .controller(function($http, $httpParamSerializerJQLike) {
- * //...
- *
- * $http({
- * url: myUrl,
- * method: 'POST',
- * data: $httpParamSerializerJQLike(myData),
- * headers: {
- * 'Content-Type': 'application/x-www-form-urlencoded'
- * }
- * });
- *
- * });
- * ```
- *
- * */
- this.$get = function() {
- return function jQueryLikeParamSerializer(params) {
- if (!params) return '';
- var parts = [];
- serialize(params, '', true);
- return parts.join('&');
-
- function serialize(toSerialize, prefix, topLevel) {
- if (toSerialize === null || isUndefined(toSerialize)) return;
- if (isArray(toSerialize)) {
- forEach(toSerialize, function(value, index) {
- serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']');
- });
- } else if (isObject(toSerialize) && !isDate(toSerialize)) {
- forEachSorted(toSerialize, function(value, key) {
- serialize(value, prefix +
- (topLevel ? '' : '[') +
- key +
- (topLevel ? '' : ']'));
- });
- } else {
- parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize)));
- }
- }
- };
- };
-}
-
-function defaultHttpResponseTransform(data, headers) {
- if (isString(data)) {
- // Strip json vulnerability protection prefix and trim whitespace
- var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim();
-
- if (tempData) {
- var contentType = headers('Content-Type');
- if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) {
- data = fromJson(tempData);
- }
- }
- }
-
- return data;
-}
-
-function isJsonLike(str) {
- var jsonStart = str.match(JSON_START);
- return jsonStart && JSON_ENDS[jsonStart[0]].test(str);
-}
-
-/**
- * Parse headers into key value object
- *
- * @param {string} headers Raw headers as a string
- * @returns {Object} Parsed headers as key value object
- */
-function parseHeaders(headers) {
- var parsed = createMap(), i;
-
- function fillInParsed(key, val) {
- if (key) {
- parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
- }
- }
-
- if (isString(headers)) {
- forEach(headers.split('\n'), function(line) {
- i = line.indexOf(':');
- fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1)));
- });
- } else if (isObject(headers)) {
- forEach(headers, function(headerVal, headerKey) {
- fillInParsed(lowercase(headerKey), trim(headerVal));
- });
- }
-
- return parsed;
-}
-
-
-/**
- * Returns a function that provides access to parsed headers.
- *
- * Headers are lazy parsed when first requested.
- * @see parseHeaders
- *
- * @param {(string|Object)} headers Headers to provide access to.
- * @returns {function(string=)} Returns a getter function which if called with:
- *
- * - if called with single an argument returns a single header value or null
- * - if called with no arguments returns an object containing all headers.
- */
-function headersGetter(headers) {
- var headersObj;
-
- return function(name) {
- if (!headersObj) headersObj = parseHeaders(headers);
-
- if (name) {
- var value = headersObj[lowercase(name)];
- if (value === void 0) {
- value = null;
- }
- return value;
- }
-
- return headersObj;
- };
-}
-
-
-/**
- * Chain all given functions
- *
- * This function is used for both request and response transforming
- *
- * @param {*} data Data to transform.
- * @param {function(string=)} headers HTTP headers getter fn.
- * @param {number} status HTTP status code of the response.
- * @param {(Function|Array.<Function>)} fns Function or an array of functions.
- * @returns {*} Transformed data.
- */
-function transformData(data, headers, status, fns) {
- if (isFunction(fns)) {
- return fns(data, headers, status);
- }
-
- forEach(fns, function(fn) {
- data = fn(data, headers, status);
- });
-
- return data;
-}
-
-
-function isSuccess(status) {
- return 200 <= status && status < 300;
-}
-
-
-/**
- * @ngdoc provider
- * @name $httpProvider
- * @description
- * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
- * */
-function $HttpProvider() {
- /**
- * @ngdoc property
- * @name $httpProvider#defaults
- * @description
- *
- * Object containing default values for all {@link ng.$http $http} requests.
- *
- * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with
- * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses
- * by default. See {@link $http#caching $http Caching} for more information.
- *
- * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token.
- * Defaults value is `'XSRF-TOKEN'`.
- *
- * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the
- * XSRF token. Defaults value is `'X-XSRF-TOKEN'`.
- *
- * - **`defaults.headers`** - {Object} - Default headers for all $http requests.
- * Refer to {@link ng.$http#setting-http-headers $http} for documentation on
- * setting default headers.
- * - **`defaults.headers.common`**
- * - **`defaults.headers.post`**
- * - **`defaults.headers.put`**
- * - **`defaults.headers.patch`**
- *
- *
- * - **`defaults.paramSerializer`** - `{string|function(Object<string,string>):string}` - A function
- * used to the prepare string representation of request parameters (specified as an object).
- * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
- * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
- *
- **/
- var defaults = this.defaults = {
- // transform incoming response data
- transformResponse: [defaultHttpResponseTransform],
-
- // transform outgoing request data
- transformRequest: [function(d) {
- return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d;
- }],
-
- // default headers
- headers: {
- common: {
- 'Accept': 'application/json, text/plain, */*'
- },
- post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON),
- put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON),
- patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON)
- },
-
- xsrfCookieName: 'XSRF-TOKEN',
- xsrfHeaderName: 'X-XSRF-TOKEN',
-
- paramSerializer: '$httpParamSerializer'
- };
-
- var useApplyAsync = false;
- /**
- * @ngdoc method
- * @name $httpProvider#useApplyAsync
- * @description
- *
- * Configure $http service to combine processing of multiple http responses received at around
- * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in
- * significant performance improvement for bigger applications that make many HTTP requests
- * concurrently (common during application bootstrap).
- *
- * Defaults to false. If no value is specified, returns the current configured value.
- *
- * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred
- * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window
- * to load and share the same digest cycle.
- *
- * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
- * otherwise, returns the current configured value.
- **/
- this.useApplyAsync = function(value) {
- if (isDefined(value)) {
- useApplyAsync = !!value;
- return this;
- }
- return useApplyAsync;
- };
-
- var useLegacyPromise = true;
- /**
- * @ngdoc method
- * @name $httpProvider#useLegacyPromiseExtensions
- * @description
- *
- * Configure `$http` service to return promises without the shorthand methods `success` and `error`.
- * This should be used to make sure that applications work without these methods.
- *
- * Defaults to true. If no value is specified, returns the current configured value.
- *
- * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods.
- *
- * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
- * otherwise, returns the current configured value.
- **/
- this.useLegacyPromiseExtensions = function(value) {
- if (isDefined(value)) {
- useLegacyPromise = !!value;
- return this;
- }
- return useLegacyPromise;
- };
-
- /**
- * @ngdoc property
- * @name $httpProvider#interceptors
- * @description
- *
- * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http}
- * pre-processing of request or postprocessing of responses.
- *
- * These service factories are ordered by request, i.e. they are applied in the same order as the
- * array, on request, but reverse order, on response.
- *
- * {@link ng.$http#interceptors Interceptors detailed info}
- **/
- var interceptorFactories = this.interceptors = [];
-
- this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
- function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
-
- var defaultCache = $cacheFactory('$http');
-
- /**
- * Make sure that default param serializer is exposed as a function
- */
- defaults.paramSerializer = isString(defaults.paramSerializer) ?
- $injector.get(defaults.paramSerializer) : defaults.paramSerializer;
-
- /**
- * Interceptors stored in reverse order. Inner interceptors before outer interceptors.
- * The reversal is needed so that we can build up the interception chain around the
- * server request.
- */
- var reversedInterceptors = [];
-
- forEach(interceptorFactories, function(interceptorFactory) {
- reversedInterceptors.unshift(isString(interceptorFactory)
- ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
- });
-
- /**
- * @ngdoc service
- * @kind function
- * @name $http
- * @requires ng.$httpBackend
- * @requires $cacheFactory
- * @requires $rootScope
- * @requires $q
- * @requires $injector
- *
- * @description
- * The `$http` service is a core Angular service that facilitates communication with the remote
- * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest)
- * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP).
- *
- * For unit testing applications that use `$http` service, see
- * {@link ngMock.$httpBackend $httpBackend mock}.
- *
- * For a higher level of abstraction, please check out the {@link ngResource.$resource
- * $resource} service.
- *
- * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by
- * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage
- * it is important to familiarize yourself with these APIs and the guarantees they provide.
- *
- *
- * ## General usage
- * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} —
- * that is used to generate an HTTP request and returns a {@link ng.$q promise}.
- *
- * ```js
- * // Simple GET request example:
- * $http({
- * method: 'GET',
- * url: '/someUrl'
- * }).then(function successCallback(response) {
- * // this callback will be called asynchronously
- * // when the response is available
- * }, function errorCallback(response) {
- * // called asynchronously if an error occurs
- * // or server returns response with an error status.
- * });
- * ```
- *
- * The response object has these properties:
- *
- * - **data** – `{string|Object}` – The response body transformed with the transform
- * functions.
- * - **status** – `{number}` – HTTP status code of the response.
- * - **headers** – `{function([headerName])}` – Header getter function.
- * - **config** – `{Object}` – The configuration object that was used to generate the request.
- * - **statusText** – `{string}` – HTTP status text of the response.
- *
- * A response status code between 200 and 299 is considered a success status and will result in
- * the success callback being called. Any response status code outside of that range is
- * considered an error status and will result in the error callback being called.
- * Also, status codes less than -1 are normalized to zero. -1 usually means the request was
- * aborted, e.g. using a `config.timeout`.
- * Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning
- * that the outcome (success or error) will be determined by the final response status code.
- *
- *
- * ## Shortcut methods
- *
- * Shortcut methods are also available. All shortcut methods require passing in the URL, and
- * request data must be passed in for POST/PUT requests. An optional config can be passed as the
- * last argument.
- *
- * ```js
- * $http.get('/someUrl', config).then(successCallback, errorCallback);
- * $http.post('/someUrl', data, config).then(successCallback, errorCallback);
- * ```
- *
- * Complete list of shortcut methods:
- *
- * - {@link ng.$http#get $http.get}
- * - {@link ng.$http#head $http.head}
- * - {@link ng.$http#post $http.post}
- * - {@link ng.$http#put $http.put}
- * - {@link ng.$http#delete $http.delete}
- * - {@link ng.$http#jsonp $http.jsonp}
- * - {@link ng.$http#patch $http.patch}
- *
- *
- * ## Writing Unit Tests that use $http
- * When unit testing (using {@link ngMock ngMock}), it is necessary to call
- * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
- * request using trained responses.
- *
- * ```
- * $httpBackend.expectGET(...);
- * $http.get(...);
- * $httpBackend.flush();
- * ```
- *
- * ## Deprecation Notice
- * <div class="alert alert-danger">
- * The `$http` legacy promise methods `success` and `error` have been deprecated.
- * Use the standard `then` method instead.
- * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to
- * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error.
- * </div>
- *
- * ## Setting HTTP Headers
- *
- * The $http service will automatically add certain HTTP headers to all requests. These defaults
- * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration
- * object, which currently contains this default configuration:
- *
- * - `$httpProvider.defaults.headers.common` (headers that are common for all requests):
- * - `Accept: application/json, text/plain, * / *`
- * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests)
- * - `Content-Type: application/json`
- * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests)
- * - `Content-Type: application/json`
- *
- * To add or overwrite these defaults, simply add or remove a property from these configuration
- * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object
- * with the lowercased HTTP method name as the key, e.g.
- * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`.
- *
- * The defaults can also be set at runtime via the `$http.defaults` object in the same
- * fashion. For example:
- *
- * ```
- * module.run(function($http) {
- * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w';
- * });
- * ```
- *
- * In addition, you can supply a `headers` property in the config object passed when
- * calling `$http(config)`, which overrides the defaults without changing them globally.
- *
- * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis,
- * Use the `headers` property, setting the desired header to `undefined`. For example:
- *
- * ```js
- * var req = {
- * method: 'POST',
- * url: 'http://example.com',
- * headers: {
- * 'Content-Type': undefined
- * },
- * data: { test: 'test' }
- * }
- *
- * $http(req).then(function(){...}, function(){...});
- * ```
- *
- * ## Transforming Requests and Responses
- *
- * Both requests and responses can be transformed using transformation functions: `transformRequest`
- * and `transformResponse`. These properties can be a single function that returns
- * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions,
- * which allows you to `push` or `unshift` a new transformation function into the transformation chain.
- *
- * <div class="alert alert-warning">
- * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline.
- * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference).
- * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest
- * function will be reflected on the scope and in any templates where the object is data-bound.
- * To prevent this, transform functions should have no side-effects.
- * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return.
- * </div>
- *
- * ### Default Transformations
- *
- * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and
- * `defaults.transformResponse` properties. If a request does not provide its own transformations
- * then these will be applied.
- *
- * You can augment or replace the default transformations by modifying these properties by adding to or
- * replacing the array.
- *
- * Angular provides the following default transformations:
- *
- * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`):
- *
- * - If the `data` property of the request configuration object contains an object, serialize it
- * into JSON format.
- *
- * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`):
- *
- * - If XSRF prefix is detected, strip it (see Security Considerations section below).
- * - If JSON response is detected, deserialize it using a JSON parser.
- *
- *
- * ### Overriding the Default Transformations Per Request
- *
- * If you wish to override the request/response transformations only for a single request then provide
- * `transformRequest` and/or `transformResponse` properties on the configuration object passed
- * into `$http`.
- *
- * Note that if you provide these properties on the config object the default transformations will be
- * overwritten. If you wish to augment the default transformations then you must include them in your
- * local transformation array.
- *
- * The following code demonstrates adding a new response transformation to be run after the default response
- * transformations have been run.
- *
- * ```js
- * function appendTransform(defaults, transform) {
- *
- * // We can't guarantee that the default transformation is an array
- * defaults = angular.isArray(defaults) ? defaults : [defaults];
- *
- * // Append the new transformation to the defaults
- * return defaults.concat(transform);
- * }
- *
- * $http({
- * url: '...',
- * method: 'GET',
- * transformResponse: appendTransform($http.defaults.transformResponse, function(value) {
- * return doTransform(value);
- * })
- * });
- * ```
- *
- *
- * ## Caching
- *
- * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must
- * set the config.cache value or the default cache value to TRUE or to a cache object (created
- * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes
- * precedence over the default cache value.
- *
- * In order to:
- * * cache all responses - set the default cache value to TRUE or to a cache object
- * * cache a specific response - set config.cache value to TRUE or to a cache object
- *
- * If caching is enabled, but neither the default cache nor config.cache are set to a cache object,
- * then the default `$cacheFactory("$http")` object is used.
- *
- * The default cache value can be set by updating the
- * {@link ng.$http#defaults `$http.defaults.cache`} property or the
- * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property.
- *
- * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using
- * the relevant cache object. The next time the same request is made, the response is returned
- * from the cache without sending a request to the server.
- *
- * Take note that:
- *
- * * Only GET and JSONP requests are cached.
- * * The cache key is the request URL including search parameters; headers are not considered.
- * * Cached responses are returned asynchronously, in the same way as responses from the server.
- * * If multiple identical requests are made using the same cache, which is not yet populated,
- * one request will be made to the server and remaining requests will return the same response.
- * * A cache-control header on the response does not affect if or how responses are cached.
- *
- *
- * ## Interceptors
- *
- * Before you start creating interceptors, be sure to understand the
- * {@link ng.$q $q and deferred/promise APIs}.
- *
- * For purposes of global error handling, authentication, or any kind of synchronous or
- * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be
- * able to intercept requests before they are handed to the server and
- * responses before they are handed over to the application code that
- * initiated these requests. The interceptors leverage the {@link ng.$q
- * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing.
- *
- * The interceptors are service factories that are registered with the `$httpProvider` by
- * adding them to the `$httpProvider.interceptors` array. The factory is called and
- * injected with dependencies (if specified) and returns the interceptor.
- *
- * There are two kinds of interceptors (and two kinds of rejection interceptors):
- *
- * * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to
- * modify the `config` object or create a new one. The function needs to return the `config`
- * object directly, or a promise containing the `config` or a new `config` object.
- * * `requestError`: interceptor gets called when a previous interceptor threw an error or
- * resolved with a rejection.
- * * `response`: interceptors get called with http `response` object. The function is free to
- * modify the `response` object or create a new one. The function needs to return the `response`
- * object directly, or as a promise containing the `response` or a new `response` object.
- * * `responseError`: interceptor gets called when a previous interceptor threw an error or
- * resolved with a rejection.
- *
- *
- * ```js
- * // register the interceptor as a service
- * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
- * return {
- * // optional method
- * 'request': function(config) {
- * // do something on success
- * return config;
- * },
- *
- * // optional method
- * 'requestError': function(rejection) {
- * // do something on error
- * if (canRecover(rejection)) {
- * return responseOrNewPromise
- * }
- * return $q.reject(rejection);
- * },
- *
- *
- *
- * // optional method
- * 'response': function(response) {
- * // do something on success
- * return response;
- * },
- *
- * // optional method
- * 'responseError': function(rejection) {
- * // do something on error
- * if (canRecover(rejection)) {
- * return responseOrNewPromise
- * }
- * return $q.reject(rejection);
- * }
- * };
- * });
- *
- * $httpProvider.interceptors.push('myHttpInterceptor');
- *
- *
- * // alternatively, register the interceptor via an anonymous factory
- * $httpProvider.interceptors.push(function($q, dependency1, dependency2) {
- * return {
- * 'request': function(config) {
- * // same as above
- * },
- *
- * 'response': function(response) {
- * // same as above
- * }
- * };
- * });
- * ```
- *
- * ## Security Considerations
- *
- * When designing web applications, consider security threats from:
- *
- * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx)
- * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery)
- *
- * Both server and the client must cooperate in order to eliminate these threats. Angular comes
- * pre-configured with strategies that address these issues, but for this to work backend server
- * cooperation is required.
- *
- * ### JSON Vulnerability Protection
- *
- * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx)
- * allows third party website to turn your JSON resource URL into
- * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To
- * counter this your server can prefix all JSON requests with following string `")]}',\n"`.
- * Angular will automatically strip the prefix before processing it as JSON.
- *
- * For example if your server needs to return:
- * ```js
- * ['one','two']
- * ```
- *
- * which is vulnerable to attack, your server can return:
- * ```js
- * )]}',
- * ['one','two']
- * ```
- *
- * Angular will strip the prefix, before processing the JSON.
- *
- *
- * ### Cross Site Request Forgery (XSRF) Protection
- *
- * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by
- * which the attacker can trick an authenticated user into unknowingly executing actions on your
- * website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the
- * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
- * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
- * cookie, your server can be assured that the XHR came from JavaScript running on your domain.
- * The header will not be set for cross-domain requests.
- *
- * To take advantage of this, your server needs to set a token in a JavaScript readable session
- * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
- * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure
- * that only JavaScript running on your domain could have sent the request. The token must be
- * unique for each user and must be verifiable by the server (to prevent the JavaScript from
- * making up its own tokens). We recommend that the token is a digest of your site's
- * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography&#41;)
- * for added security.
- *
- * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName
- * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
- * or the per-request config object.
- *
- * In order to prevent collisions in environments where multiple Angular apps share the
- * same domain or subdomain, we recommend that each application uses unique cookie name.
- *
- * @param {object} config Object describing the request to be made and how it should be
- * processed. The object has following properties:
- *
- * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
- * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
- * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized
- * with the `paramSerializer` and appended as GET parameters.
- * - **data** – `{string|Object}` – Data to be sent as the request message data.
- * - **headers** – `{Object}` – Map of strings or functions which return strings representing
- * HTTP headers to send to the server. If the return value of a function is null, the
- * header will not be sent. Functions accept a config object as an argument.
- * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object.
- * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`.
- * The handler will be called in the context of a `$apply` block.
- * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload
- * object. To bind events to the XMLHttpRequest object, use `eventHandlers`.
- * The handler will be called in the context of a `$apply` block.
- * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token.
- * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token.
- * - **transformRequest** –
- * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
- * transform function or an array of such functions. The transform function takes the http
- * request body and headers and returns its transformed (typically serialized) version.
- * See {@link ng.$http#overriding-the-default-transformations-per-request
- * Overriding the Default Transformations}
- * - **transformResponse** –
- * `{function(data, headersGetter, status)|Array.<function(data, headersGetter, status)>}` –
- * transform function or an array of such functions. The transform function takes the http
- * response body, headers and status and returns its transformed (typically deserialized) version.
- * See {@link ng.$http#overriding-the-default-transformations-per-request
- * Overriding the Default Transformations}
- * - **paramSerializer** - `{string|function(Object<string,string>):string}` - A function used to
- * prepare the string representation of request parameters (specified as an object).
- * If specified as string, it is interpreted as function registered with the
- * {@link $injector $injector}, which means you can create your own serializer
- * by registering it as a {@link auto.$provide#service service}.
- * The default serializer is the {@link $httpParamSerializer $httpParamSerializer};
- * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike}
- * - **cache** – `{boolean|Object}` – A boolean value or object created with
- * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response.
- * See {@link $http#caching $http Caching} for more information.
- * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise}
- * that should abort the request when resolved.
- * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the
- * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials)
- * for more information.
- * - **responseType** - `{string}` - see
- * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype).
- *
- * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object
- * when the request succeeds or fails.
- *
- *
- * @property {Array.<Object>} pendingRequests Array of config objects for currently pending
- * requests. This is primarily meant to be used for debugging purposes.
- *
- *
- * @example
-<example module="httpExample">
-<file name="index.html">
- <div ng-controller="FetchController">
- <select ng-model="method" aria-label="Request method">
- <option>GET</option>
- <option>JSONP</option>
- </select>
- <input type="text" ng-model="url" size="80" aria-label="URL" />
- <button id="fetchbtn" ng-click="fetch()">fetch</button><br>
- <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
- <button id="samplejsonpbtn"
- ng-click="updateModel('JSONP',
- 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">
- Sample JSONP
- </button>
- <button id="invalidjsonpbtn"
- ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')">
- Invalid JSONP
- </button>
- <pre>http status code: {{status}}</pre>
- <pre>http response data: {{data}}</pre>
- </div>
-</file>
-<file name="script.js">
- angular.module('httpExample', [])
- .controller('FetchController', ['$scope', '$http', '$templateCache',
- function($scope, $http, $templateCache) {
- $scope.method = 'GET';
- $scope.url = 'http-hello.html';
-
- $scope.fetch = function() {
- $scope.code = null;
- $scope.response = null;
-
- $http({method: $scope.method, url: $scope.url, cache: $templateCache}).
- then(function(response) {
- $scope.status = response.status;
- $scope.data = response.data;
- }, function(response) {
- $scope.data = response.data || "Request failed";
- $scope.status = response.status;
- });
- };
-
- $scope.updateModel = function(method, url) {
- $scope.method = method;
- $scope.url = url;
- };
- }]);
-</file>
-<file name="http-hello.html">
- Hello, $http!
-</file>
-<file name="protractor.js" type="protractor">
- var status = element(by.binding('status'));
- var data = element(by.binding('data'));
- var fetchBtn = element(by.id('fetchbtn'));
- var sampleGetBtn = element(by.id('samplegetbtn'));
- var sampleJsonpBtn = element(by.id('samplejsonpbtn'));
- var invalidJsonpBtn = element(by.id('invalidjsonpbtn'));
-
- it('should make an xhr GET request', function() {
- sampleGetBtn.click();
- fetchBtn.click();
- expect(status.getText()).toMatch('200');
- expect(data.getText()).toMatch(/Hello, \$http!/);
- });
-
-// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185
-// it('should make a JSONP request to angularjs.org', function() {
-// sampleJsonpBtn.click();
-// fetchBtn.click();
-// expect(status.getText()).toMatch('200');
-// expect(data.getText()).toMatch(/Super Hero!/);
-// });
-
- it('should make JSONP request to invalid URL and invoke the error handler',
- function() {
- invalidJsonpBtn.click();
- fetchBtn.click();
- expect(status.getText()).toMatch('0');
- expect(data.getText()).toMatch('Request failed');
- });
-</file>
-</example>
- */
- function $http(requestConfig) {
-
- if (!isObject(requestConfig)) {
- throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
- }
-
- if (!isString(requestConfig.url)) {
- throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url);
- }
-
- var config = extend({
- method: 'get',
- transformRequest: defaults.transformRequest,
- transformResponse: defaults.transformResponse,
- paramSerializer: defaults.paramSerializer
- }, requestConfig);
-
- config.headers = mergeHeaders(requestConfig);
- config.method = uppercase(config.method);
- config.paramSerializer = isString(config.paramSerializer) ?
- $injector.get(config.paramSerializer) : config.paramSerializer;
-
- var requestInterceptors = [];
- var responseInterceptors = [];
- var promise = $q.when(config);
-
- // apply interceptors
- forEach(reversedInterceptors, function(interceptor) {
- if (interceptor.request || interceptor.requestError) {
- requestInterceptors.unshift(interceptor.request, interceptor.requestError);
- }
- if (interceptor.response || interceptor.responseError) {
- responseInterceptors.push(interceptor.response, interceptor.responseError);
- }
- });
-
- promise = chainInterceptors(promise, requestInterceptors);
- promise = promise.then(serverRequest);
- promise = chainInterceptors(promise, responseInterceptors);
-
- if (useLegacyPromise) {
- promise.success = function(fn) {
- assertArgFn(fn, 'fn');
-
- promise.then(function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
-
- promise.error = function(fn) {
- assertArgFn(fn, 'fn');
-
- promise.then(null, function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
- } else {
- promise.success = $httpMinErrLegacyFn('success');
- promise.error = $httpMinErrLegacyFn('error');
- }
-
- return promise;
-
-
- function chainInterceptors(promise, interceptors) {
- for (var i = 0, ii = interceptors.length; i < ii;) {
- var thenFn = interceptors[i++];
- var rejectFn = interceptors[i++];
-
- promise = promise.then(thenFn, rejectFn);
- }
-
- interceptors.length = 0;
-
- return promise;
- }
-
- function executeHeaderFns(headers, config) {
- var headerContent, processedHeaders = {};
-
- forEach(headers, function(headerFn, header) {
- if (isFunction(headerFn)) {
- headerContent = headerFn(config);
- if (headerContent != null) {
- processedHeaders[header] = headerContent;
- }
- } else {
- processedHeaders[header] = headerFn;
- }
- });
-
- return processedHeaders;
- }
-
- function mergeHeaders(config) {
- var defHeaders = defaults.headers,
- reqHeaders = extend({}, config.headers),
- defHeaderName, lowercaseDefHeaderName, reqHeaderName;
-
- defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]);
-
- // using for-in instead of forEach to avoid unnecessary iteration after header has been found
- defaultHeadersIteration:
- for (defHeaderName in defHeaders) {
- lowercaseDefHeaderName = lowercase(defHeaderName);
-
- for (reqHeaderName in reqHeaders) {
- if (lowercase(reqHeaderName) === lowercaseDefHeaderName) {
- continue defaultHeadersIteration;
- }
- }
-
- reqHeaders[defHeaderName] = defHeaders[defHeaderName];
- }
-
- // execute if header value is a function for merged headers
- return executeHeaderFns(reqHeaders, shallowCopy(config));
- }
-
- function serverRequest(config) {
- var headers = config.headers;
- var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest);
-
- // strip content-type if data is undefined
- if (isUndefined(reqData)) {
- forEach(headers, function(value, header) {
- if (lowercase(header) === 'content-type') {
- delete headers[header];
- }
- });
- }
-
- if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
- config.withCredentials = defaults.withCredentials;
- }
-
- // send request
- return sendReq(config, reqData).then(transformResponse, transformResponse);
- }
-
- function transformResponse(response) {
- // make a copy since the response must be cacheable
- var resp = extend({}, response);
- resp.data = transformData(response.data, response.headers, response.status,
- config.transformResponse);
- return (isSuccess(response.status))
- ? resp
- : $q.reject(resp);
- }
- }
-
- $http.pendingRequests = [];
-
- /**
- * @ngdoc method
- * @name $http#get
- *
- * @description
- * Shortcut method to perform `GET` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#delete
- *
- * @description
- * Shortcut method to perform `DELETE` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#head
- *
- * @description
- * Shortcut method to perform `HEAD` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#jsonp
- *
- * @description
- * Shortcut method to perform `JSONP` request.
- * If you would like to customise where and how the callbacks are stored then try overriding
- * or decorating the {@link $jsonpCallbacks} service.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request.
- * The name of the callback should be the string `JSON_CALLBACK`.
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
- createShortMethods('get', 'delete', 'head', 'jsonp');
-
- /**
- * @ngdoc method
- * @name $http#post
- *
- * @description
- * Shortcut method to perform `POST` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {*} data Request content
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#put
- *
- * @description
- * Shortcut method to perform `PUT` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {*} data Request content
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
-
- /**
- * @ngdoc method
- * @name $http#patch
- *
- * @description
- * Shortcut method to perform `PATCH` request.
- *
- * @param {string} url Relative or absolute URL specifying the destination of the request
- * @param {*} data Request content
- * @param {Object=} config Optional configuration object
- * @returns {HttpPromise} Future object
- */
- createShortMethodsWithData('post', 'put', 'patch');
-
- /**
- * @ngdoc property
- * @name $http#defaults
- *
- * @description
- * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of
- * default headers, withCredentials as well as request and response transformations.
- *
- * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above.
- */
- $http.defaults = defaults;
-
-
- return $http;
-
-
- function createShortMethods(names) {
- forEach(arguments, function(name) {
- $http[name] = function(url, config) {
- return $http(extend({}, config || {}, {
- method: name,
- url: url
- }));
- };
- });
- }
-
-
- function createShortMethodsWithData(name) {
- forEach(arguments, function(name) {
- $http[name] = function(url, data, config) {
- return $http(extend({}, config || {}, {
- method: name,
- url: url,
- data: data
- }));
- };
- });
- }
-
-
- /**
- * Makes the request.
- *
- * !!! ACCESSES CLOSURE VARS:
- * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests
- */
- function sendReq(config, reqData) {
- var deferred = $q.defer(),
- promise = deferred.promise,
- cache,
- cachedResp,
- reqHeaders = config.headers,
- url = buildUrl(config.url, config.paramSerializer(config.params));
-
- $http.pendingRequests.push(config);
- promise.then(removePendingReq, removePendingReq);
-
-
- if ((config.cache || defaults.cache) && config.cache !== false &&
- (config.method === 'GET' || config.method === 'JSONP')) {
- cache = isObject(config.cache) ? config.cache
- : isObject(defaults.cache) ? defaults.cache
- : defaultCache;
- }
-
- if (cache) {
- cachedResp = cache.get(url);
- if (isDefined(cachedResp)) {
- if (isPromiseLike(cachedResp)) {
- // cached request has already been sent, but there is no response yet
- cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult);
- } else {
- // serving from cache
- if (isArray(cachedResp)) {
- resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
- } else {
- resolvePromise(cachedResp, 200, {}, 'OK');
- }
- }
- } else {
- // put the promise for the non-transformed response into cache as a placeholder
- cache.put(url, promise);
- }
- }
-
-
- // if we won't have the response in cache, set the xsrf headers and
- // send the request to the backend
- if (isUndefined(cachedResp)) {
- var xsrfValue = urlIsSameOrigin(config.url)
- ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
- : undefined;
- if (xsrfValue) {
- reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
- }
-
- $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
- config.withCredentials, config.responseType,
- createApplyHandlers(config.eventHandlers),
- createApplyHandlers(config.uploadEventHandlers));
- }
-
- return promise;
-
- function createApplyHandlers(eventHandlers) {
- if (eventHandlers) {
- var applyHandlers = {};
- forEach(eventHandlers, function(eventHandler, key) {
- applyHandlers[key] = function(event) {
- if (useApplyAsync) {
- $rootScope.$applyAsync(callEventHandler);
- } else if ($rootScope.$$phase) {
- callEventHandler();
- } else {
- $rootScope.$apply(callEventHandler);
- }
-
- function callEventHandler() {
- eventHandler(event);
- }
- };
- });
- return applyHandlers;
- }
- }
-
-
- /**
- * Callback registered to $httpBackend():
- * - caches the response if desired
- * - resolves the raw $http promise
- * - calls $apply
- */
- function done(status, response, headersString, statusText) {
- if (cache) {
- if (isSuccess(status)) {
- cache.put(url, [status, response, parseHeaders(headersString), statusText]);
- } else {
- // remove promise from the cache
- cache.remove(url);
- }
- }
-
- function resolveHttpPromise() {
- resolvePromise(response, status, headersString, statusText);
- }
-
- if (useApplyAsync) {
- $rootScope.$applyAsync(resolveHttpPromise);
- } else {
- resolveHttpPromise();
- if (!$rootScope.$$phase) $rootScope.$apply();
- }
- }
-
-
- /**
- * Resolves the raw $http promise.
- */
- function resolvePromise(response, status, headers, statusText) {
- //status: HTTP response status code, 0, -1 (aborted by timeout / promise)
- status = status >= -1 ? status : 0;
-
- (isSuccess(status) ? deferred.resolve : deferred.reject)({
- data: response,
- status: status,
- headers: headersGetter(headers),
- config: config,
- statusText: statusText
- });
- }
-
- function resolvePromiseWithResult(result) {
- resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText);
- }
-
- function removePendingReq() {
- var idx = $http.pendingRequests.indexOf(config);
- if (idx !== -1) $http.pendingRequests.splice(idx, 1);
- }
- }
-
-
- function buildUrl(url, serializedParams) {
- if (serializedParams.length > 0) {
- url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams;
- }
- return url;
- }
- }];
-}
-
-/**
- * @ngdoc service
- * @name $xhrFactory
- *
- * @description
- * Factory function used to create XMLHttpRequest objects.
- *
- * Replace or decorate this service to create your own custom XMLHttpRequest objects.
- *
- * ```
- * angular.module('myApp', [])
- * .factory('$xhrFactory', function() {
- * return function createXhr(method, url) {
- * return new window.XMLHttpRequest({mozSystem: true});
- * };
- * });
- * ```
- *
- * @param {string} method HTTP method of the request (GET, POST, PUT, ..)
- * @param {string} url URL of the request.
- */
-function $xhrFactoryProvider() {
- this.$get = function() {
- return function createXhr() {
- return new window.XMLHttpRequest();
- };
- };
-}
-
-/**
- * @ngdoc service
- * @name $httpBackend
- * @requires $jsonpCallbacks
- * @requires $document
- * @requires $xhrFactory
- *
- * @description
- * HTTP backend used by the {@link ng.$http service} that delegates to
- * XMLHttpRequest object or JSONP and deals with browser incompatibilities.
- *
- * You should never need to use this service directly, instead use the higher-level abstractions:
- * {@link ng.$http $http} or {@link ngResource.$resource $resource}.
- *
- * During testing this implementation is swapped with {@link ngMock.$httpBackend mock
- * $httpBackend} which can be trained with responses.
- */
-function $HttpBackendProvider() {
- this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) {
- return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]);
- }];
-}
-
-function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
- // TODO(vojta): fix the signature
- return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) {
- $browser.$$incOutstandingRequestCount();
- url = url || $browser.url();
-
- if (lowercase(method) === 'jsonp') {
- var callbackPath = callbacks.createCallback(url);
- var jsonpDone = jsonpReq(url, callbackPath, function(status, text) {
- // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING)
- var response = (status === 200) && callbacks.getResponse(callbackPath);
- completeRequest(callback, status, response, "", text);
- callbacks.removeCallback(callbackPath);
- });
- } else {
-
- var xhr = createXhr(method, url);
-
- xhr.open(method, url, true);
- forEach(headers, function(value, key) {
- if (isDefined(value)) {
- xhr.setRequestHeader(key, value);
- }
- });
-
- xhr.onload = function requestLoaded() {
- var statusText = xhr.statusText || '';
-
- // responseText is the old-school way of retrieving response (supported by IE9)
- // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
- var 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;
-
- // 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;
-
- forEach(eventHandlers, function(value, key) {
- xhr.addEventListener(key, value);
- });
-
- forEach(uploadEventHandlers, function(value, key) {
- xhr.upload.addEventListener(key, value);
- });
-
- if (withCredentials) {
- xhr.withCredentials = true;
- }
-
- if (responseType) {
- try {
- xhr.responseType = responseType;
- } catch (e) {
- // WebKit added support for the json responseType value on 09/03/2013
- // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are
- // known to throw when setting the value "json" as the response type. Other older
- // browsers implementing the responseType
- //
- // The json response type can be ignored if not supported, because JSON payloads are
- // parsed on the client-side regardless.
- if (responseType !== 'json') {
- throw e;
- }
- }
- }
-
- xhr.send(isUndefined(post) ? null : post);
- }
-
- if (timeout > 0) {
- var timeoutId = $browserDefer(timeoutRequest, timeout);
- } else if (isPromiseLike(timeout)) {
- timeout.then(timeoutRequest);
- }
-
-
- function timeoutRequest() {
- jsonpDone && jsonpDone();
- xhr && xhr.abort();
- }
-
- function completeRequest(callback, status, response, headersString, statusText) {
- // cancel timeout and subsequent timeout promise resolution
- if (isDefined(timeoutId)) {
- $browserDefer.cancel(timeoutId);
- }
- jsonpDone = xhr = null;
-
- callback(status, response, headersString, statusText);
- $browser.$$completeOutstandingRequest(noop);
- }
- };
-
- function jsonpReq(url, callbackPath, done) {
- url = url.replace('JSON_CALLBACK', callbackPath);
- // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.:
- // - fetches local scripts via XHR and evals them
- // - adds and immediately removes script elements from the document
- var script = rawDocument.createElement('script'), callback = null;
- script.type = "text/javascript";
- script.src = url;
- script.async = true;
-
- callback = function(event) {
- removeEventListenerFn(script, "load", callback);
- removeEventListenerFn(script, "error", callback);
- rawDocument.body.removeChild(script);
- script = null;
- var status = -1;
- var text = "unknown";
-
- if (event) {
- if (event.type === "load" && !callbacks.wasCalled(callbackPath)) {
- event = { type: "error" };
- }
- text = event.type;
- status = event.type === "error" ? 404 : 200;
- }
-
- if (done) {
- done(status, text);
- }
- };
-
- addEventListenerFn(script, "load", callback);
- addEventListenerFn(script, "error", callback);
- rawDocument.body.appendChild(script);
- return callback;
- }
-}
-
-var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate');
-$interpolateMinErr.throwNoconcat = function(text) {
- throw $interpolateMinErr('noconcat',
- "Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
- "interpolations that concatenate multiple expressions when a trusted value is " +
- "required. See http://docs.angularjs.org/api/ng.$sce", text);
-};
-
-$interpolateMinErr.interr = function(text, err) {
- return $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString());
-};
-
-/**
- * @ngdoc provider
- * @name $interpolateProvider
- *
- * @description
- *
- * Used for configuring the interpolation markup. Defaults to `{{` and `}}`.
- *
- * <div class="alert alert-danger">
- * This feature is sometimes used to mix different markup languages, e.g. to wrap an Angular
- * template within a Python Jinja template (or any other template language). Mixing templating
- * languages is **very dangerous**. The embedding template language will not safely escape Angular
- * expressions, so any user-controlled values in the template will cause Cross Site Scripting (XSS)
- * security bugs!
- * </div>
- *
- * @example
-<example name="custom-interpolation-markup" module="customInterpolationApp">
-<file name="index.html">
-<script>
- var customInterpolationApp = angular.module('customInterpolationApp', []);
-
- customInterpolationApp.config(function($interpolateProvider) {
- $interpolateProvider.startSymbol('//');
- $interpolateProvider.endSymbol('//');
- });
-
-
- customInterpolationApp.controller('DemoController', function() {
- this.label = "This binding is brought you by // interpolation symbols.";
- });
-</script>
-<div ng-controller="DemoController as demo">
- //demo.label//
-</div>
-</file>
-<file name="protractor.js" type="protractor">
- it('should interpolate binding with custom symbols', function() {
- expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.');
- });
-</file>
-</example>
- */
-function $InterpolateProvider() {
- var startSymbol = '{{';
- var endSymbol = '}}';
-
- /**
- * @ngdoc method
- * @name $interpolateProvider#startSymbol
- * @description
- * Symbol to denote start of expression in the interpolated string. Defaults to `{{`.
- *
- * @param {string=} value new value to set the starting symbol to.
- * @returns {string|self} Returns the symbol when used as getter and self if used as setter.
- */
- this.startSymbol = function(value) {
- if (value) {
- startSymbol = value;
- return this;
- } else {
- return startSymbol;
- }
- };
-
- /**
- * @ngdoc method
- * @name $interpolateProvider#endSymbol
- * @description
- * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`.
- *
- * @param {string=} value new value to set the ending symbol to.
- * @returns {string|self} Returns the symbol when used as getter and self if used as setter.
- */
- this.endSymbol = function(value) {
- if (value) {
- endSymbol = value;
- return this;
- } else {
- return endSymbol;
- }
- };
-
-
- this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) {
- var startSymbolLength = startSymbol.length,
- endSymbolLength = endSymbol.length,
- escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'),
- escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g');
-
- function escape(ch) {
- return '\\\\\\' + ch;
- }
-
- function unescapeText(text) {
- return text.replace(escapedStartRegexp, startSymbol).
- replace(escapedEndRegexp, endSymbol);
- }
-
- function stringify(value) {
- if (value == null) { // null || undefined
- return '';
- }
- switch (typeof value) {
- case 'string':
- break;
- case 'number':
- value = '' + value;
- break;
- default:
- value = toJson(value);
- }
-
- return value;
- }
-
- //TODO: this is the same as the constantWatchDelegate in parse.js
- function constantWatchDelegate(scope, listener, objectEquality, constantInterp) {
- var unwatch;
- return unwatch = scope.$watch(function constantInterpolateWatch(scope) {
- unwatch();
- return constantInterp(scope);
- }, listener, objectEquality);
- }
-
- /**
- * @ngdoc service
- * @name $interpolate
- * @kind function
- *
- * @requires $parse
- * @requires $sce
- *
- * @description
- *
- * Compiles a string with markup into an interpolation function. This service is used by the
- * HTML {@link ng.$compile $compile} service for data binding. See
- * {@link ng.$interpolateProvider $interpolateProvider} for configuring the
- * interpolation markup.
- *
- *
- * ```js
- * var $interpolate = ...; // injected
- * var exp = $interpolate('Hello {{name | uppercase}}!');
- * expect(exp({name:'Angular'})).toEqual('Hello ANGULAR!');
- * ```
- *
- * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is
- * `true`, the interpolation function will return `undefined` unless all embedded expressions
- * evaluate to a value other than `undefined`.
- *
- * ```js
- * var $interpolate = ...; // injected
- * var context = {greeting: 'Hello', name: undefined };
- *
- * // default "forgiving" mode
- * var exp = $interpolate('{{greeting}} {{name}}!');
- * expect(exp(context)).toEqual('Hello !');
- *
- * // "allOrNothing" mode
- * exp = $interpolate('{{greeting}} {{name}}!', false, null, true);
- * expect(exp(context)).toBeUndefined();
- * context.name = 'Angular';
- * expect(exp(context)).toEqual('Hello Angular!');
- * ```
- *
- * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior.
- *
- * #### Escaped Interpolation
- * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers
- * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash).
- * It will be rendered as a regular start/end marker, and will not be interpreted as an expression
- * or binding.
- *
- * This enables web-servers to prevent script injection attacks and defacing attacks, to some
- * degree, while also enabling code examples to work without relying on the
- * {@link ng.directive:ngNonBindable ngNonBindable} directive.
- *
- * **For security purposes, it is strongly encouraged that web servers escape user-supplied data,
- * replacing angle brackets (&lt;, &gt;) with &amp;lt; and &amp;gt; respectively, and replacing all
- * interpolation start/end markers with their escaped counterparts.**
- *
- * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered
- * output when the $interpolate service processes the text. So, for HTML elements interpolated
- * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter
- * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such,
- * this is typically useful only when user-data is used in rendering a template from the server, or
- * when otherwise untrusted data is used by a directive.
- *
- * <example>
- * <file name="index.html">
- * <div ng-init="username='A user'">
- * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\}
- * </p>
- * <p><strong>{{username}}</strong> attempts to inject code which will deface the
- * application, but fails to accomplish their task, because the server has correctly
- * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash)
- * characters.</p>
- * <p>Instead, the result of the attempted script injection is visible, and can be removed
- * from the database by an administrator.</p>
- * </div>
- * </file>
- * </example>
- *
- * @knownIssue
- * It is currently not possible for an interpolated expression to contain the interpolation end
- * symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e.
- * an interpolated expression consisting of a single-quote (`'`) and the `' }}` string.
- *
- * @knownIssue
- * All directives and components must use the standard `{{` `}}` interpolation symbols
- * in their templates. If you change the application interpolation symbols the {@link $compile}
- * service will attempt to denormalize the standard symbols to the custom symbols.
- * The denormalization process is not clever enough to know not to replace instances of the standard
- * symbols where they would not normally be treated as interpolation symbols. For example in the following
- * code snippet the closing braces of the literal object will get incorrectly denormalized:
- *
- * ```
- * <div data-context='{"context":{"id":3,"type":"page"}}">
- * ```
- *
- * The workaround is to ensure that such instances are separated by whitespace:
- * ```
- * <div data-context='{"context":{"id":3,"type":"page"} }">
- * ```
- *
- * See https://github.com/angular/angular.js/pull/14610#issuecomment-219401099 for more information.
- *
- * @param {string} text The text with markup to interpolate.
- * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have
- * embedded expression in order to return an interpolation function. Strings with no
- * embedded expression will return null for the interpolation function.
- * @param {string=} trustedContext when provided, the returned function passes the interpolated
- * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult,
- * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that
- * provides Strict Contextual Escaping for details.
- * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined
- * unless all embedded expressions evaluate to a value other than `undefined`.
- * @returns {function(context)} an interpolation function which is used to compute the
- * interpolated string. The function has these parameters:
- *
- * - `context`: evaluation context for all expressions embedded in the interpolated text
- */
- function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
- // Provide a quick exit and simplified result function for text with no interpolation
- if (!text.length || text.indexOf(startSymbol) === -1) {
- var constantInterp;
- if (!mustHaveExpression) {
- var unescapedText = unescapeText(text);
- constantInterp = valueFn(unescapedText);
- constantInterp.exp = text;
- constantInterp.expressions = [];
- constantInterp.$$watchDelegate = constantWatchDelegate;
- }
- return constantInterp;
- }
-
- allOrNothing = !!allOrNothing;
- var startIndex,
- endIndex,
- index = 0,
- expressions = [],
- parseFns = [],
- textLength = text.length,
- exp,
- concat = [],
- expressionPositions = [];
-
- while (index < textLength) {
- if (((startIndex = text.indexOf(startSymbol, index)) != -1) &&
- ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) {
- if (index !== startIndex) {
- concat.push(unescapeText(text.substring(index, startIndex)));
- }
- exp = text.substring(startIndex + startSymbolLength, endIndex);
- expressions.push(exp);
- parseFns.push($parse(exp, parseStringifyInterceptor));
- index = endIndex + endSymbolLength;
- expressionPositions.push(concat.length);
- concat.push('');
- } else {
- // we did not find an interpolation, so we have to add the remainder to the separators array
- if (index !== textLength) {
- concat.push(unescapeText(text.substring(index)));
- }
- break;
- }
- }
-
- // Concatenating expressions makes it hard to reason about whether some combination of
- // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a
- // single expression be used for iframe[src], object[src], etc., we ensure that the value
- // that's used is assigned or constructed by some JS code somewhere that is more testable or
- // make it obvious that you bound the value to some user controlled value. This helps reduce
- // the load when auditing for XSS issues.
- if (trustedContext && concat.length > 1) {
- $interpolateMinErr.throwNoconcat(text);
- }
-
- if (!mustHaveExpression || expressions.length) {
- var compute = function(values) {
- for (var i = 0, ii = expressions.length; i < ii; i++) {
- if (allOrNothing && isUndefined(values[i])) return;
- concat[expressionPositions[i]] = values[i];
- }
- return concat.join('');
- };
-
- var getValue = function(value) {
- return trustedContext ?
- $sce.getTrusted(trustedContext, value) :
- $sce.valueOf(value);
- };
-
- return extend(function interpolationFn(context) {
- var i = 0;
- var ii = expressions.length;
- var values = new Array(ii);
-
- try {
- for (; i < ii; i++) {
- values[i] = parseFns[i](context);
- }
-
- return compute(values);
- } catch (err) {
- $exceptionHandler($interpolateMinErr.interr(text, err));
- }
-
- }, {
- // all of these properties are undocumented for now
- exp: text, //just for compatibility with regular watchers created via $watch
- expressions: expressions,
- $$watchDelegate: function(scope, listener) {
- var lastValue;
- return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
- var currValue = compute(values);
- if (isFunction(listener)) {
- listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
- }
- lastValue = currValue;
- });
- }
- });
- }
-
- function parseStringifyInterceptor(value) {
- try {
- value = getValue(value);
- return allOrNothing && !isDefined(value) ? value : stringify(value);
- } catch (err) {
- $exceptionHandler($interpolateMinErr.interr(text, err));
- }
- }
- }
-
-
- /**
- * @ngdoc method
- * @name $interpolate#startSymbol
- * @description
- * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`.
- *
- * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change
- * the symbol.
- *
- * @returns {string} start symbol.
- */
- $interpolate.startSymbol = function() {
- return startSymbol;
- };
-
-
- /**
- * @ngdoc method
- * @name $interpolate#endSymbol
- * @description
- * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`.
- *
- * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change
- * the symbol.
- *
- * @returns {string} end symbol.
- */
- $interpolate.endSymbol = function() {
- return endSymbol;
- };
-
- return $interpolate;
- }];
-}
-
-function $IntervalProvider() {
- this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
- function($rootScope, $window, $q, $$q, $browser) {
- var intervals = {};
-
-
- /**
- * @ngdoc service
- * @name $interval
- *
- * @description
- * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay`
- * milliseconds.
- *
- * The return value of registering an interval function is a promise. This promise will be
- * notified upon each tick of the interval, and will be resolved after `count` iterations, or
- * run indefinitely if `count` is not defined. The value of the notification will be the
- * number of iterations that have run.
- * To cancel an interval, call `$interval.cancel(promise)`.
- *
- * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
- * move forward by `millis` milliseconds and trigger any functions scheduled to run in that
- * time.
- *
- * <div class="alert alert-warning">
- * **Note**: Intervals created by this service must be explicitly destroyed when you are finished
- * with them. In particular they are not automatically destroyed when a controller's scope or a
- * directive's element are destroyed.
- * You should take this into consideration and make sure to always cancel the interval at the
- * appropriate moment. See the example below for more details on how and when to do this.
- * </div>
- *
- * @param {function()} fn A function that should be called repeatedly.
- * @param {number} delay Number of milliseconds between each function call.
- * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
- * indefinitely.
- * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
- * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @param {...*=} Pass additional parameters to the executed function.
- * @returns {promise} A promise which will be notified on each iteration.
- *
- * @example
- * <example module="intervalExample">
- * <file name="index.html">
- * <script>
- * angular.module('intervalExample', [])
- * .controller('ExampleController', ['$scope', '$interval',
- * function($scope, $interval) {
- * $scope.format = 'M/d/yy h:mm:ss a';
- * $scope.blood_1 = 100;
- * $scope.blood_2 = 120;
- *
- * var stop;
- * $scope.fight = function() {
- * // Don't start a new fight if we are already fighting
- * if ( angular.isDefined(stop) ) return;
- *
- * stop = $interval(function() {
- * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) {
- * $scope.blood_1 = $scope.blood_1 - 3;
- * $scope.blood_2 = $scope.blood_2 - 4;
- * } else {
- * $scope.stopFight();
- * }
- * }, 100);
- * };
- *
- * $scope.stopFight = function() {
- * if (angular.isDefined(stop)) {
- * $interval.cancel(stop);
- * stop = undefined;
- * }
- * };
- *
- * $scope.resetFight = function() {
- * $scope.blood_1 = 100;
- * $scope.blood_2 = 120;
- * };
- *
- * $scope.$on('$destroy', function() {
- * // Make sure that the interval is destroyed too
- * $scope.stopFight();
- * });
- * }])
- * // Register the 'myCurrentTime' directive factory method.
- * // We inject $interval and dateFilter service since the factory method is DI.
- * .directive('myCurrentTime', ['$interval', 'dateFilter',
- * function($interval, dateFilter) {
- * // return the directive link function. (compile function not needed)
- * return function(scope, element, attrs) {
- * var format, // date format
- * stopTime; // so that we can cancel the time updates
- *
- * // used to update the UI
- * function updateTime() {
- * element.text(dateFilter(new Date(), format));
- * }
- *
- * // watch the expression, and update the UI on change.
- * scope.$watch(attrs.myCurrentTime, function(value) {
- * format = value;
- * updateTime();
- * });
- *
- * stopTime = $interval(updateTime, 1000);
- *
- * // listen on DOM destroy (removal) event, and cancel the next UI update
- * // to prevent updating time after the DOM element was removed.
- * element.on('$destroy', function() {
- * $interval.cancel(stopTime);
- * });
- * }
- * }]);
- * </script>
- *
- * <div>
- * <div ng-controller="ExampleController">
- * <label>Date format: <input ng-model="format"></label> <hr/>
- * Current time is: <span my-current-time="format"></span>
- * <hr/>
- * Blood 1 : <font color='red'>{{blood_1}}</font>
- * Blood 2 : <font color='red'>{{blood_2}}</font>
- * <button type="button" data-ng-click="fight()">Fight</button>
- * <button type="button" data-ng-click="stopFight()">StopFight</button>
- * <button type="button" data-ng-click="resetFight()">resetFight</button>
- * </div>
- * </div>
- *
- * </file>
- * </example>
- */
- function interval(fn, delay, count, invokeApply) {
- var hasParams = arguments.length > 4,
- args = hasParams ? sliceArgs(arguments, 4) : [],
- setInterval = $window.setInterval,
- clearInterval = $window.clearInterval,
- iteration = 0,
- skipApply = (isDefined(invokeApply) && !invokeApply),
- deferred = (skipApply ? $$q : $q).defer(),
- promise = deferred.promise;
-
- count = isDefined(count) ? count : 0;
-
- promise.$$intervalId = setInterval(function tick() {
- if (skipApply) {
- $browser.defer(callback);
- } else {
- $rootScope.$evalAsync(callback);
- }
- deferred.notify(iteration++);
-
- if (count > 0 && iteration >= count) {
- deferred.resolve(iteration);
- clearInterval(promise.$$intervalId);
- delete intervals[promise.$$intervalId];
- }
-
- if (!skipApply) $rootScope.$apply();
-
- }, delay);
-
- intervals[promise.$$intervalId] = deferred;
-
- return promise;
-
- function callback() {
- if (!hasParams) {
- fn(iteration);
- } else {
- fn.apply(null, args);
- }
- }
- }
-
-
- /**
- * @ngdoc method
- * @name $interval#cancel
- *
- * @description
- * Cancels a task associated with the `promise`.
- *
- * @param {Promise=} promise returned by the `$interval` function.
- * @returns {boolean} Returns `true` if the task was successfully canceled.
- */
- interval.cancel = function(promise) {
- if (promise && promise.$$intervalId in intervals) {
- intervals[promise.$$intervalId].reject('canceled');
- $window.clearInterval(promise.$$intervalId);
- delete intervals[promise.$$intervalId];
- return true;
- }
- return false;
- };
-
- return interval;
- }];
-}
-
-/**
- * @ngdoc service
- * @name $jsonpCallbacks
- * @requires $window
- * @description
- * This service handles the lifecycle of callbacks to handle JSONP requests.
- * Override this service if you wish to customise where the callbacks are stored and
- * how they vary compared to the requested url.
- */
-var $jsonpCallbacksProvider = function() {
- this.$get = ['$window', function($window) {
- var callbacks = $window.angular.callbacks;
- var callbackMap = {};
-
- function createCallback(callbackId) {
- var callback = function(data) {
- callback.data = data;
- callback.called = true;
- };
- callback.id = callbackId;
- return callback;
- }
-
- return {
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#createCallback
- * @param {string} url the url of the JSONP request
- * @returns {string} the callback path to send to the server as part of the JSONP request
- * @description
- * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback
- * to pass to the server, which will be used to call the callback with its payload in the JSONP response.
- */
- createCallback: function(url) {
- var callbackId = '_' + (callbacks.$$counter++).toString(36);
- var callbackPath = 'angular.callbacks.' + callbackId;
- var callback = createCallback(callbackId);
- callbackMap[callbackPath] = callbacks[callbackId] = callback;
- return callbackPath;
- },
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#wasCalled
- * @param {string} callbackPath the path to the callback that was sent in the JSONP request
- * @returns {boolean} whether the callback has been called, as a result of the JSONP response
- * @description
- * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the
- * callback that was passed in the request.
- */
- wasCalled: function(callbackPath) {
- return callbackMap[callbackPath].called;
- },
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#getResponse
- * @param {string} callbackPath the path to the callback that was sent in the JSONP request
- * @returns {*} the data received from the response via the registered callback
- * @description
- * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback
- * in the JSONP response.
- */
- getResponse: function(callbackPath) {
- return callbackMap[callbackPath].data;
- },
- /**
- * @ngdoc method
- * @name $jsonpCallbacks#removeCallback
- * @param {string} callbackPath the path to the callback that was sent in the JSONP request
- * @description
- * {@link $httpBackend} calls this method to remove the callback after the JSONP request has
- * completed or timed-out.
- */
- removeCallback: function(callbackPath) {
- var callback = callbackMap[callbackPath];
- delete callbacks[callback.id];
- delete callbackMap[callbackPath];
- }
- };
- }];
-};
-
-/**
- * @ngdoc service
- * @name $locale
- *
- * @description
- * $locale service provides localization rules for various Angular components. As of right now the
- * only public api is:
- *
- * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
- */
-
-var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/,
+var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
var $locationMinErr = minErr('$location');
@@ -12784,12 +6582,36 @@ function encodePath(path) {
i = segments.length;
while (i--) {
- segments[i] = encodeUriSegment(segments[i]);
+ // decode forward slashes to prevent them from being double encoded
+ segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/'));
+ }
+
+ return segments.join('/');
+}
+
+function decodePath(path, html5Mode) {
+ var segments = path.split('/'),
+ i = segments.length;
+
+ while (i--) {
+ segments[i] = decodeURIComponent(segments[i]);
+ if (html5Mode) {
+ // encode forward slashes to prevent them from being mistaken for path separators
+ segments[i] = segments[i].replace(/\//g, '%2F');
+ }
}
return segments.join('/');
}
+function normalizePath(pathValue, searchValue, hashValue) {
+ var search = toKeyValue(searchValue),
+ hash = hashValue ? '#' + encodeUriSegment(hashValue) : '',
+ path = encodePath(pathValue);
+
+ return path + (search ? '?' + search : '') + hash;
+}
+
function parseAbsoluteUrl(absoluteUrl, locationObj) {
var parsedUrl = urlResolve(absoluteUrl);
@@ -12798,26 +6620,31 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) {
locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
}
+var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/;
+function parseAppUrl(url, locationObj, html5Mode) {
+
+ if (DOUBLE_SLASH_REGEX.test(url)) {
+ throw $locationMinErr('badpath', 'Invalid url "{0}".', url);
+ }
-function parseAppUrl(relativeUrl, locationObj) {
- var prefixed = (relativeUrl.charAt(0) !== '/');
+ var prefixed = (url.charAt(0) !== '/');
if (prefixed) {
- relativeUrl = '/' + relativeUrl;
+ url = '/' + url;
}
- var match = urlResolve(relativeUrl);
- locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ?
- match.pathname.substring(1) : match.pathname);
+ var match = urlResolve(url);
+ var path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname;
+ locationObj.$$path = decodePath(path, html5Mode);
locationObj.$$search = parseKeyValue(match.search);
locationObj.$$hash = decodeURIComponent(match.hash);
// make sure path starts with '/';
- if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') {
+ if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') {
locationObj.$$path = '/' + locationObj.$$path;
}
}
-function startsWith(haystack, needle) {
- return haystack.lastIndexOf(needle, 0) === 0;
+function startsWith(str, search) {
+ return str.slice(0, search.length) === search;
}
/**
@@ -12833,17 +6660,11 @@ function stripBaseUrl(base, url) {
}
}
-
function stripHash(url) {
var index = url.indexOf('#');
- return index == -1 ? url : url.substr(0, index);
-}
-
-function trimEmptyHash(url) {
- return url.replace(/(#.+)|#$/, '$1');
+ return index === -1 ? url : url.substr(0, index);
}
-
function stripFile(url) {
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
}
@@ -12855,13 +6676,13 @@ function serverBase(url) {
/**
- * LocationHtml5Url represents an url
+ * LocationHtml5Url represents a URL
* This object is exposed as $location service when HTML5 mode is enabled and supported
*
* @constructor
* @param {string} appBase application base URL
* @param {string} appBaseNoFile application base URL stripped of any filename
- * @param {string} basePrefix url path prefix
+ * @param {string} basePrefix URL path prefix
*/
function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$html5 = true;
@@ -12870,8 +6691,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
- * Parse given html5 (regular) url string into properties
- * @param {string} url HTML5 url
+ * Parse given HTML5 (regular) URL string into properties
+ * @param {string} url HTML5 URL
* @private
*/
this.$$parse = function(url) {
@@ -12881,7 +6702,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
appBaseNoFile);
}
- parseAppUrl(pathUrl, this);
+ parseAppUrl(pathUrl, this, true);
if (!this.$$path) {
this.$$path = '/';
@@ -12890,16 +6711,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$compose();
};
- /**
- * Compose url and update `absUrl` property
- * @private
- */
- this.$$compose = function() {
- var search = toKeyValue(this.$$search),
- hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
-
- this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
- this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
+ this.$$normalizeUrl = function(url) {
+ return appBaseNoFile + url.substr(1); // first char is always '/'
};
this.$$parseLinkUrl = function(url, relHref) {
@@ -12912,16 +6725,17 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
var appUrl, prevAppUrl;
var rewrittenUrl;
+
if (isDefined(appUrl = stripBaseUrl(appBase, url))) {
prevAppUrl = appUrl;
- if (isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) {
+ if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) {
rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl);
} else {
rewrittenUrl = appBase + prevAppUrl;
}
} else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBaseNoFile + appUrl;
- } else if (appBaseNoFile == url + '/') {
+ } else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
}
if (rewrittenUrl) {
@@ -12933,7 +6747,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
- * LocationHashbangUrl represents url
+ * LocationHashbangUrl represents URL
* This object is exposed as $location service when developer doesn't opt into html5 mode.
* It also serves as the base class for html5 mode fallback on legacy browsers.
*
@@ -12948,8 +6762,8 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
- * Parse given hashbang url into properties
- * @param {string} url Hashbang url
+ * Parse given hashbang URL into properties
+ * @param {string} url Hashbang URL
* @private
*/
this.$$parse = function(url) {
@@ -12958,7 +6772,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') {
- // The rest of the url starts with a hash so we have
+ // The rest of the URL starts with a hash so we have
// got either a hashbang path or a plain hash fragment
withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl);
if (isUndefined(withoutHashUrl)) {
@@ -12976,12 +6790,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
withoutHashUrl = '';
if (isUndefined(withoutBaseUrl)) {
appBase = url;
- this.replace();
+ /** @type {?} */ (this).replace();
}
}
}
- parseAppUrl(withoutHashUrl, this);
+ parseAppUrl(withoutHashUrl, this, false);
this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase);
@@ -12995,7 +6809,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
* * a.setAttribute('href', '/foo')
* * a.pathname === '/C:/foo' //true
*
- * Inside of Angular, we're always using pathnames that
+ * Inside of AngularJS, we're always using pathnames that
* do not include drive names for routing.
*/
function removeWindowsDriveName(path, url, base) {
@@ -13022,20 +6836,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
}
};
- /**
- * Compose hashbang url and update `absUrl` property
- * @private
- */
- this.$$compose = function() {
- var search = toKeyValue(this.$$search),
- hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
-
- this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
- this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
+ this.$$normalizeUrl = function(url) {
+ return appBase + (url ? hashPrefix + url : '');
};
this.$$parseLinkUrl = function(url, relHref) {
- if (stripHash(appBase) == stripHash(url)) {
+ if (stripHash(appBase) === stripHash(url)) {
this.$$parse(url);
return true;
}
@@ -13045,7 +6851,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
- * LocationHashbangUrl represents url
+ * LocationHashbangUrl represents URL
* This object is exposed as $location service when html5 history api is enabled but the browser
* does not support it.
*
@@ -13069,7 +6875,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
var rewrittenUrl;
var appUrl;
- if (appBase == stripHash(url)) {
+ if (appBase === stripHash(url)) {
rewrittenUrl = url;
} else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBase + hashPrefix + appUrl;
@@ -13082,22 +6888,17 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
return !!rewrittenUrl;
};
- this.$$compose = function() {
- var search = toKeyValue(this.$$search),
- hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
-
- this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
+ this.$$normalizeUrl = function(url) {
// include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#'
- this.$$absUrl = appBase + hashPrefix + this.$$url;
+ return appBase + hashPrefix + url;
};
-
}
var locationPrototype = {
/**
- * Ensure absolute url is initialized.
+ * Ensure absolute URL is initialized.
* @private
*/
$$absUrl:'',
@@ -13115,23 +6916,33 @@ var locationPrototype = {
$$replace: false,
/**
+ * Compose url and update `url` and `absUrl` property
+ * @private
+ */
+ $$compose: function() {
+ this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash);
+ this.$$absUrl = this.$$normalizeUrl(this.$$url);
+ this.$$urlUpdatedByLocation = true;
+ },
+
+ /**
* @ngdoc method
* @name $location#absUrl
*
* @description
* This method is getter only.
*
- * Return full url representation with all segments encoded according to rules specified in
+ * Return full URL representation with all segments encoded according to rules specified in
* [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var absUrl = $location.absUrl();
* // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
* ```
*
- * @return {string} full url
+ * @return {string} full URL
*/
absUrl: locationGetter('$$absUrl'),
@@ -13142,18 +6953,18 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return url (e.g. `/path?a=b#hash`) when called without any parameter.
+ * Return URL (e.g. `/path?a=b#hash`) when called without any parameter.
*
* Change path, search and hash, when called with parameter and return `$location`.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var url = $location.url();
* // => "/some/path?foo=bar&baz=xoxo"
* ```
*
- * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
+ * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`)
* @return {string} url
*/
url: function(url) {
@@ -13176,16 +6987,16 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return protocol of current url.
+ * Return protocol of current URL.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var protocol = $location.protocol();
* // => "http"
* ```
*
- * @return {string} protocol of current url
+ * @return {string} protocol of current URL
*/
protocol: locationGetter('$$protocol'),
@@ -13196,24 +7007,24 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return host of current url.
+ * Return host of current URL.
*
- * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
+ * Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var host = $location.host();
* // => "example.com"
*
- * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
* host = $location.host();
* // => "example.com"
* host = location.host;
* // => "example.com:8080"
* ```
*
- * @return {string} host of current url.
+ * @return {string} host of current URL.
*/
host: locationGetter('$$host'),
@@ -13224,11 +7035,11 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return port of current url.
+ * Return port of current URL.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var port = $location.port();
* // => 80
* ```
@@ -13244,7 +7055,7 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return path of current url when called without any parameter.
+ * Return path of current URL when called without any parameter.
*
* Change path when called with parameter and return `$location`.
*
@@ -13253,7 +7064,7 @@ var locationPrototype = {
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var path = $location.path();
* // => "/some/path"
* ```
@@ -13263,7 +7074,7 @@ var locationPrototype = {
*/
path: locationGetterSetter('$$path', function(path) {
path = path !== null ? path.toString() : '';
- return path.charAt(0) == '/' ? path : '/' + path;
+ return path.charAt(0) === '/' ? path : '/' + path;
}),
/**
@@ -13273,13 +7084,13 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return search part (as object) of current url when called without any parameter.
+ * Return search part (as object) of current URL when called without any parameter.
*
* Change search part when called with parameter and return `$location`.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var searchObject = $location.search();
* // => {foo: 'bar', baz: 'xoxo'}
*
@@ -13295,7 +7106,7 @@ var locationPrototype = {
* of `$location` to the specified value.
*
* If the argument is a hash object containing an array of values, these values will be encoded
- * as duplicate search parameters in the url.
+ * as duplicate search parameters in the URL.
*
* @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue`
* will override only a single search property.
@@ -13357,7 +7168,7 @@ var locationPrototype = {
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
* var hash = $location.hash();
* // => "hashValue"
* ```
@@ -13418,6 +7229,7 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun
// 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;
+ this.$$urlUpdatedByLocation = true;
return this;
};
@@ -13425,14 +7237,14 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun
function locationGetter(property) {
- return function() {
+ return /** @this */ function() {
return this[property];
};
}
function locationGetterSetter(property, preprocess) {
- return function(value) {
+ return /** @this */ function(value) {
if (isUndefined(value)) {
return this[property];
}
@@ -13474,11 +7286,13 @@ function locationGetterSetter(property, preprocess) {
/**
* @ngdoc provider
* @name $locationProvider
+ * @this
+ *
* @description
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
*/
function $LocationProvider() {
- var hashPrefix = '',
+ var hashPrefix = '!',
html5Mode = {
enabled: false,
requireBase: true,
@@ -13489,6 +7303,7 @@ function $LocationProvider() {
* @ngdoc method
* @name $locationProvider#hashPrefix
* @description
+ * The default value for the prefix is `'!'`.
* @param {string=} prefix Prefix for hash part (containing path and search)
* @returns {*} current value if used as getter or itself (chaining) if used as setter
*/
@@ -13515,8 +7330,12 @@ function $LocationProvider() {
* 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: `true`) When html5Mode is enabled,
- * enables/disables url rewriting for relative links.
+ * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled,
+ * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will
+ * only happen on links with an attribute that matches the given string. For example, if set
+ * to `'internal-link'`, then the URL will only be rewritten for `<a internal-link>` links.
+ * Note that [attribute name normalization](guide/directive#normalization) does not apply
+ * here, so `'internalLink'` will **not** match `'internal-link'`.
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
@@ -13534,7 +7353,7 @@ function $LocationProvider() {
html5Mode.requireBase = mode.requireBase;
}
- if (isBoolean(mode.rewriteLinks)) {
+ if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) {
html5Mode.rewriteLinks = mode.rewriteLinks;
}
@@ -13594,7 +7413,7 @@ function $LocationProvider() {
if (html5Mode.enabled) {
if (!baseHref && html5Mode.requireBase) {
throw $locationMinErr('nobase',
- "$location in HTML5 mode requires a <base> tag to be present!");
+ '$location in HTML5 mode requires a <base> tag to be present!');
}
appBase = serverBase(initialUrl) + (baseHref || '/');
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
@@ -13611,6 +7430,13 @@ function $LocationProvider() {
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
+ // Determine if two URLs are equal despite potentially having different encoding/normalizing
+ // such as $location.absUrl() vs $browser.url()
+ // See https://github.com/angular/angular.js/issues/16592
+ function urlsEqual(a, b) {
+ return a === b || urlResolve(a).href === urlResolve(b).href;
+ }
+
function setBrowserUrlWithFallback(url, replace, state) {
var oldUrl = $location.url();
var oldState = $location.$$state;
@@ -13631,10 +7457,11 @@ function $LocationProvider() {
}
$rootElement.on('click', function(event) {
+ var rewriteLinks = html5Mode.rewriteLinks;
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
- if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return;
+ if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return;
var elm = jqLite(event.target);
@@ -13644,6 +7471,8 @@ function $LocationProvider() {
if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
}
+ if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return;
+
var absHref = elm.prop('href');
// get the actual href attribute - see
// http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
@@ -13660,15 +7489,13 @@ function $LocationProvider() {
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,
+ // We do a preventDefault for all urls that are part of the AngularJS 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()) {
+ if ($location.absUrl() !== $browser.url()) {
$rootScope.$apply();
- // hack to work around FF6 bug 684208 when scenario runner clicks on links
- $window.angular['ff-684208-preventDefault'] = true;
}
}
}
@@ -13676,7 +7503,7 @@ function $LocationProvider() {
// rewrite hashbang url <> html5 url
- if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) {
+ if ($location.absUrl() !== initialUrl) {
$browser.url($location.absUrl(), true);
}
@@ -13685,7 +7512,7 @@ function $LocationProvider() {
// update $location when $browser url changes
$browser.onUrlChange(function(newUrl, newState) {
- if (isUndefined(stripBaseUrl(appBaseNoFile, newUrl))) {
+ if (!startsWith(newUrl, appBaseNoFile)) {
// If we are navigating outside of the app then force a reload
$window.location.href = newUrl;
return;
@@ -13695,7 +7522,6 @@ function $LocationProvider() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
- newUrl = trimEmptyHash(newUrl);
$location.$$parse(newUrl);
$location.$$state = newState;
@@ -13720,36 +7546,40 @@ function $LocationProvider() {
// update browser
$rootScope.$watch(function $locationWatch() {
- var oldUrl = trimEmptyHash($browser.url());
- var newUrl = trimEmptyHash($location.absUrl());
- var oldState = $browser.state();
- var currentReplace = $location.$$replace;
- var urlOrStateChanged = oldUrl !== newUrl ||
- ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
-
- if (initializing || urlOrStateChanged) {
- initializing = false;
-
- $rootScope.$evalAsync(function() {
- var newUrl = $location.absUrl();
- var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
- $location.$$state, oldState).defaultPrevented;
-
- // if the location was changed by a `$locationChangeStart` handler then stop
- // processing this location change
- if ($location.absUrl() !== newUrl) return;
-
- if (defaultPrevented) {
- $location.$$parse(oldUrl);
- $location.$$state = oldState;
- } else {
- if (urlOrStateChanged) {
- setBrowserUrlWithFallback(newUrl, currentReplace,
- oldState === $location.$$state ? null : $location.$$state);
+ if (initializing || $location.$$urlUpdatedByLocation) {
+ $location.$$urlUpdatedByLocation = false;
+
+ var oldUrl = $browser.url();
+ var newUrl = $location.absUrl();
+ var oldState = $browser.state();
+ var currentReplace = $location.$$replace;
+ var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) ||
+ ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
+
+ if (initializing || urlOrStateChanged) {
+ initializing = false;
+
+ $rootScope.$evalAsync(function() {
+ var newUrl = $location.absUrl();
+ var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
+ $location.$$state, oldState).defaultPrevented;
+
+ // if the location was changed by a `$locationChangeStart` handler then stop
+ // processing this location change
+ if ($location.absUrl() !== newUrl) return;
+
+ if (defaultPrevented) {
+ $location.$$parse(oldUrl);
+ $location.$$state = oldState;
+ } else {
+ if (urlOrStateChanged) {
+ setBrowserUrlWithFallback(newUrl, currentReplace,
+ oldState === $location.$$state ? null : $location.$$state);
+ }
+ afterLocationChange(oldUrl, oldState);
}
- afterLocationChange(oldUrl, oldState);
- }
- });
+ });
+ }
}
$location.$$replace = false;
@@ -13778,11 +7608,19 @@ function $LocationProvider() {
*
* The main purpose of this service is to simplify debugging and troubleshooting.
*
+ * To reveal the location of the calls to `$log` in the JavaScript console,
+ * you can "blackbox" the AngularJS source in your browser:
+ *
+ * [Mozilla description of blackboxing](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Black_box_a_source).
+ * [Chrome description of blackboxing](https://developer.chrome.com/devtools/docs/blackboxing).
+ *
+ * Note: Not all browsers support blackboxing.
+ *
* The default is to log `debug` messages. You can use
* {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this.
*
* @example
- <example module="logExample">
+ <example module="logExample" name="log-service">
<file name="script.js">
angular.module('logExample', [])
.controller('LogController', ['$scope', '$log', function($scope, $log) {
@@ -13808,6 +7646,8 @@ function $LocationProvider() {
/**
* @ngdoc provider
* @name $logProvider
+ * @this
+ *
* @description
* Use the `$logProvider` to configure how the application logs messages
*/
@@ -13825,13 +7665,22 @@ function $LogProvider() {
this.debugEnabled = function(flag) {
if (isDefined(flag)) {
debug = flag;
- return this;
+ return this;
} else {
return debug;
}
};
this.$get = ['$window', function($window) {
+ // Support: IE 9-11, Edge 12-14+
+ // IE/Edge display errors in such a way that it requires the user to click in 4 places
+ // to see the stack trace. There is no way to feature-detect it so there's a chance
+ // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't
+ // break apps. Other browsers display errors in a sensible way and some of them map stack
+ // traces along source maps if available so it makes sense to let browsers display it
+ // as they want.
+ var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent);
+
return {
/**
* @ngdoc method
@@ -13884,12 +7733,12 @@ function $LogProvider() {
fn.apply(self, arguments);
}
};
- }())
+ })()
};
function formatError(arg) {
- if (arg instanceof Error) {
- if (arg.stack) {
+ if (isError(arg)) {
+ if (arg.stack && formatStackTrace) {
arg = (arg.message && arg.stack.indexOf(arg.message) === -1)
? 'Error: ' + arg.message + '\n' + arg.stack
: arg.stack;
@@ -13902,29 +7751,17 @@ function $LogProvider() {
function consoleLog(type) {
var console = $window.console || {},
- logFn = console[type] || console.log || noop,
- hasApply = false;
-
- // Note: reading logFn.apply throws an error in IE11 in IE8 document mode.
- // The reason behind this is that console.log has type "object" in IE8...
- try {
- hasApply = !!logFn.apply;
- } catch (e) {}
+ logFn = console[type] || console.log || noop;
- if (hasApply) {
- return function() {
- var args = [];
- forEach(arguments, function(arg) {
- args.push(formatError(arg));
- });
- return logFn.apply(console, args);
- };
- }
-
- // we are IE which either doesn't have window.console => this is noop and we do nothing,
- // or we are IE where console.log doesn't have apply so we log at least first 2 args
- return function(arg1, arg2) {
- logFn(arg1, arg2 == null ? '' : arg2);
+ return function() {
+ var args = [];
+ forEach(arguments, function(arg) {
+ args.push(formatError(arg));
+ });
+ // Support: IE 9 only
+ // console methods don't inherit from Function.prototype in IE 9 so we can't
+ // call `logFn.apply(console, args)` directly.
+ return Function.prototype.apply.call(logFn, console, args);
};
}
}];
@@ -13943,41 +7780,23 @@ function $LogProvider() {
var $parseMinErr = minErr('$parse');
-// Sandboxing Angular Expressions
+var objectValueOf = {}.constructor.prototype.valueOf;
+
+// Sandboxing AngularJS Expressions
// ------------------------------
-// Angular expressions are generally considered safe because these expressions only have direct
-// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by
-// obtaining a reference to native JS functions such as the Function constructor.
+// AngularJS expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by
+// various means such as obtaining a reference to native JS functions like the Function constructor.
//
-// As an example, consider the following Angular expression:
+// As an example, consider the following AngularJS expression:
//
// {}.toString.constructor('alert("evil JS code")')
//
-// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
-// against the expression language, but not to prevent exploits that were enabled by exposing
-// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good
-// practice and therefore we are not even trying to protect against interaction with an object
-// explicitly exposed in this way.
-//
-// In general, it is not possible to access a Window object from an angular expression unless a
-// window or some DOM object that has a reference to window is published onto a Scope.
-// Similarly we prevent invocations of function known to be dangerous, as well as assignments to
-// native objects.
+// It is important to realize that if you create an expression from a string that contains user provided
+// content then it is possible that your application contains a security vulnerability to an XSS style attack.
//
// See https://docs.angularjs.org/guide/security
-function ensureSafeMemberName(name, fullExpression) {
- if (name === "__defineGetter__" || name === "__defineSetter__"
- || name === "__lookupGetter__" || name === "__lookupSetter__"
- || name === "__proto__") {
- throw $parseMinErr('isecfld',
- 'Attempting to access a disallowed field in Angular expressions! '
- + 'Expression: {0}', fullExpression);
- }
- return name;
-}
-
function getStringValue(name) {
// Property names must be strings. This means that non-string objects cannot be used
// as keys in an object. Any non-string object, including a number, is typecasted
@@ -13996,64 +7815,10 @@ function getStringValue(name) {
return name + '';
}
-function ensureSafeObject(obj, fullExpression) {
- // nifty check if obj is Function that is fast and works across iframes and other contexts
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isWindow(obj)
- obj.window === obj) {
- throw $parseMinErr('isecwindow',
- 'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isElement(obj)
- obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
- throw $parseMinErr('isecdom',
- 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// block Object so that we can't get hold of dangerous Object.* methods
- obj === Object) {
- throw $parseMinErr('isecobj',
- 'Referencing Object in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
- return obj;
-}
-
-var CALL = Function.prototype.call;
-var APPLY = Function.prototype.apply;
-var BIND = Function.prototype.bind;
-
-function ensureSafeFunction(obj, fullExpression) {
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (obj === CALL || obj === APPLY || obj === BIND) {
- throw $parseMinErr('isecff',
- 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
-}
-
-function ensureSafeAssignContext(obj, fullExpression) {
- if (obj) {
- if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor ||
- obj === {}.constructor || obj === [].constructor || obj === Function.constructor) {
- throw $parseMinErr('isecaf',
- 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression);
- }
- }
-}
var OPERATORS = createMap();
forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
-var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
+var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'};
/////////////////////////////////////////
@@ -14062,7 +7827,7 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'
/**
* @constructor
*/
-var Lexer = function(options) {
+var Lexer = function Lexer(options) {
this.options = options;
};
@@ -14076,7 +7841,7 @@ Lexer.prototype = {
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
- if (ch === '"' || ch === "'") {
+ if (ch === '"' || ch === '\'') {
this.readString(ch);
} else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
this.readNumber();
@@ -14115,7 +7880,7 @@ Lexer.prototype = {
},
isNumber: function(ch) {
- return ('0' <= ch && ch <= '9') && typeof ch === "string";
+ return ('0' <= ch && ch <= '9') && typeof ch === 'string';
},
isWhitespace: function(ch) {
@@ -14148,9 +7913,8 @@ Lexer.prototype = {
codePointAt: function(ch) {
if (ch.length === 1) return ch.charCodeAt(0);
- /*jshint bitwise: false*/
+ // eslint-disable-next-line no-bitwise
return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
- /*jshint bitwise: true*/
},
peekMultichar: function() {
@@ -14185,19 +7949,19 @@ Lexer.prototype = {
var start = this.index;
while (this.index < this.text.length) {
var ch = lowercase(this.text.charAt(this.index));
- if (ch == '.' || this.isNumber(ch)) {
+ if (ch === '.' || this.isNumber(ch)) {
number += ch;
} else {
var peekCh = this.peek();
- if (ch == 'e' && this.isExpOperator(peekCh)) {
+ if (ch === 'e' && this.isExpOperator(peekCh)) {
number += ch;
} else if (this.isExpOperator(ch) &&
peekCh && this.isNumber(peekCh) &&
- number.charAt(number.length - 1) == 'e') {
+ number.charAt(number.length - 1) === 'e') {
number += ch;
} else if (this.isExpOperator(ch) &&
(!peekCh || !this.isNumber(peekCh)) &&
- number.charAt(number.length - 1) == 'e') {
+ number.charAt(number.length - 1) === 'e') {
this.throwError('Invalid exponent');
} else {
break;
@@ -14272,7 +8036,7 @@ Lexer.prototype = {
}
};
-var AST = function(lexer, options) {
+var AST = function AST(lexer, options) {
this.lexer = lexer;
this.options = options;
};
@@ -14328,8 +8092,7 @@ AST.prototype = {
filterChain: function() {
var left = this.expression();
- var token;
- while ((token = this.expect('|'))) {
+ while (this.expect('|')) {
left = this.filter(left);
}
return left;
@@ -14342,6 +8105,10 @@ AST.prototype = {
assignment: function() {
var result = this.ternary();
if (this.expect('=')) {
+ if (!isAssignable(result)) {
+ throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
+ }
+
result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='};
}
return result;
@@ -14541,7 +8308,7 @@ AST.prototype = {
this.consume(':');
property.value = this.expression();
} else {
- this.throwError("invalid key", this.peek());
+ this.throwError('invalid key', this.peek());
}
properties.push(property);
} while (this.expect(','));
@@ -14622,14 +8389,47 @@ function isStateless($filter, filterName) {
return !fn.$stateful;
}
-function findConstantAndWatchExpressions(ast, $filter) {
+var PURITY_ABSOLUTE = 1;
+var PURITY_RELATIVE = 2;
+
+// Detect nodes which could depend on non-shallow state of objects
+function isPure(node, parentIsPure) {
+ switch (node.type) {
+ // Computed members might invoke a stateful toString()
+ case AST.MemberExpression:
+ if (node.computed) {
+ return false;
+ }
+ break;
+
+ // Unary always convert to primative
+ case AST.UnaryExpression:
+ return PURITY_ABSOLUTE;
+
+ // The binary + operator can invoke a stateful toString().
+ case AST.BinaryExpression:
+ return node.operator !== '+' ? PURITY_ABSOLUTE : false;
+
+ // Functions / filters probably read state from within objects
+ case AST.CallExpression:
+ return false;
+ }
+
+ return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure;
+}
+
+function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
var allConstants;
var argsToWatch;
+ var isStatelessFilter;
+
+ var astIsPure = ast.isPure = isPure(ast, parentIsPure);
+
switch (ast.type) {
case AST.Program:
allConstants = true;
forEach(ast.body, function(expr) {
- findConstantAndWatchExpressions(expr.expression, $filter);
+ findConstantAndWatchExpressions(expr.expression, $filter, astIsPure);
allConstants = allConstants && expr.expression.constant;
});
ast.constant = allConstants;
@@ -14639,26 +8439,26 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = [];
break;
case AST.UnaryExpression:
- findConstantAndWatchExpressions(ast.argument, $filter);
+ findConstantAndWatchExpressions(ast.argument, $filter, astIsPure);
ast.constant = ast.argument.constant;
ast.toWatch = ast.argument.toWatch;
break;
case AST.BinaryExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch);
break;
case AST.LogicalExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.ConditionalExpression:
- findConstantAndWatchExpressions(ast.test, $filter);
- findConstantAndWatchExpressions(ast.alternate, $filter);
- findConstantAndWatchExpressions(ast.consequent, $filter);
+ findConstantAndWatchExpressions(ast.test, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure);
ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
@@ -14667,29 +8467,28 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = [ast];
break;
case AST.MemberExpression:
- findConstantAndWatchExpressions(ast.object, $filter);
+ findConstantAndWatchExpressions(ast.object, $filter, astIsPure);
if (ast.computed) {
- findConstantAndWatchExpressions(ast.property, $filter);
+ findConstantAndWatchExpressions(ast.property, $filter, astIsPure);
}
ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
- ast.toWatch = [ast];
+ ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.CallExpression:
- allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false;
+ isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false;
+ allConstants = isStatelessFilter;
argsToWatch = [];
forEach(ast.arguments, function(expr) {
- findConstantAndWatchExpressions(expr, $filter);
+ findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
- if (!expr.constant) {
- argsToWatch.push.apply(argsToWatch, expr.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
- ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast];
+ ast.toWatch = isStatelessFilter ? argsToWatch : [ast];
break;
case AST.AssignmentExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = [ast];
break;
@@ -14697,11 +8496,9 @@ function findConstantAndWatchExpressions(ast, $filter) {
allConstants = true;
argsToWatch = [];
forEach(ast.elements, function(expr) {
- findConstantAndWatchExpressions(expr, $filter);
+ findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
- if (!expr.constant) {
- argsToWatch.push.apply(argsToWatch, expr.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
ast.toWatch = argsToWatch;
@@ -14710,10 +8507,14 @@ function findConstantAndWatchExpressions(ast, $filter) {
allConstants = true;
argsToWatch = [];
forEach(ast.properties, function(property) {
- findConstantAndWatchExpressions(property.value, $filter);
- allConstants = allConstants && property.value.constant && !property.computed;
- if (!property.value.constant) {
- argsToWatch.push.apply(argsToWatch, property.value.toWatch);
+ findConstantAndWatchExpressions(property.value, $filter, astIsPure);
+ allConstants = allConstants && property.value.constant;
+ argsToWatch.push.apply(argsToWatch, property.value.toWatch);
+ if (property.computed) {
+ //`{[key]: value}` implicitly does `key.toString()` which may be non-pure
+ findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false);
+ allConstants = allConstants && property.key.constant;
+ argsToWatch.push.apply(argsToWatch, property.key.toWatch);
}
});
ast.constant = allConstants;
@@ -14731,7 +8532,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
}
function getInputs(body) {
- if (body.length != 1) return;
+ if (body.length !== 1) return;
var lastExpression = body[0].expression;
var candidate = lastExpression.toWatch;
if (candidate.length !== 1) return candidate;
@@ -14760,19 +8561,16 @@ function isConstant(ast) {
return ast.constant;
}
-function ASTCompiler(astBuilder, $filter) {
- this.astBuilder = astBuilder;
+function ASTCompiler($filter) {
this.$filter = $filter;
}
ASTCompiler.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(ast) {
var self = this;
- var ast = this.astBuilder.ast(expression);
this.state = {
nextId: 0,
filters: {},
- expensiveChecks: expensiveChecks,
fn: {vars: [], body: [], own: {}},
assign: {vars: [], body: [], own: {}},
inputs: []
@@ -14797,7 +8595,7 @@ ASTCompiler.prototype = {
var intoId = self.nextId();
self.recurse(watch, intoId);
self.return_(intoId);
- self.state.inputs.push(fnKey);
+ self.state.inputs.push({name: fnKey, isPure: watch.isPure});
watch.watchId = key;
});
this.state.computing = 'fn';
@@ -14813,30 +8611,17 @@ ASTCompiler.prototype = {
this.watchFns() +
'return fn;';
- /* jshint -W054 */
+ // eslint-disable-next-line no-new-func
var fn = (new Function('$filter',
- 'ensureSafeMemberName',
- 'ensureSafeObject',
- 'ensureSafeFunction',
'getStringValue',
- 'ensureSafeAssignContext',
'ifDefined',
'plus',
- 'text',
fnString))(
this.$filter,
- ensureSafeMemberName,
- ensureSafeObject,
- ensureSafeFunction,
getStringValue,
- ensureSafeAssignContext,
ifDefined,
- plusFn,
- expression);
- /* jshint +W054 */
+ plusFn);
this.state = this.stage = undefined;
- fn.literal = isLiteral(ast);
- fn.constant = isConstant(ast);
return fn;
},
@@ -14846,13 +8631,16 @@ ASTCompiler.prototype = {
watchFns: function() {
var result = [];
- var fns = this.state.inputs;
+ var inputs = this.state.inputs;
var self = this;
- forEach(fns, function(name) {
- result.push('var ' + name + '=' + self.generateFunction(name, 's'));
+ forEach(inputs, function(input) {
+ result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's'));
+ if (input.isPure) {
+ result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';');
+ }
});
- if (fns.length) {
- result.push('fn.inputs=[' + fns.join(',') + '];');
+ if (inputs.length) {
+ result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];');
}
return result.join('');
},
@@ -14907,7 +8695,7 @@ ASTCompiler.prototype = {
case AST.Literal:
expression = this.escape(ast.value);
this.assign(intoId, expression);
- recursionFn(expression);
+ recursionFn(intoId || expression);
break;
case AST.UnaryExpression:
this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; });
@@ -14947,22 +8735,18 @@ ASTCompiler.prototype = {
nameId.computed = false;
nameId.name = ast.name;
}
- ensureSafeMemberName(ast.name);
self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)),
function() {
self.if_(self.stage === 'inputs' || 's', function() {
if (create && create !== 1) {
self.if_(
- self.not(self.nonComputedMember('s', ast.name)),
+ self.isNull(self.nonComputedMember('s', ast.name)),
self.lazyAssign(self.nonComputedMember('s', ast.name), '{}'));
}
self.assign(intoId, self.nonComputedMember('s', ast.name));
});
}, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name))
);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) {
- self.addEnsureSafeObject(intoId);
- }
recursionFn(intoId);
break;
case AST.MemberExpression:
@@ -14970,32 +8754,24 @@ ASTCompiler.prototype = {
intoId = intoId || this.nextId();
self.recurse(ast.object, left, undefined, function() {
self.if_(self.notNull(left), function() {
- if (create && create !== 1) {
- self.addEnsureSafeAssignContext(left);
- }
if (ast.computed) {
right = self.nextId();
self.recurse(ast.property, right);
self.getStringValue(right);
- self.addEnsureSafeMemberName(right);
if (create && create !== 1) {
self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
}
- expression = self.ensureSafeObject(self.computedMember(left, right));
+ expression = self.computedMember(left, right);
self.assign(intoId, expression);
if (nameId) {
nameId.computed = true;
nameId.name = right;
}
} else {
- ensureSafeMemberName(ast.property.name);
if (create && create !== 1) {
- self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
+ self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
}
expression = self.nonComputedMember(left, ast.property.name);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) {
- expression = self.ensureSafeObject(expression);
- }
self.assign(intoId, expression);
if (nameId) {
nameId.computed = false;
@@ -15027,21 +8803,16 @@ ASTCompiler.prototype = {
args = [];
self.recurse(ast.callee, right, left, function() {
self.if_(self.notNull(right), function() {
- self.addEnsureSafeFunction(right);
forEach(ast.arguments, function(expr) {
- self.recurse(expr, self.nextId(), undefined, function(argument) {
- args.push(self.ensureSafeObject(argument));
+ self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
+ args.push(argument);
});
});
if (left.name) {
- if (!self.state.expensiveChecks) {
- self.addEnsureSafeObject(left.context);
- }
expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')';
} else {
expression = right + '(' + args.join(',') + ')';
}
- expression = self.ensureSafeObject(expression);
self.assign(intoId, expression);
}, function() {
self.assign(intoId, 'undefined');
@@ -15053,14 +8824,9 @@ ASTCompiler.prototype = {
case AST.AssignmentExpression:
right = this.nextId();
left = {};
- if (!isAssignable(ast.left)) {
- throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
- }
this.recurse(ast.left, undefined, left, function() {
self.if_(self.notNull(left.context), function() {
self.recurse(ast.right, right);
- self.addEnsureSafeObject(self.member(left.context, left.name, left.computed));
- self.addEnsureSafeAssignContext(left.context);
expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
self.assign(intoId, expression);
recursionFn(intoId || expression);
@@ -15070,13 +8836,13 @@ ASTCompiler.prototype = {
case AST.ArrayExpression:
args = [];
forEach(ast.elements, function(expr) {
- self.recurse(expr, self.nextId(), undefined, function(argument) {
+ self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
args.push(argument);
});
});
expression = '[' + args.join(',') + ']';
this.assign(intoId, expression);
- recursionFn(expression);
+ recursionFn(intoId || expression);
break;
case AST.ObjectExpression:
args = [];
@@ -15118,15 +8884,15 @@ ASTCompiler.prototype = {
break;
case AST.ThisExpression:
this.assign(intoId, 's');
- recursionFn('s');
+ recursionFn(intoId || 's');
break;
case AST.LocalsExpression:
this.assign(intoId, 'l');
- recursionFn('l');
+ recursionFn(intoId || 'l');
break;
case AST.NGValueParameter:
this.assign(intoId, 'v');
- recursionFn('v');
+ recursionFn(intoId || 'v');
break;
}
},
@@ -15185,12 +8951,16 @@ ASTCompiler.prototype = {
return '!(' + expression + ')';
},
+ isNull: function(expression) {
+ return expression + '==null';
+ },
+
notNull: function(expression) {
return expression + '!=null';
},
nonComputedMember: function(left, right) {
- var SAFE_IDENTIFIER = /[$_a-zA-Z][$_a-zA-Z0-9]*/;
+ var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g;
if (SAFE_IDENTIFIER.test(right)) {
return left + '.' + right;
@@ -15208,42 +8978,10 @@ ASTCompiler.prototype = {
return this.nonComputedMember(left, right);
},
- addEnsureSafeObject: function(item) {
- this.current().body.push(this.ensureSafeObject(item), ';');
- },
-
- addEnsureSafeMemberName: function(item) {
- this.current().body.push(this.ensureSafeMemberName(item), ';');
- },
-
- addEnsureSafeFunction: function(item) {
- this.current().body.push(this.ensureSafeFunction(item), ';');
- },
-
- addEnsureSafeAssignContext: function(item) {
- this.current().body.push(this.ensureSafeAssignContext(item), ';');
- },
-
- ensureSafeObject: function(item) {
- return 'ensureSafeObject(' + item + ',text)';
- },
-
- ensureSafeMemberName: function(item) {
- return 'ensureSafeMemberName(' + item + ',text)';
- },
-
- ensureSafeFunction: function(item) {
- return 'ensureSafeFunction(' + item + ',text)';
- },
-
getStringValue: function(item) {
this.assign(item, 'getStringValue(' + item + ')');
},
- ensureSafeAssignContext: function(item) {
- return 'ensureSafeAssignContext(' + item + ',text)';
- },
-
lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
var self = this;
return function() {
@@ -15265,7 +9003,7 @@ ASTCompiler.prototype = {
},
escape: function(value) {
- if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'";
+ if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\'';
if (isNumber(value)) return value.toString();
if (value === true) return 'true';
if (value === false) return 'false';
@@ -15289,17 +9027,13 @@ ASTCompiler.prototype = {
};
-function ASTInterpreter(astBuilder, $filter) {
- this.astBuilder = astBuilder;
+function ASTInterpreter($filter) {
this.$filter = $filter;
}
ASTInterpreter.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(ast) {
var self = this;
- var ast = this.astBuilder.ast(expression);
- this.expression = expression;
- this.expensiveChecks = expensiveChecks;
findConstantAndWatchExpressions(ast, self.$filter);
var assignable;
var assign;
@@ -15312,6 +9046,7 @@ ASTInterpreter.prototype = {
inputs = [];
forEach(toWatch, function(watch, key) {
var input = self.recurse(watch);
+ input.isPure = watch.isPure;
watch.input = input;
inputs.push(input);
watch.watchId = key;
@@ -15338,13 +9073,11 @@ ASTInterpreter.prototype = {
if (inputs) {
fn.inputs = inputs;
}
- fn.literal = isLiteral(ast);
- fn.constant = isConstant(ast);
return fn;
},
recurse: function(ast, context, create) {
- var left, right, self = this, args, expression;
+ var left, right, self = this, args;
if (ast.input) {
return this.inputs(ast.input, ast.watchId);
}
@@ -15370,20 +9103,16 @@ ASTInterpreter.prototype = {
context
);
case AST.Identifier:
- ensureSafeMemberName(ast.name, self.expression);
- return self.identifier(ast.name,
- self.expensiveChecks || isPossiblyDangerousMemberName(ast.name),
- context, create, self.expression);
+ return self.identifier(ast.name, context, create);
case AST.MemberExpression:
left = this.recurse(ast.object, false, !!create);
if (!ast.computed) {
- ensureSafeMemberName(ast.property.name, self.expression);
right = ast.property.name;
}
if (ast.computed) right = this.recurse(ast.property);
return ast.computed ?
- this.computedMember(left, right, context, create, self.expression) :
- this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression);
+ this.computedMember(left, right, context, create) :
+ this.nonComputedMember(left, right, context, create);
case AST.CallExpression:
args = [];
forEach(ast.arguments, function(expr) {
@@ -15404,13 +9133,11 @@ ASTInterpreter.prototype = {
var rhs = right(scope, locals, assign, inputs);
var value;
if (rhs.value != null) {
- ensureSafeObject(rhs.context, self.expression);
- ensureSafeFunction(rhs.value, self.expression);
var values = [];
for (var i = 0; i < args.length; ++i) {
- values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression));
+ values.push(args[i](scope, locals, assign, inputs));
}
- value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression);
+ value = rhs.value.apply(rhs.context, values);
}
return context ? {value: value} : value;
};
@@ -15420,8 +9147,6 @@ ASTInterpreter.prototype = {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
- ensureSafeObject(lhs.value, self.expression);
- ensureSafeAssignContext(lhs.context);
lhs.context[lhs.name] = rhs;
return context ? {value: rhs} : rhs;
};
@@ -15497,7 +9222,7 @@ ASTInterpreter.prototype = {
if (isDefined(arg)) {
arg = -arg;
} else {
- arg = 0;
+ arg = -0;
}
return context ? {value: arg} : arg;
};
@@ -15556,12 +9281,14 @@ ASTInterpreter.prototype = {
},
'binary==': function(left, right, context) {
return function(scope, locals, assign, inputs) {
+ // eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary!=': function(left, right, context) {
return function(scope, locals, assign, inputs) {
+ // eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
@@ -15611,16 +9338,13 @@ ASTInterpreter.prototype = {
value: function(value, context) {
return function() { return context ? {context: undefined, name: undefined, value: value} : value; };
},
- identifier: function(name, expensiveChecks, context, create, expression) {
+ identifier: function(name, context, create) {
return function(scope, locals, assign, inputs) {
var base = locals && (name in locals) ? locals : scope;
- if (create && create !== 1 && base && !(base[name])) {
+ if (create && create !== 1 && base && base[name] == null) {
base[name] = {};
}
var value = base ? base[name] : undefined;
- if (expensiveChecks) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: base, name: name, value: value};
} else {
@@ -15628,7 +9352,7 @@ ASTInterpreter.prototype = {
}
};
},
- computedMember: function(left, right, context, create, expression) {
+ computedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs;
@@ -15636,15 +9360,12 @@ ASTInterpreter.prototype = {
if (lhs != null) {
rhs = right(scope, locals, assign, inputs);
rhs = getStringValue(rhs);
- ensureSafeMemberName(rhs, expression);
if (create && create !== 1) {
- ensureSafeAssignContext(lhs);
if (lhs && !(lhs[rhs])) {
lhs[rhs] = {};
}
}
value = lhs[rhs];
- ensureSafeObject(value, expression);
}
if (context) {
return {context: lhs, name: rhs, value: value};
@@ -15653,19 +9374,15 @@ ASTInterpreter.prototype = {
}
};
},
- nonComputedMember: function(left, right, expensiveChecks, context, create, expression) {
+ nonComputedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
if (create && create !== 1) {
- ensureSafeAssignContext(lhs);
- if (lhs && !(lhs[right])) {
+ if (lhs && lhs[right] == null) {
lhs[right] = {};
}
}
var value = lhs != null ? lhs[right] : undefined;
- if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: lhs, name: right, value: value};
} else {
@@ -15684,28 +9401,38 @@ ASTInterpreter.prototype = {
/**
* @constructor
*/
-var Parser = function(lexer, $filter, options) {
- this.lexer = lexer;
- this.$filter = $filter;
- this.options = options;
+function Parser(lexer, $filter, options) {
this.ast = new AST(lexer, options);
- this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) :
- new ASTCompiler(this.ast, $filter);
-};
+ this.astCompiler = options.csp ? new ASTInterpreter($filter) :
+ new ASTCompiler($filter);
+}
Parser.prototype = {
constructor: Parser,
parse: function(text) {
- return this.astCompiler.compile(text, this.options.expensiveChecks);
- }
-};
+ var ast = this.getAst(text);
+ var fn = this.astCompiler.compile(ast.ast);
+ fn.literal = isLiteral(ast.ast);
+ fn.constant = isConstant(ast.ast);
+ fn.oneTime = ast.oneTime;
+ return fn;
+ },
-function isPossiblyDangerousMemberName(name) {
- return name == 'constructor';
-}
+ getAst: function(exp) {
+ var oneTime = false;
+ exp = exp.trim();
-var objectValueOf = Object.prototype.valueOf;
+ if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
+ oneTime = true;
+ exp = exp.substring(2);
+ }
+ return {
+ ast: this.ast.ast(exp),
+ oneTime: oneTime
+ };
+ }
+};
function getValueOf(value) {
return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value);
@@ -15720,15 +9447,15 @@ function getValueOf(value) {
*
* @description
*
- * Converts Angular {@link guide/expression expression} into a function.
+ * Converts AngularJS {@link guide/expression expression} into a function.
*
* ```js
* var getter = $parse('user.name');
* var setter = getter.assign;
- * var context = {user:{name:'angular'}};
+ * var context = {user:{name:'AngularJS'}};
* var locals = {user:{name:'local'}};
*
- * expect(getter(context)).toEqual('angular');
+ * expect(getter(context)).toEqual('AngularJS');
* setter(context, 'newValue');
* expect(context.user.name).toEqual('newValue');
* expect(getter(context, locals)).toEqual('local');
@@ -15757,14 +9484,14 @@ function getValueOf(value) {
/**
* @ngdoc provider
* @name $parseProvider
+ * @this
*
* @description
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
* service.
*/
function $ParseProvider() {
- var cacheDefault = createMap();
- var cacheExpensive = createMap();
+ var cache = createMap();
var literals = {
'true': true,
'false': false,
@@ -15791,9 +9518,10 @@ function $ParseProvider() {
/**
* @ngdoc method
* @name $parseProvider#setIdentifierFns
+ *
* @description
*
- * Allows defining the set of characters that are allowed in Angular expressions. The function
+ * Allows defining the set of characters that are allowed in AngularJS expressions. The function
* `identifierStart` will get called to know if a given character is a valid character to be the
* first character for an identifier. The function `identifierContinue` will get called to know if
* a given character is a valid character to be a follow-up identifier character. The functions
@@ -15803,7 +9531,7 @@ function $ParseProvider() {
* representation. It is expected for the function to return `true` or `false`, whether that
* character is allowed or not.
*
- * Since this function will be called extensivelly, keep the implementation of these functions fast,
+ * Since this function will be called extensively, keep the implementation of these functions fast,
* as the performance of these functions have a direct impact on the expressions parsing speed.
*
* @param {function=} identifierStart The function that will decide whether the given character is
@@ -15821,60 +9549,29 @@ function $ParseProvider() {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: noUnsafeEval,
- expensiveChecks: false,
- literals: copy(literals),
- isIdentifierStart: isFunction(identStart) && identStart,
- isIdentifierContinue: isFunction(identContinue) && identContinue
- },
- $parseOptionsExpensive = {
- csp: noUnsafeEval,
- expensiveChecks: true,
literals: copy(literals),
isIdentifierStart: isFunction(identStart) && identStart,
isIdentifierContinue: isFunction(identContinue) && identContinue
};
- var runningChecksEnabled = false;
-
- $parse.$$runningExpensiveChecks = function() {
- return runningChecksEnabled;
- };
-
+ $parse.$$getAst = $$getAst;
return $parse;
- function $parse(exp, interceptorFn, expensiveChecks) {
- var parsedExpression, oneTime, cacheKey;
-
- expensiveChecks = expensiveChecks || runningChecksEnabled;
+ function $parse(exp, interceptorFn) {
+ var parsedExpression, cacheKey;
switch (typeof exp) {
case 'string':
exp = exp.trim();
cacheKey = exp;
- var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
parsedExpression = cache[cacheKey];
if (!parsedExpression) {
- if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
- oneTime = true;
- exp = exp.substring(2);
- }
- var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
- var lexer = new Lexer(parseOptions);
- var parser = new Parser(lexer, $filter, parseOptions);
+ var lexer = new Lexer($parseOptions);
+ var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp);
- if (parsedExpression.constant) {
- parsedExpression.$$watchDelegate = constantWatchDelegate;
- } else if (oneTime) {
- parsedExpression.$$watchDelegate = parsedExpression.literal ?
- oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
- } else if (parsedExpression.inputs) {
- parsedExpression.$$watchDelegate = inputsWatchDelegate;
- }
- if (expensiveChecks) {
- parsedExpression = expensiveChecksInterceptor(parsedExpression);
- }
- cache[cacheKey] = parsedExpression;
+
+ cache[cacheKey] = addWatchDelegate(parsedExpression);
}
return addInterceptor(parsedExpression, interceptorFn);
@@ -15886,31 +9583,13 @@ function $ParseProvider() {
}
}
- function expensiveChecksInterceptor(fn) {
- if (!fn) return fn;
- expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate;
- expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign);
- expensiveCheckFn.constant = fn.constant;
- expensiveCheckFn.literal = fn.literal;
- for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) {
- fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]);
- }
- expensiveCheckFn.inputs = fn.inputs;
-
- return expensiveCheckFn;
-
- function expensiveCheckFn(scope, locals, assign, inputs) {
- var expensiveCheckOldValue = runningChecksEnabled;
- runningChecksEnabled = true;
- try {
- return fn(scope, locals, assign, inputs);
- } finally {
- runningChecksEnabled = expensiveCheckOldValue;
- }
- }
+ function $$getAst(exp) {
+ var lexer = new Lexer($parseOptions);
+ var parser = new Parser(lexer, $filter, $parseOptions);
+ return parser.getAst(exp).ast;
}
- function expressionInputDirtyCheck(newValue, oldValueOfValue) {
+ function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) {
if (newValue == null || oldValueOfValue == null) { // null/undefined
return newValue === oldValueOfValue;
@@ -15923,7 +9602,7 @@ function $ParseProvider() {
// be cheaply dirty-checked
newValue = getValueOf(newValue);
- if (typeof newValue === 'object') {
+ if (typeof newValue === 'object' && !compareObjectIdentity) {
// objects/arrays are not supported - deep-watching them would be too expensive
return false;
}
@@ -15932,6 +9611,7 @@ function $ParseProvider() {
}
//Primitive or NaN
+ // eslint-disable-next-line no-self-compare
return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue);
}
@@ -15944,7 +9624,7 @@ function $ParseProvider() {
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newInputValue = inputExpressions(scope);
- if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) {
+ if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) {
lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
oldInputValueOf = newInputValue && getValueOf(newInputValue);
}
@@ -15964,7 +9644,7 @@ function $ParseProvider() {
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
- if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) {
+ if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) {
oldInputValues[i] = newInputValue;
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
@@ -15978,91 +9658,127 @@ function $ParseProvider() {
}, listener, objectEquality, prettyPrintExpression);
}
- function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
+ function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
+ var isDone = parsedExpression.literal ? isAllDefined : isDefined;
var unwatch, lastValue;
- return unwatch = scope.$watch(function oneTimeWatch(scope) {
- return parsedExpression(scope);
- }, function oneTimeListener(value, old, scope) {
- lastValue = value;
- if (isFunction(listener)) {
- listener.apply(this, arguments);
- }
- if (isDefined(value)) {
- scope.$$postDigest(function() {
- if (isDefined(lastValue)) {
- unwatch();
- }
- });
- }
- }, objectEquality);
- }
- function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
- var unwatch, lastValue;
- return unwatch = scope.$watch(function oneTimeWatch(scope) {
- return parsedExpression(scope);
- }, function oneTimeListener(value, old, scope) {
- lastValue = value;
- if (isFunction(listener)) {
- listener.call(this, value, old, scope);
- }
- if (isAllDefined(value)) {
- scope.$$postDigest(function() {
- if (isAllDefined(lastValue)) unwatch();
- });
+ var exp = parsedExpression.$$intercepted || parsedExpression;
+ var post = parsedExpression.$$interceptor || identity;
+
+ var useInputs = parsedExpression.inputs && !exp.inputs;
+
+ // Propogate the literal/inputs/constant attributes
+ // ... but not oneTime since we are handling it
+ oneTimeWatch.literal = parsedExpression.literal;
+ oneTimeWatch.constant = parsedExpression.constant;
+ oneTimeWatch.inputs = parsedExpression.inputs;
+
+ // Allow other delegates to run on this wrapped expression
+ addWatchDelegate(oneTimeWatch);
+
+ unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
+
+ return unwatch;
+
+ function unwatchIfDone() {
+ if (isDone(lastValue)) {
+ unwatch();
}
- }, objectEquality);
+ }
- function isAllDefined(value) {
- var allDefined = true;
- forEach(value, function(val) {
- if (!isDefined(val)) allDefined = false;
- });
- return allDefined;
+ function oneTimeWatch(scope, locals, assign, inputs) {
+ lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs);
+ if (isDone(lastValue)) {
+ scope.$$postDigest(unwatchIfDone);
+ }
+ return post(lastValue);
}
}
+ function isAllDefined(value) {
+ var allDefined = true;
+ forEach(value, function(val) {
+ if (!isDefined(val)) allDefined = false;
+ });
+ return allDefined;
+ }
+
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
- var unwatch;
- return unwatch = scope.$watch(function constantWatch(scope) {
+ var unwatch = scope.$watch(function constantWatch(scope) {
unwatch();
return parsedExpression(scope);
}, listener, objectEquality);
+ return unwatch;
+ }
+
+ function addWatchDelegate(parsedExpression) {
+ if (parsedExpression.constant) {
+ parsedExpression.$$watchDelegate = constantWatchDelegate;
+ } else if (parsedExpression.oneTime) {
+ parsedExpression.$$watchDelegate = oneTimeWatchDelegate;
+ } else if (parsedExpression.inputs) {
+ parsedExpression.$$watchDelegate = inputsWatchDelegate;
+ }
+
+ return parsedExpression;
+ }
+
+ function chainInterceptors(first, second) {
+ function chainedInterceptor(value) {
+ return second(first(value));
+ }
+ chainedInterceptor.$stateful = first.$stateful || second.$stateful;
+ chainedInterceptor.$$pure = first.$$pure && second.$$pure;
+
+ return chainedInterceptor;
}
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
- var watchDelegate = parsedExpression.$$watchDelegate;
- var useInputs = false;
- var regularWatch =
- watchDelegate !== oneTimeLiteralWatchDelegate &&
- watchDelegate !== oneTimeWatchDelegate;
+ // Extract any existing interceptors out of the parsedExpression
+ // to ensure the original parsedExpression is always the $$intercepted
+ if (parsedExpression.$$interceptor) {
+ interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn);
+ parsedExpression = parsedExpression.$$intercepted;
+ }
+
+ var useInputs = false;
- var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
+ var fn = function interceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
- return interceptorFn(value, scope, locals);
- } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
- var value = parsedExpression(scope, locals, assign, inputs);
- var result = interceptorFn(value, scope, locals);
- // we only return the interceptor's result if the
- // initial value is defined (for bind-once)
- return isDefined(value) ? result : value;
+ return interceptorFn(value);
};
- // Propagate $$watchDelegates other then inputsWatchDelegate
- if (parsedExpression.$$watchDelegate &&
- parsedExpression.$$watchDelegate !== inputsWatchDelegate) {
- fn.$$watchDelegate = parsedExpression.$$watchDelegate;
- } else if (!interceptorFn.$stateful) {
- // If there is an interceptor, but no watchDelegate then treat the interceptor like
- // we treat filters - it is assumed to be a pure function unless flagged with $stateful
- fn.$$watchDelegate = inputsWatchDelegate;
+ // Maintain references to the interceptor/intercepted
+ fn.$$intercepted = parsedExpression;
+ fn.$$interceptor = interceptorFn;
+
+ // Propogate the literal/oneTime/constant attributes
+ fn.literal = parsedExpression.literal;
+ fn.oneTime = parsedExpression.oneTime;
+ fn.constant = parsedExpression.constant;
+
+ // Treat the interceptor like filters.
+ // If it is not $stateful then only watch its inputs.
+ // If the expression itself has no inputs then use the full expression as an input.
+ if (!interceptorFn.$stateful) {
useInputs = !parsedExpression.inputs;
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
+
+ if (!interceptorFn.$$pure) {
+ fn.inputs = fn.inputs.map(function(e) {
+ // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a
+ // non-pure interceptor function.
+ if (e.isPure === PURITY_RELATIVE) {
+ return function depurifier(s) { return e(s); };
+ }
+ return e;
+ });
+ }
}
- return fn;
+ return addWatchDelegate(fn);
}
}];
}
@@ -16076,13 +9792,13 @@ function $ParseProvider() {
* A service that helps you run functions asynchronously, and use their return values (or exceptions)
* when they are done processing.
*
- * This is an implementation of promises/deferred objects inspired by
- * [Kris Kowal's Q](https://github.com/kriskowal/q).
+ * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred
+ * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
* implementations, and the other which resembles ES6 (ES2015) promises to some degree.
*
- * # $q constructor
+ * ## $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
* function as the first argument. This is similar to the native Promise implementation from ES6,
@@ -16170,7 +9886,7 @@ function $ParseProvider() {
* For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the
* section on serial or parallel joining of promises.
*
- * # The Deferred API
+ * ## The Deferred API
*
* A new instance of deferred is constructed by calling `$q.defer()`.
*
@@ -16192,7 +9908,7 @@ function $ParseProvider() {
* - promise – `{Promise}` – promise object associated with this deferred.
*
*
- * # The Promise API
+ * ## The Promise API
*
* A new promise instance is created when a deferred instance is created and can be retrieved by
* calling `deferred.promise`.
@@ -16224,7 +9940,7 @@ function $ParseProvider() {
* specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
* more information.
*
- * # Chaining promises
+ * ## Chaining promises
*
* Because calling the `then` method of a promise returns a new derived promise, it is easily
* possible to create a chain of promises:
@@ -16244,17 +9960,17 @@ function $ParseProvider() {
* $http's response interceptors.
*
*
- * # Differences between Kris Kowal's Q and $q
+ * ## Differences between Kris Kowal's Q and $q
*
* There are two main differences:
*
* - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation
- * mechanism in angular, which means faster propagation of resolution or rejection into your
+ * mechanism in AngularJS, which means faster propagation of resolution or rejection into your
* models and avoiding unnecessary browser repaints, which would result in flickering UI.
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
- * # Testing
+ * ## Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
@@ -16284,21 +10000,61 @@ function $ParseProvider() {
*
* @returns {Promise} The newly created promise.
*/
+/**
+ * @ngdoc provider
+ * @name $qProvider
+ * @this
+ *
+ * @description
+ */
function $QProvider() {
-
+ var errorOnUnhandledRejections = true;
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
return qFactory(function(callback) {
$rootScope.$evalAsync(callback);
- }, $exceptionHandler);
+ }, $exceptionHandler, errorOnUnhandledRejections);
}];
+
+ /**
+ * @ngdoc method
+ * @name $qProvider#errorOnUnhandledRejections
+ * @kind function
+ *
+ * @description
+ * Retrieves or overrides whether to generate an error when a rejected promise is not handled.
+ * This feature is enabled by default.
+ *
+ * @param {boolean=} value Whether to generate an error when a rejected promise is not handled.
+ * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for
+ * chaining otherwise.
+ */
+ this.errorOnUnhandledRejections = function(value) {
+ if (isDefined(value)) {
+ errorOnUnhandledRejections = value;
+ return this;
+ } else {
+ return errorOnUnhandledRejections;
+ }
+ };
}
+/** @this */
function $$QProvider() {
+ var errorOnUnhandledRejections = true;
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
return qFactory(function(callback) {
$browser.defer(callback);
- }, $exceptionHandler);
+ }, $exceptionHandler, errorOnUnhandledRejections);
}];
+
+ this.errorOnUnhandledRejections = function(value) {
+ if (isDefined(value)) {
+ errorOnUnhandledRejections = value;
+ return this;
+ } else {
+ return errorOnUnhandledRejections;
+ }
+ };
}
/**
@@ -16307,10 +10063,14 @@ function $$QProvider() {
* @param {function(function)} nextTick Function for executing functions in the next turn.
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
* debugging purposes.
+ * @param {boolean=} errorOnUnhandledRejections Whether an error should be generated on unhandled
+ * promises rejections.
* @returns {object} Promise manager.
*/
-function qFactory(nextTick, exceptionHandler) {
+function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
var $qMinErr = minErr('$q', TypeError);
+ var queueSize = 0;
+ var checkQueue = [];
/**
* @ngdoc method
@@ -16322,14 +10082,18 @@ function qFactory(nextTick, exceptionHandler) {
*
* @returns {Deferred} Returns a new instance of deferred.
*/
- var defer = function() {
- var d = new Deferred();
- //Necessary to support unbound execution :/
- d.resolve = simpleBind(d, d.resolve);
- d.reject = simpleBind(d, d.reject);
- d.notify = simpleBind(d, d.notify);
- return d;
- };
+ function defer() {
+ return new Deferred();
+ }
+
+ function Deferred() {
+ var promise = this.promise = new Promise();
+ //Non prototype methods necessary to support unbound execution :/
+ this.resolve = function(val) { resolvePromise(promise, val); };
+ this.reject = function(reason) { rejectPromise(promise, reason); };
+ this.notify = function(progress) { notifyPromise(promise, progress); };
+ }
+
function Promise() {
this.$$state = { status: 0 };
@@ -16340,144 +10104,166 @@ function qFactory(nextTick, exceptionHandler) {
if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) {
return this;
}
- var result = new Deferred();
+ var result = new Promise();
this.$$state.pending = this.$$state.pending || [];
this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
- return result.promise;
+ return result;
},
- "catch": function(callback) {
+ 'catch': function(callback) {
return this.then(null, callback);
},
- "finally": function(callback, progressBack) {
+ 'finally': function(callback, progressBack) {
return this.then(function(value) {
- return handleCallback(value, true, callback);
+ return handleCallback(value, resolve, callback);
}, function(error) {
- return handleCallback(error, false, callback);
+ return handleCallback(error, reject, callback);
}, progressBack);
}
});
- //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native
- function simpleBind(context, fn) {
- return function(value) {
- fn.call(context, value);
- };
- }
-
function processQueue(state) {
- var fn, deferred, pending;
+ var fn, promise, pending;
pending = state.pending;
state.processScheduled = false;
state.pending = undefined;
- for (var i = 0, ii = pending.length; i < ii; ++i) {
- deferred = pending[i][0];
- fn = pending[i][state.status];
- try {
- if (isFunction(fn)) {
- deferred.resolve(fn(state.value));
- } else if (state.status === 1) {
- deferred.resolve(state.value);
+ try {
+ for (var i = 0, ii = pending.length; i < ii; ++i) {
+ markQStateExceptionHandled(state);
+ promise = pending[i][0];
+ fn = pending[i][state.status];
+ try {
+ if (isFunction(fn)) {
+ resolvePromise(promise, fn(state.value));
+ } else if (state.status === 1) {
+ resolvePromise(promise, state.value);
+ } else {
+ rejectPromise(promise, state.value);
+ }
+ } catch (e) {
+ rejectPromise(promise, e);
+ // This error is explicitly marked for being passed to the $exceptionHandler
+ if (e && e.$$passToExceptionHandler === true) {
+ exceptionHandler(e);
+ }
+ }
+ }
+ } finally {
+ --queueSize;
+ if (errorOnUnhandledRejections && queueSize === 0) {
+ nextTick(processChecks);
+ }
+ }
+ }
+
+ function processChecks() {
+ // eslint-disable-next-line no-unmodified-loop-condition
+ while (!queueSize && checkQueue.length) {
+ var toCheck = checkQueue.shift();
+ if (!isStateExceptionHandled(toCheck)) {
+ markQStateExceptionHandled(toCheck);
+ var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value);
+ if (isError(toCheck.value)) {
+ exceptionHandler(toCheck.value, errorMessage);
} else {
- deferred.reject(state.value);
+ exceptionHandler(errorMessage);
}
- } catch (e) {
- deferred.reject(e);
- exceptionHandler(e);
}
}
}
function scheduleProcessQueue(state) {
+ if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) {
+ if (queueSize === 0 && checkQueue.length === 0) {
+ nextTick(processChecks);
+ }
+ checkQueue.push(state);
+ }
if (state.processScheduled || !state.pending) return;
state.processScheduled = true;
+ ++queueSize;
nextTick(function() { processQueue(state); });
}
- function Deferred() {
- this.promise = new Promise();
+ function resolvePromise(promise, val) {
+ if (promise.$$state.status) return;
+ if (val === promise) {
+ $$reject(promise, $qMinErr(
+ 'qcycle',
+ 'Expected promise to be resolved with value other than itself \'{0}\'',
+ val));
+ } else {
+ $$resolve(promise, val);
+ }
+
}
- extend(Deferred.prototype, {
- resolve: function(val) {
- if (this.promise.$$state.status) return;
- if (val === this.promise) {
- this.$$reject($qMinErr(
- 'qcycle',
- "Expected promise to be resolved with value other than itself '{0}'",
- val));
+ function $$resolve(promise, val) {
+ var then;
+ var done = false;
+ try {
+ if (isObject(val) || isFunction(val)) then = val.then;
+ if (isFunction(then)) {
+ promise.$$state.status = -1;
+ then.call(val, doResolve, doReject, doNotify);
} else {
- this.$$resolve(val);
- }
-
- },
-
- $$resolve: function(val) {
- var then;
- var that = this;
- var done = false;
- try {
- if ((isObject(val) || isFunction(val))) then = val && val.then;
- if (isFunction(then)) {
- this.promise.$$state.status = -1;
- then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify));
- } else {
- this.promise.$$state.value = val;
- this.promise.$$state.status = 1;
- scheduleProcessQueue(this.promise.$$state);
- }
- } catch (e) {
- rejectPromise(e);
- exceptionHandler(e);
+ promise.$$state.value = val;
+ promise.$$state.status = 1;
+ scheduleProcessQueue(promise.$$state);
}
+ } catch (e) {
+ doReject(e);
+ }
- function resolvePromise(val) {
- if (done) return;
- done = true;
- that.$$resolve(val);
- }
- function rejectPromise(val) {
- if (done) return;
- done = true;
- that.$$reject(val);
- }
- },
+ function doResolve(val) {
+ if (done) return;
+ done = true;
+ $$resolve(promise, val);
+ }
+ function doReject(val) {
+ if (done) return;
+ done = true;
+ $$reject(promise, val);
+ }
+ function doNotify(progress) {
+ notifyPromise(promise, progress);
+ }
+ }
- reject: function(reason) {
- if (this.promise.$$state.status) return;
- this.$$reject(reason);
- },
+ function rejectPromise(promise, reason) {
+ if (promise.$$state.status) return;
+ $$reject(promise, reason);
+ }
- $$reject: function(reason) {
- this.promise.$$state.value = reason;
- this.promise.$$state.status = 2;
- scheduleProcessQueue(this.promise.$$state);
- },
+ function $$reject(promise, reason) {
+ promise.$$state.value = reason;
+ promise.$$state.status = 2;
+ scheduleProcessQueue(promise.$$state);
+ }
- notify: function(progress) {
- var callbacks = this.promise.$$state.pending;
+ function notifyPromise(promise, progress) {
+ var callbacks = promise.$$state.pending;
- if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) {
- nextTick(function() {
- var callback, result;
- for (var i = 0, ii = callbacks.length; i < ii; i++) {
- result = callbacks[i][0];
- callback = callbacks[i][3];
- try {
- result.notify(isFunction(callback) ? callback(progress) : progress);
- } catch (e) {
- exceptionHandler(e);
- }
+ if ((promise.$$state.status <= 0) && callbacks && callbacks.length) {
+ nextTick(function() {
+ var callback, result;
+ for (var i = 0, ii = callbacks.length; i < ii; i++) {
+ result = callbacks[i][0];
+ callback = callbacks[i][3];
+ try {
+ notifyPromise(result, isFunction(callback) ? callback(progress) : progress);
+ } catch (e) {
+ exceptionHandler(e);
}
- });
- }
+ }
+ });
}
- });
+ }
/**
* @ngdoc method
@@ -16515,39 +10301,27 @@ function qFactory(nextTick, exceptionHandler) {
* @param {*} reason Constant, message, exception or an object representing the rejection reason.
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
*/
- var reject = function(reason) {
- var result = new Deferred();
- result.reject(reason);
- return result.promise;
- };
-
- var makePromise = function makePromise(value, resolved) {
- var result = new Deferred();
- if (resolved) {
- result.resolve(value);
- } else {
- result.reject(value);
- }
- return result.promise;
- };
+ function reject(reason) {
+ var result = new Promise();
+ rejectPromise(result, reason);
+ return result;
+ }
- var handleCallback = function handleCallback(value, isResolved, callback) {
+ function handleCallback(value, resolver, callback) {
var callbackOutput = null;
try {
if (isFunction(callback)) callbackOutput = callback();
} catch (e) {
- return makePromise(e, false);
+ return reject(e);
}
if (isPromiseLike(callbackOutput)) {
return callbackOutput.then(function() {
- return makePromise(value, isResolved);
- }, function(error) {
- return makePromise(error, false);
- });
+ return resolver(value);
+ }, reject);
} else {
- return makePromise(value, isResolved);
+ return resolver(value);
}
- };
+ }
/**
* @ngdoc method
@@ -16567,11 +10341,11 @@ function qFactory(nextTick, exceptionHandler) {
*/
- var when = function(value, callback, errback, progressBack) {
- var result = new Deferred();
- result.resolve(value);
- return result.promise.then(callback, errback, progressBack);
- };
+ function when(value, callback, errback, progressBack) {
+ var result = new Promise();
+ resolvePromise(result, value);
+ return result.then(callback, errback, progressBack);
+ }
/**
* @ngdoc method
@@ -16606,27 +10380,25 @@ function qFactory(nextTick, exceptionHandler) {
*/
function all(promises) {
- var deferred = new Deferred(),
+ var result = new Promise(),
counter = 0,
results = isArray(promises) ? [] : {};
forEach(promises, function(promise, key) {
counter++;
when(promise).then(function(value) {
- if (results.hasOwnProperty(key)) return;
results[key] = value;
- if (!(--counter)) deferred.resolve(results);
+ if (!(--counter)) resolvePromise(result, results);
}, function(reason) {
- if (results.hasOwnProperty(key)) return;
- deferred.reject(reason);
+ rejectPromise(result, reason);
});
});
if (counter === 0) {
- deferred.resolve(results);
+ resolvePromise(result, results);
}
- return deferred.promise;
+ return result;
}
/**
@@ -16653,25 +10425,25 @@ function qFactory(nextTick, exceptionHandler) {
return deferred.promise;
}
- var $Q = function Q(resolver) {
+ function $Q(resolver) {
if (!isFunction(resolver)) {
- throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
+ throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver);
}
- var deferred = new Deferred();
+ var promise = new Promise();
function resolveFn(value) {
- deferred.resolve(value);
+ resolvePromise(promise, value);
}
function rejectFn(reason) {
- deferred.reject(reason);
+ rejectPromise(promise, reason);
}
resolver(resolveFn, rejectFn);
- return deferred.promise;
- };
+ return promise;
+ }
// Let's make the instanceof operator work for promises, so that
// `new $q(fn) instanceof $q` would evaluate to true.
@@ -16687,6 +10459,23 @@ function qFactory(nextTick, exceptionHandler) {
return $Q;
}
+function isStateExceptionHandled(state) {
+ return !!state.pur;
+}
+function markQStateExceptionHandled(state) {
+ state.pur = true;
+}
+function markQExceptionHandled(q) {
+ // Built-in `$q` promises will always have a `$$state` property. This check is to allow
+ // overwriting `$q` with a different promise library (e.g. Bluebird + angular-bluebird-promises).
+ // (Currently, this is the only method that might be called with a promise, even if it is not
+ // created by the built-in `$q`.)
+ if (q.$$state) {
+ markQStateExceptionHandled(q.$$state);
+ }
+}
+
+/** @this */
function $$RAFProvider() { //rAF
this.$get = ['$window', '$timeout', function($window, $timeout) {
var requestAnimationFrame = $window.requestAnimationFrame ||
@@ -16776,6 +10565,8 @@ function $$RAFProvider() { //rAF
/**
* @ngdoc service
* @name $rootScope
+ * @this
+ *
* @description
*
* Every application has a single root {@link ng.$rootScope.Scope scope}.
@@ -16806,6 +10597,7 @@ function $RootScopeProvider() {
this.$$watchersCount = 0;
this.$id = nextUid();
this.$$ChildScope = null;
+ this.$$suspended = false;
}
ChildScope.prototype = parent;
return ChildScope;
@@ -16820,14 +10612,19 @@ function $RootScopeProvider() {
function cleanUpScope($scope) {
+ // Support: IE 9 only
if (msie === 9) {
// There is a memory leak in IE9 if all child scopes are not disconnected
// completely when a scope is destroyed. So this code will recurse up through
// all this scopes children
//
// See issue https://github.com/angular/angular.js/issues/10706
- $scope.$$childHead && cleanUpScope($scope.$$childHead);
- $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling);
+ if ($scope.$$childHead) {
+ cleanUpScope($scope.$$childHead);
+ }
+ if ($scope.$$nextSibling) {
+ cleanUpScope($scope.$$nextSibling);
+ }
}
// The code below works around IE9 and V8's memory leaks
@@ -16853,7 +10650,7 @@ function $RootScopeProvider() {
* an in-depth introduction and usage examples.
*
*
- * # Inheritance
+ * ## Inheritance
* A scope can inherit from a parent scope, as in this example:
* ```js
var parent = $rootScope;
@@ -16888,6 +10685,7 @@ function $RootScopeProvider() {
this.$$childHead = this.$$childTail = null;
this.$root = this;
this.$$destroyed = false;
+ this.$$suspended = false;
this.$$listeners = {};
this.$$listenerCount = {};
this.$$watchersCount = 0;
@@ -16979,7 +10777,7 @@ function $RootScopeProvider() {
// 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', destroyChildScope);
+ if (isolate || parent !== this) child.$on('$destroy', destroyChildScope);
return child;
},
@@ -16996,7 +10794,7 @@ function $RootScopeProvider() {
* $digest()} and should return the value that will be watched. (`watchExpression` should not change
* its value when executed multiple times with the same input because it may be executed multiple
* times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be
- * [idempotent](http://en.wikipedia.org/wiki/Idempotence).
+ * [idempotent](http://en.wikipedia.org/wiki/Idempotence).)
* - The `listener` is called only when the value from the current `watchExpression` and the
* previous call to `watchExpression` are not equal (with the exception of the initial run,
* see below). Inequality is determined according to reference inequality,
@@ -17007,6 +10805,8 @@ function $RootScopeProvider() {
* according to the {@link angular.equals} function. To save the value of the object for
* later comparison, the {@link angular.copy} function is used. This therefore means that
* watching complex objects will have adverse memory and performance implications.
+ * - This should not be used to watch for changes in objects that are (or contain)
+ * [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}.
* - The watch `listener` may change the model, which may trigger other `listener`s to fire.
* This is achieved by rerunning the watchers until no changes are detected. The rerun
* iteration limit is 10 to prevent an infinite loop deadlock.
@@ -17026,7 +10826,7 @@ function $RootScopeProvider() {
*
*
*
- * # Example
+ * @example
* ```js
// let's assume that scope was dependency injected as the $rootScope
var scope = $rootScope;
@@ -17102,14 +10902,15 @@ function $RootScopeProvider() {
*/
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
var get = $parse(watchExp);
+ var fn = isFunction(listener) ? listener : noop;
if (get.$$watchDelegate) {
- return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
+ return get.$$watchDelegate(this, fn, objectEquality, get, watchExp);
}
var scope = this,
array = scope.$$watchers,
watcher = {
- fn: listener,
+ fn: fn,
last: initWatchVal,
get: get,
exp: prettyPrintExpression || watchExp,
@@ -17118,21 +10919,23 @@ function $RootScopeProvider() {
lastDirtyWatch = null;
- if (!isFunction(listener)) {
- watcher.fn = noop;
- }
-
if (!array) {
array = scope.$$watchers = [];
+ array.$$digestWatchIndex = -1;
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
+ array.$$digestWatchIndex++;
incrementWatchersCount(this, 1);
return function deregisterWatch() {
- if (arrayRemove(array, watcher) >= 0) {
+ var index = arrayRemove(array, watcher);
+ if (index >= 0) {
incrementWatchersCount(scope, -1);
+ if (index < array.$$digestWatchIndex) {
+ array.$$digestWatchIndex--;
+ }
}
lastDirtyWatch = null;
};
@@ -17147,8 +10950,8 @@ function $RootScopeProvider() {
* A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`.
* If any one expression in the collection changes the `listener` is executed.
*
- * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every
- * call to $digest() to see if any items changes.
+ * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return
+ * values are examined for changes on every call to `$digest`.
* - The `listener` is called whenever any expression in the `watchExpressions` array changes.
*
* @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually
@@ -17192,9 +10995,8 @@ function $RootScopeProvider() {
}
forEach(watchExpressions, function(expr, i) {
- var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) {
+ var unwatchFn = self.$watch(expr, function watchGroupSubAction(value) {
newValues[i] = value;
- oldValues[i] = oldValue;
if (!changeReactionScheduled) {
changeReactionScheduled = true;
self.$evalAsync(watchGroupAction);
@@ -17206,11 +11008,17 @@ function $RootScopeProvider() {
function watchGroupAction() {
changeReactionScheduled = false;
- if (firstRun) {
- firstRun = false;
- listener(newValues, newValues, self);
- } else {
- listener(newValues, oldValues, self);
+ try {
+ if (firstRun) {
+ firstRun = false;
+ listener(newValues, newValues, self);
+ } else {
+ listener(newValues, oldValues, self);
+ }
+ } finally {
+ for (var i = 0; i < watchExpressions.length; i++) {
+ oldValues[i] = newValues[i];
+ }
}
}
@@ -17238,7 +11046,7 @@ function $RootScopeProvider() {
* adding, removing, and moving items belonging to an object or array.
*
*
- * # Example
+ * @example
* ```js
$scope.names = ['igor', 'matias', 'misko', 'james'];
$scope.dataCount = 4;
@@ -17278,7 +11086,11 @@ function $RootScopeProvider() {
* de-registration function is executed, the internal watch operation is terminated.
*/
$watchCollection: function(obj, listener) {
- $watchCollectionInterceptor.$stateful = true;
+ // Mark the interceptor as
+ // ... $$pure when literal since the instance will change when any input changes
+ $watchCollectionInterceptor.$$pure = $parse(obj).literal;
+ // ... $stateful when non-literal since we must read the state of the collection
+ $watchCollectionInterceptor.$stateful = !$watchCollectionInterceptor.$$pure;
var self = this;
// the current value, updated on each dirty-check run
@@ -17329,6 +11141,7 @@ function $RootScopeProvider() {
oldItem = oldValue[i];
newItem = newValue[i];
+ // eslint-disable-next-line no-self-compare
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
@@ -17351,6 +11164,7 @@ function $RootScopeProvider() {
oldItem = oldValue[key];
if (key in oldValue) {
+ // eslint-disable-next-line no-self-compare
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
@@ -17434,7 +11248,7 @@ function $RootScopeProvider() {
*
* In unit tests, you may need to call `$digest()` to simulate the scope life cycle.
*
- * # Example
+ * @example
* ```js
var scope = ...;
scope.name = 'misko';
@@ -17463,9 +11277,8 @@ function $RootScopeProvider() {
$digest: function() {
var watch, value, last, fn, get,
watchers,
- length,
dirty, ttl = TTL,
- next, current, target = this,
+ next, current, target = asyncQueue.length ? $rootScope : this,
watchLog = [],
logIdx, asyncTask;
@@ -17487,12 +11300,13 @@ function $RootScopeProvider() {
current = target;
// It's safe for asyncQueuePosition to be a local variable here because this loop can't
- // be reentered recursively. Calling $digest from a function passed to $applyAsync would
+ // be reentered recursively. Calling $digest from a function passed to $evalAsync would
// lead to a '$digest already in progress' error.
for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) {
try {
asyncTask = asyncQueue[asyncQueuePosition];
- asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
+ fn = asyncTask.fn;
+ fn(asyncTask.scope, asyncTask.locals);
} catch (e) {
$exceptionHandler(e);
}
@@ -17502,12 +11316,12 @@ function $RootScopeProvider() {
traverseScopesLoop:
do { // "traverse the scopes" loop
- if ((watchers = current.$$watchers)) {
+ if ((watchers = !current.$$suspended && current.$$watchers)) {
// process our watches
- length = watchers.length;
- while (length--) {
+ watchers.$$digestWatchIndex = watchers.length;
+ while (watchers.$$digestWatchIndex--) {
try {
- watch = watchers[length];
+ watch = watchers[watchers.$$digestWatchIndex];
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch) {
@@ -17515,8 +11329,7 @@ function $RootScopeProvider() {
if ((value = get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
- : (typeof value === 'number' && typeof last === 'number'
- && isNaN(value) && isNaN(last)))) {
+ : (isNumberNaN(value) && isNumberNaN(last)))) {
dirty = true;
lastDirtyWatch = watch;
watch.last = watch.eq ? copy(value, null) : value;
@@ -17547,7 +11360,9 @@ function $RootScopeProvider() {
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast
- if (!(next = ((current.$$watchersCount && current.$$childHead) ||
+ // (though it differs due to having the extra check for $$suspended and does not
+ // check $$listenerCount)
+ if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
@@ -17578,8 +11393,101 @@ function $RootScopeProvider() {
}
}
postDigestQueue.length = postDigestQueuePosition = 0;
+
+ // Check for changes to browser url that happened during the $digest
+ // (for which no event is fired; e.g. via `history.pushState()`)
+ $browser.$$checkUrlChange();
+ },
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$suspend
+ * @kind function
+ *
+ * @description
+ * Suspend watchers of this scope subtree so that they will not be invoked during digest.
+ *
+ * This can be used to optimize your application when you know that running those watchers
+ * is redundant.
+ *
+ * **Warning**
+ *
+ * Suspending scopes from the digest cycle can have unwanted and difficult to debug results.
+ * Only use this approach if you are confident that you know what you are doing and have
+ * ample tests to ensure that bindings get updated as you expect.
+ *
+ * Some of the things to consider are:
+ *
+ * * Any external event on a directive/component will not trigger a digest while the hosting
+ * scope is suspended - even if the event handler calls `$apply()` or `$rootScope.$digest()`.
+ * * Transcluded content exists on a scope that inherits from outside a directive but exists
+ * as a child of the directive's containing scope. If the containing scope is suspended the
+ * transcluded scope will also be suspended, even if the scope from which the transcluded
+ * scope inherits is not suspended.
+ * * Multiple directives trying to manage the suspended status of a scope can confuse each other:
+ * * A call to `$suspend()` on an already suspended scope is a no-op.
+ * * A call to `$resume()` on a non-suspended scope is a no-op.
+ * * If two directives suspend a scope, then one of them resumes the scope, the scope will no
+ * longer be suspended. This could result in the other directive believing a scope to be
+ * suspended when it is not.
+ * * If a parent scope is suspended then all its descendants will be also excluded from future
+ * digests whether or not they have been suspended themselves. Note that this also applies to
+ * isolate child scopes.
+ * * Calling `$digest()` directly on a descendant of a suspended scope will still run the watchers
+ * for that scope and its descendants. When digesting we only check whether the current scope is
+ * locally suspended, rather than checking whether it has a suspended ancestor.
+ * * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be
+ * included in future digests until all its ancestors have been resumed.
+ * * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply()`
+ * against the `$rootScope` and so will still trigger a global digest even if the promise was
+ * initiated by a component that lives on a suspended scope.
+ */
+ $suspend: function() {
+ this.$$suspended = true;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$isSuspended
+ * @kind function
+ *
+ * @description
+ * Call this method to determine if this scope has been explicitly suspended. It will not
+ * tell you whether an ancestor has been suspended.
+ * To determine if this scope will be excluded from a digest triggered at the $rootScope,
+ * for example, you must check all its ancestors:
+ *
+ * ```
+ * function isExcludedFromDigest(scope) {
+ * while(scope) {
+ * if (scope.$isSuspended()) return true;
+ * scope = scope.$parent;
+ * }
+ * return false;
+ * ```
+ *
+ * Be aware that a scope may not be included in digests if it has a suspended ancestor,
+ * even if `$isSuspended()` returns false.
+ *
+ * @returns true if the current scope has been suspended.
+ */
+ $isSuspended: function() {
+ return this.$$suspended;
},
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$resume
+ * @kind function
+ *
+ * @description
+ * Resume watchers of this scope subtree in case it was suspended.
+ *
+ * See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach.
+ */
+ $resume: function() {
+ this.$$suspended = false;
+ },
/**
* @ngdoc event
@@ -17635,8 +11543,8 @@ function $RootScopeProvider() {
// sever all the references to parent scopes (after this cleanup, the current scope should
// not be retained by any of our references and should be eligible for garbage collection)
- if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
- if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
+ if (parent && parent.$$childHead === this) parent.$$childHead = this.$$nextSibling;
+ if (parent && parent.$$childTail === this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
@@ -17657,10 +11565,10 @@ function $RootScopeProvider() {
*
* @description
* Executes the `expression` on the current scope and returns the result. Any exceptions in
- * the expression are propagated (uncaught). This is useful when evaluating Angular
+ * the expression are propagated (uncaught). This is useful when evaluating AngularJS
* expressions.
*
- * # Example
+ * @example
* ```js
var scope = ng.$rootScope.Scope();
scope.a = 1;
@@ -17670,7 +11578,7 @@ function $RootScopeProvider() {
expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
* ```
*
- * @param {(string|function())=} expression An angular expression to be executed.
+ * @param {(string|function())=} expression An AngularJS expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
@@ -17705,7 +11613,7 @@ function $RootScopeProvider() {
* will be scheduled. However, it is encouraged to always call code that changes the model
* from within an `$apply` call. That includes code evaluated via `$evalAsync`.
*
- * @param {(string|function())=} expression An angular expression to be executed.
+ * @param {(string|function())=} expression An AngularJS expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
@@ -17720,10 +11628,10 @@ function $RootScopeProvider() {
if (asyncQueue.length) {
$rootScope.$digest();
}
- });
+ }, null, '$evalAsync');
}
- asyncQueue.push({scope: this, expression: $parse(expr), locals: locals});
+ asyncQueue.push({scope: this, fn: $parse(expr), locals: locals});
},
$$postDigest: function(fn) {
@@ -17736,15 +11644,14 @@ function $RootScopeProvider() {
* @kind function
*
* @description
- * `$apply()` is used to execute an expression in angular from outside of the angular
+ * `$apply()` is used to execute an expression in AngularJS from outside of the AngularJS
* framework. (For example from browser DOM events, setTimeout, XHR or third party libraries).
- * Because we are calling into the angular framework we need to perform proper scope life
+ * Because we are calling into the AngularJS framework we need to perform proper scope life
* cycle of {@link ng.$exceptionHandler exception handling},
* {@link ng.$rootScope.Scope#$digest executing watches}.
*
- * ## Life cycle
+ * **Life cycle: Pseudo-Code of `$apply()`**
*
- * # Pseudo-Code of `$apply()`
* ```js
function $apply(expr) {
try {
@@ -17768,7 +11675,7 @@ function $RootScopeProvider() {
* expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method.
*
*
- * @param {(string|function())=} exp An angular expression to be executed.
+ * @param {(string|function())=} exp An AngularJS expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with current `scope` parameter.
@@ -17790,6 +11697,7 @@ function $RootScopeProvider() {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
+ // eslint-disable-next-line no-unsafe-finally
throw e;
}
}
@@ -17807,14 +11715,16 @@ function $RootScopeProvider() {
* This can be used to queue up multiple expressions which need to be evaluated in the same
* digest.
*
- * @param {(string|function())=} exp An angular expression to be executed.
+ * @param {(string|function())=} exp An AngularJS expression to be executed.
*
* - `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 && applyAsyncQueue.push($applyAsyncExpression);
+ if (expr) {
+ applyAsyncQueue.push($applyAsyncExpression);
+ }
expr = $parse(expr);
scheduleApplyAsync();
@@ -17869,7 +11779,10 @@ function $RootScopeProvider() {
return function() {
var indexOfListener = namedListeners.indexOf(listener);
if (indexOfListener !== -1) {
- namedListeners[indexOfListener] = null;
+ // Use delete in the hope of the browser deallocating the memory for the array entry,
+ // while not shifting the array indexes of other listeners.
+ // See issue https://github.com/angular/angular.js/issues/16135
+ delete namedListeners[indexOfListener];
decrementListenerCount(self, 1, name);
}
};
@@ -17936,8 +11849,7 @@ function $RootScopeProvider() {
}
//if any listener on the current scope stops propagation, prevent bubbling
if (stopPropagation) {
- event.currentScope = null;
- return event;
+ break;
}
//traverse upwards
scope = scope.$parent;
@@ -18011,7 +11923,8 @@ function $RootScopeProvider() {
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $digest
- // (though it differs due to having the extra check for $$listenerCount)
+ // (though it differs due to having the extra check for $$listenerCount and
+ // does not check $$suspended)
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
@@ -18086,7 +11999,7 @@ function $RootScopeProvider() {
if (applyAsyncId === null) {
applyAsyncId = $browser.defer(function() {
$rootScope.$apply(flushApplyAsync);
- });
+ }, null, '$applyAsync');
}
}
}];
@@ -18097,7 +12010,7 @@ function $RootScopeProvider() {
* @name $rootElement
*
* @description
- * The root element of Angular application. This is either the element where {@link
+ * The root element of AngularJS application. This is either the element where {@link
* ng.directive:ngApp ngApp} was declared or the element passed into
* {@link angular.bootstrap}. The element represents the root element of application. It is also the
* location where the application's {@link auto.$injector $injector} service gets
@@ -18108,67 +12021,79 @@ function $RootScopeProvider() {
// the implementation is in angular.bootstrap
/**
+ * @this
* @description
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
function $$SanitizeUriProvider() {
- var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
- imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
+
+ var aHrefSanitizationTrustedUrlList = /^\s*(https?|s?ftp|mailto|tel|file):/,
+ imgSrcSanitizationTrustedUrlList = /^\s*((https?|ftp|file|blob):|data:image\/)/;
/**
* @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
+ * Retrieves or overrides the default regular expression that is used for determining trusted safe
* urls during a[href] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML anchor links.
+ *
+ * Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring
+ * the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)`
+ * which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL.
+ *
+ * If the URL matches the `aHrefSanitizationTrustedUrlList` regular expression, it is returned unchanged.
*
- * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
*
- * @param {RegExp=} regexp New regexp to whitelist urls with.
+ * @param {RegExp=} regexp New regexp to trust urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
- this.aHrefSanitizationWhitelist = function(regexp) {
+ this.aHrefSanitizationTrustedUrlList = function(regexp) {
if (isDefined(regexp)) {
- aHrefSanitizationWhitelist = regexp;
+ aHrefSanitizationTrustedUrlList = regexp;
return this;
}
- return aHrefSanitizationWhitelist;
+ return aHrefSanitizationTrustedUrlList;
};
/**
* @description
- * Retrieves or overrides the default regular expression that is used for whitelisting of safe
+ * Retrieves or overrides the default regular expression that is used for determining trusted safe
* urls during img[src] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links.
*
- * Any url about to be assigned to img[src] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * Any URL due to be assigned to an `img[src]` attribute via interpolation is marked as requiring
+ * the $sce.MEDIA_URL security context. When interpolation occurs a call is made to
+ * `$sce.trustAsMediaUrl(url)` which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize
+ * the potentially malicious URL.
*
- * @param {RegExp=} regexp New regexp to whitelist urls with.
+ * If the URL matches the `imgSrcSanitizationTrustedUrlList` regular expression, it is returned
+ * unchanged.
+ *
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
+ *
+ * @param {RegExp=} regexp New regexp to trust urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
- this.imgSrcSanitizationWhitelist = function(regexp) {
+ this.imgSrcSanitizationTrustedUrlList = function(regexp) {
if (isDefined(regexp)) {
- imgSrcSanitizationWhitelist = regexp;
+ imgSrcSanitizationTrustedUrlList = regexp;
return this;
}
- return imgSrcSanitizationWhitelist;
+ return imgSrcSanitizationTrustedUrlList;
};
this.$get = function() {
- return function sanitizeUri(uri, isImage) {
- var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
- var normalizedVal;
- normalizedVal = urlResolve(uri).href;
+ return function sanitizeUri(uri, isMediaUrl) {
+ // if (!uri) return uri;
+ var regex = isMediaUrl ? imgSrcSanitizationTrustedUrlList : aHrefSanitizationTrustedUrlList;
+ var normalizedVal = urlResolve(uri && uri.trim()).href;
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
return 'unsafe:' + normalizedVal;
}
@@ -18188,20 +12113,43 @@ function $$SanitizeUriProvider() {
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+/* exported $SceProvider, $SceDelegateProvider */
+
var $sceMinErr = minErr('$sce');
var SCE_CONTEXTS = {
+ // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding).
HTML: 'html',
+
+ // Style statements or stylesheets. Currently unused in AngularJS.
CSS: 'css',
+
+ // An URL used in a context where it refers to the source of media, which are not expected to be run
+ // as scripts, such as an image, audio, video, etc.
+ MEDIA_URL: 'mediaUrl',
+
+ // An URL used in a context where it does not refer to a resource that loads code.
+ // A value that can be trusted as a URL can also trusted as a MEDIA_URL.
URL: 'url',
- // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a
- // url. (e.g. ng-include, script src, templateUrl)
+
+ // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as
+ // code. (e.g. ng-include, script src binding, templateUrl)
+ // A value that can be trusted as a RESOURCE_URL, can also trusted as a URL and a MEDIA_URL.
RESOURCE_URL: 'resourceUrl',
+
+ // Script. Currently unused in AngularJS.
JS: 'js'
};
// Helper functions follow.
+var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g;
+
+function snakeToCamel(name) {
+ return name
+ .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace);
+}
+
function adjustMatcher(matcher) {
if (matcher === 'self') {
return matcher;
@@ -18215,8 +12163,8 @@ function adjustMatcher(matcher) {
'Illegal sequence *** in string matcher. String: {0}', matcher);
}
matcher = escapeForRegexp(matcher).
- replace('\\*\\*', '.*').
- replace('\\*', '[^:/.?&;]*');
+ replace(/\\\*\\\*/g, '.*').
+ replace(/\\\*/g, '[^:/.?&;]*');
return new RegExp('^' + matcher + '$');
} else if (isRegExp(matcher)) {
// The only other type of matcher allowed is a Regexp.
@@ -18251,6 +12199,16 @@ function adjustMatchers(matchers) {
* `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict
* Contextual Escaping (SCE)} services to AngularJS.
*
+ * For an overview of this service and the functionnality it provides in AngularJS, see the main
+ * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how
+ * SCE works in their application, which shouldn't be needed in most cases.
+ *
+ * <div class="alert alert-danger">
+ * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or
+ * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners,
+ * changes to this service will also influence users, so be extra careful and document your changes.
+ * </div>
+ *
* Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of
* the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is
* because, while the `$sce` provides numerous shorthand methods, etc., you really only need to
@@ -18262,125 +12220,177 @@ function adjustMatchers(matchers) {
* The default instance of `$sceDelegate` should work out of the box with little pain. While you
* can override it completely to change the behavior of `$sce`, the common case would
* involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting
- * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as
- * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist
- * $sceDelegateProvider.resourceUrlWhitelist} and {@link
- * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}
+ * your own trusted and banned resource lists for trusting URLs used for loading AngularJS resources
+ * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * $sceDelegateProvider.trustedResourceUrlList} and {@link
+ * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}
*/
/**
* @ngdoc provider
* @name $sceDelegateProvider
+ * @this
+ *
* @description
*
* The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
- * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure
- * that the URLs used for sourcing Angular templates are safe. Refer {@link
- * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and
- * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}
+ * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}.
+ *
+ * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and
+ * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularJS templates and
+ * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See
+ * {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * $sceDelegateProvider.trustedResourceUrlList} and
+ * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList},
*
- * For the general details about this service in Angular, read the main page for {@link ng.$sce
+ * For the general details about this service in AngularJS, read the main page for {@link ng.$sce
* Strict Contextual Escaping (SCE)}.
*
* **Example**: Consider the following case. <a name="example"></a>
*
* - your app is hosted at url `http://myapp.example.com/`
* - but some of your templates are hosted on other domains you control such as
- * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc.
+ * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc.
* - and you have an open redirect at `http://myapp.example.com/clickThru?...`.
*
* Here is what a secure configuration for this scenario might look like:
*
* ```
* angular.module('myApp', []).config(function($sceDelegateProvider) {
- * $sceDelegateProvider.resourceUrlWhitelist([
+ * $sceDelegateProvider.trustedResourceUrlList([
* // Allow same origin resource loads.
* 'self',
* // Allow loading from our assets domain. Notice the difference between * and **.
* 'http://srv*.assets.example.com/**'
* ]);
*
- * // The blacklist overrides the whitelist so the open redirect here is blocked.
- * $sceDelegateProvider.resourceUrlBlacklist([
+ * // The banned resource URL list overrides the trusted resource URL list so the open redirect
+ * // here is blocked.
+ * $sceDelegateProvider.bannedResourceUrlList([
* 'http://myapp.example.com/clickThru**'
* ]);
* });
* ```
+ * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require
+ * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates
+ * requested by {@link ng.$templateRequest $templateRequest} that are present in
+ * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism
+ * to populate your templates in that cache at config time, then it is a good idea to remove 'self'
+ * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types
+ * of issues, like for instance attacker-controlled `ng-includes`.
*/
function $SceDelegateProvider() {
this.SCE_CONTEXTS = SCE_CONTEXTS;
// Resource URLs can also be trusted by policy.
- var resourceUrlWhitelist = ['self'],
- resourceUrlBlacklist = [];
+ var trustedResourceUrlList = ['self'],
+ bannedResourceUrlList = [];
/**
* @ngdoc method
- * @name $sceDelegateProvider#resourceUrlWhitelist
+ * @name $sceDelegateProvider#trustedResourceUrlList
* @kind function
*
- * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
- *
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
+ * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with
+ * the value provided. This must be an array or null. A snapshot of this array is used so
+ * further changes to the array are ignored.
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.
*
- * <div class="alert alert-warning">
- * **Note:** an empty whitelist array will block all URLs!
- * </div>
+ * @return {Array} The currently set trusted resource URL array.
*
- * @return {Array} the currently set whitelist array.
+ * @description
+ * Sets/Gets the list trusted of resource URLs.
*
- * The **default value** when no whitelist has been explicitly set is `['self']` allowing only
- * same origin resource requests.
+ * The **default value** when no `trustedResourceUrlList` has been explicitly set is `['self']`
+ * allowing only same origin resource requests.
*
- * @description
- * Sets/Gets the whitelist of trusted resource URLs.
+ * <div class="alert alert-warning">
+ * **Note:** the default `trustedResourceUrlList` of 'self' is not recommended if your app shares
+ * its origin with other apps! It is a good idea to limit it to only your application's directory.
+ * </div>
*/
- this.resourceUrlWhitelist = function(value) {
+ this.trustedResourceUrlList = function(value) {
if (arguments.length) {
- resourceUrlWhitelist = adjustMatchers(value);
+ trustedResourceUrlList = adjustMatchers(value);
}
- return resourceUrlWhitelist;
+ return trustedResourceUrlList;
};
/**
* @ngdoc method
- * @name $sceDelegateProvider#resourceUrlBlacklist
+ * @name $sceDelegateProvider#resourceUrlWhitelist
* @kind function
*
- * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
- *
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
+ * @deprecated
+ * sinceVersion="1.8.1"
*
- * The typical usage for the blacklist is to **block
- * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
- * these would otherwise be trusted but actually return content from the redirected domain.
- *
- * Finally, **the blacklist overrides the whitelist** and has the final say.
+ * This method is deprecated. Use {@link $sceDelegateProvider#trustedResourceUrlList
+ * trustedResourceUrlList} instead.
+ */
+ Object.defineProperty(this, 'resourceUrlWhitelist', {
+ get: function() {
+ return this.trustedResourceUrlList;
+ },
+ set: function(value) {
+ this.trustedResourceUrlList = value;
+ }
+ });
+
+ /**
+ * @ngdoc method
+ * @name $sceDelegateProvider#bannedResourceUrlList
+ * @kind function
*
- * @return {Array} the currently set blacklist array.
+ * @param {Array=} bannedResourceUrlList When provided, replaces the `bannedResourceUrlList` with
+ * the value provided. This must be an array or null. A snapshot of this array is used so
+ * further changes to the array are ignored.</p><p>
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.</p><p>
+ * The typical usage for the `bannedResourceUrlList` is to **block
+ * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
+ * these would otherwise be trusted but actually return content from the redirected domain.
+ * </p><p>
+ * Finally, **the banned resource URL list overrides the trusted resource URL list** and has
+ * the final say.
*
- * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there
- * is no blacklist.)
+ * @return {Array} The currently set `bannedResourceUrlList` array.
*
* @description
- * Sets/Gets the blacklist of trusted resource URLs.
+ * Sets/Gets the `bannedResourceUrlList` of trusted resource URLs.
+ *
+ * The **default value** when no trusted resource URL list has been explicitly set is the empty
+ * array (i.e. there is no `bannedResourceUrlList`.)
*/
-
- this.resourceUrlBlacklist = function(value) {
+ this.bannedResourceUrlList = function(value) {
if (arguments.length) {
- resourceUrlBlacklist = adjustMatchers(value);
+ bannedResourceUrlList = adjustMatchers(value);
}
- return resourceUrlBlacklist;
+ return bannedResourceUrlList;
};
- this.$get = ['$injector', function($injector) {
+ /**
+ * @ngdoc method
+ * @name $sceDelegateProvider#resourceUrlBlacklist
+ * @kind function
+ *
+ * @deprecated
+ * sinceVersion="1.8.1"
+ *
+ * This method is deprecated. Use {@link $sceDelegateProvider#bannedResourceUrlList
+ * bannedResourceUrlList} instead.
+ */
+ Object.defineProperty(this, 'resourceUrlBlacklist', {
+ get: function() {
+ return this.bannedResourceUrlList;
+ },
+ set: function(value) {
+ this.bannedResourceUrlList = value;
+ }
+ });
+
+ this.$get = ['$injector', '$$sanitizeUri', function($injector, $$sanitizeUri) {
var htmlSanitizer = function htmlSanitizer(html) {
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
@@ -18393,7 +12403,7 @@ function $SceDelegateProvider() {
function matchUrl(matcher, parsedUrl) {
if (matcher === 'self') {
- return urlIsSameOrigin(parsedUrl);
+ return urlIsSameOrigin(parsedUrl) || urlIsSameOriginAsBaseUrl(parsedUrl);
} else {
// definitely a regex. See adjustMatchers()
return !!matcher.exec(parsedUrl.href);
@@ -18403,17 +12413,17 @@ function $SceDelegateProvider() {
function isResourceUrlAllowedByPolicy(url) {
var parsedUrl = urlResolve(url.toString());
var i, n, allowed = false;
- // Ensure that at least one item from the whitelist allows this url.
- for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) {
- if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) {
+ // Ensure that at least one item from the trusted resource URL list allows this url.
+ for (i = 0, n = trustedResourceUrlList.length; i < n; i++) {
+ if (matchUrl(trustedResourceUrlList[i], parsedUrl)) {
allowed = true;
break;
}
}
if (allowed) {
- // Ensure that no item from the blacklist blocked this url.
- for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) {
- if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) {
+ // Ensure that no item from the banned resource URL list has blocked this url.
+ for (i = 0, n = bannedResourceUrlList.length; i < n; i++) {
+ if (matchUrl(bannedResourceUrlList[i], parsedUrl)) {
allowed = false;
break;
}
@@ -18445,7 +12455,8 @@ function $SceDelegateProvider() {
byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase);
- byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.MEDIA_URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.URL] = generateHolderType(byType[SCE_CONTEXTS.MEDIA_URL]);
byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]);
@@ -18454,17 +12465,24 @@ function $SceDelegateProvider() {
* @name $sceDelegate#trustAs
*
* @description
- * Returns an object that is trusted by angular for use in specified strict
- * contextual escaping contexts (such as ng-bind-html, ng-include, any src
- * attribute interpolation, any dom event binding attribute interpolation
- * such as for onclick, etc.) that uses the provided value.
- * See {@link ng.$sce $sce} for enabling strict contextual escaping.
+ * Returns a trusted representation of the parameter for the specified context. This trusted
+ * object will later on be used as-is, without any security check, by bindings or directives
+ * that require this security context.
+ * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass
+ * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as
+ * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the
+ * sanitizer loaded, passing the value itself will render all the HTML that does not pose a
+ * security risk.
+ *
+ * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those
+ * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual
+ * escaping.
+ *
+ * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`,
+ * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`.
*
- * @param {string} type The kind of context in which this value is safe for use. e.g. url,
- * resourceUrl, html, js and css.
- * @param {*} value The value that that should be considered trusted/safe.
- * @returns {*} A value that can be used to stand in for the provided `value` in places
- * where Angular expects a $sce.trustAs() return value.
+ * @param {*} value The value that should be considered trusted.
+ * @return {*} A trusted representation of value, that can be used in the given context.
*/
function trustAs(type, trustedValue) {
var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
@@ -18496,11 +12514,11 @@ function $SceDelegateProvider() {
* ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}.
*
* If the passed parameter is not a value that had been returned by {@link
- * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is.
+ * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is.
*
* @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}
- * call or anything else.
- * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs
+ * call or anything else.
+ * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs
* `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns
* `value` unchanged.
*/
@@ -18517,33 +12535,56 @@ function $SceDelegateProvider() {
* @name $sceDelegate#getTrusted
*
* @description
- * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and
- * returns the originally supplied value if the queried context type is a supertype of the
- * created type. If this condition isn't satisfied, throws an exception.
+ * Given an object and a security context in which to assign it, returns a value that's safe to
+ * use in this context, which was represented by the parameter. To do so, this function either
+ * unwraps the safe type it has been given (for instance, a {@link ng.$sceDelegate#trustAs
+ * `$sceDelegate.trustAs`} result), or it might try to sanitize the value given, depending on
+ * the context and sanitizer availablility.
+ *
+ * The contexts that can be sanitized are $sce.MEDIA_URL, $sce.URL and $sce.HTML. The first two are available
+ * by default, and the third one relies on the `$sanitize` service (which may be loaded through
+ * the `ngSanitize` module). Furthermore, for $sce.RESOURCE_URL context, a plain string may be
+ * accepted if the resource url policy defined by {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * `$sceDelegateProvider.trustedResourceUrlList`} and {@link ng.$sceDelegateProvider#bannedResourceUrlList
+ * `$sceDelegateProvider.bannedResourceUrlList`} accepts that resource.
+ *
+ * This function will throw if the safe type isn't appropriate for this context, or if the
+ * value given cannot be accepted in the context (which might be caused by sanitization not
+ * being available, or the value not being recognized as safe).
*
* <div class="alert alert-danger">
* Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
* (XSS) vulnerability in your application.
* </div>
*
- * @param {string} type The kind of context in which this value is to be used.
+ * @param {string} type The context in which this value is to be used (such as `$sce.HTML`).
* @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs
- * `$sceDelegate.trustAs`} call.
- * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs
- * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception.
+ * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.)
+ * @return {*} A version of the value that's safe to use in the given context, or throws an
+ * exception if this is impossible.
*/
function getTrusted(type, maybeTrusted) {
if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') {
return maybeTrusted;
}
var constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
+ // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return
+ // as-is.
if (constructor && maybeTrusted instanceof constructor) {
return maybeTrusted.$$unwrapTrustedValue();
}
- // If we get here, then we may only take one of two actions.
- // 1. sanitize the value for the requested type, or
- // 2. throw an exception.
- if (type === SCE_CONTEXTS.RESOURCE_URL) {
+
+ // If maybeTrusted is a trusted class instance but not of the correct trusted type
+ // then unwrap it and allow it to pass through to the rest of the checks
+ if (isFunction(maybeTrusted.$$unwrapTrustedValue)) {
+ maybeTrusted = maybeTrusted.$$unwrapTrustedValue();
+ }
+
+ // If we get here, then we will either sanitize the value or throw an exception.
+ if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) {
+ // we attempt to sanitize non-resource URLs
+ return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL);
+ } else if (type === SCE_CONTEXTS.RESOURCE_URL) {
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
return maybeTrusted;
} else {
@@ -18552,8 +12593,10 @@ function $SceDelegateProvider() {
maybeTrusted.toString());
}
} else if (type === SCE_CONTEXTS.HTML) {
+ // htmlSanitizer throws its own error when no sanitizer is available.
return htmlSanitizer(maybeTrusted);
}
+ // Default error when the $sce service has no way to make the input safe.
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
}
@@ -18567,6 +12610,8 @@ function $SceDelegateProvider() {
/**
* @ngdoc provider
* @name $sceProvider
+ * @this
+ *
* @description
*
* The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service.
@@ -18576,8 +12621,6 @@ function $SceDelegateProvider() {
* Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}.
*/
-/* jshint maxlen: false*/
-
/**
* @ngdoc service
* @name $sce
@@ -18587,23 +12630,29 @@ function $SceDelegateProvider() {
*
* `$sce` is a service that provides Strict Contextual Escaping services to AngularJS.
*
- * # Strict Contextual Escaping
+ * ## Strict Contextual Escaping
+ *
+ * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render
+ * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and
+ * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
+ *
+ * ### Overview
*
- * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain
- * contexts to result in a value that is marked as safe to use for that context. One example of
- * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer
- * to these contexts as privileged or SCE contexts.
+ * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in
+ * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically
+ * run security checks on them (sanitizations, trusted URL resource, depending on context), or throw
+ * when it cannot guarantee the security of the result. That behavior depends strongly on contexts:
+ * HTML can be sanitized, but template URLs cannot, for instance.
*
- * As of version 1.2, Angular ships with SCE enabled by default.
+ * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML:
+ * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it
+ * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and
+ * render the input as-is, you will need to mark it as trusted for that context before attempting
+ * to bind it.
*
- * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow
- * one to execute arbitrary javascript by the use of the expression() syntax. Refer
- * <http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx> to learn more about them.
- * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>`
- * to the top of your HTML document.
+ * As of version 1.2, AngularJS ships with SCE enabled by default.
*
- * SCE assists in writing code in a way that (a) is secure by default and (b) makes auditing for
- * security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
+ * ### In practice
*
* Here's an example of a binding in a privileged context:
*
@@ -18613,10 +12662,10 @@ function $SceDelegateProvider() {
* ```
*
* Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE
- * disabled, this application allows the user to render arbitrary HTML into the DIV.
- * In a more realistic example, one may be rendering user comments, blog articles, etc. via
- * bindings. (HTML is just one example of a context where rendering user controlled input creates
- * security vulnerabilities.)
+ * disabled, this application allows the user to render arbitrary HTML into the DIV, which would
+ * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog
+ * articles, etc. via bindings. (HTML is just one example of a context where rendering user
+ * controlled input creates security vulnerabilities.)
*
* For the case of HTML, you might use a library, either on the client side, or on the server side,
* to sanitize unsafe HTML before binding to the value and rendering it in the document.
@@ -18626,25 +12675,29 @@ function $SceDelegateProvider() {
* ensure that you didn't accidentally delete the line that sanitized the value, or renamed some
* properties/fields and forgot to update the binding to the sanitized value?
*
- * To be secure by default, you want to ensure that any such bindings are disallowed unless you can
- * determine that something explicitly says it's safe to use a value for binding in that
- * context. You can then audit your code (a simple grep would do) to ensure that this is only done
- * for those values that you can easily tell are safe - because they were received from your server,
- * sanitized by your library, etc. You can organize your codebase to help with this - perhaps
- * allowing only the files in a specific directory to do this. Ensuring that the internal API
- * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task.
+ * To be secure by default, AngularJS makes sure bindings go through that sanitization, or
+ * any similar validation process, unless there's a good reason to trust the given value in this
+ * context. That trust is formalized with a function call. This means that as a developer, you
+ * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues,
+ * you just need to ensure the values you mark as trusted indeed are safe - because they were
+ * received from your server, sanitized by your library, etc. You can organize your codebase to
+ * help with this - perhaps allowing only the files in a specific directory to do this.
+ * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then
+ * becomes a more manageable task.
*
* In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs}
* (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to
- * obtain values that will be accepted by SCE / privileged contexts.
+ * build the trusted versions of your values.
*
- *
- * ## How does it work?
+ * ### How does it work?
*
* In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted
- * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link
- * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
- * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals.
+ * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as
+ * a way to enforce the required security context in your data sink. Directives use {@link
+ * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs
+ * the {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. Also,
+ * when binding without directives, AngularJS will understand the context of your bindings
+ * automatically.
*
* As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
* ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly
@@ -18660,16 +12713,16 @@ function $SceDelegateProvider() {
* }];
* ```
*
- * ## Impact on loading templates
+ * ### Impact on loading templates
*
* This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as
* `templateUrl`'s specified by {@link guide/directive directives}.
*
- * By default, Angular only loads templates from the same domain and protocol as the application
+ * By default, AngularJS only loads templates from the same domain and protocol as the application
* document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
* $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or
- * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
- * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
+ * protocols, you may either add them to the {@link ng.$sceDelegateProvider#trustedResourceUrlList
+ * trustedResourceUrlList} or {@link ng.$sce#trustAsResourceUrl wrap them} into trusted values.
*
* *Please note*:
* The browser's
@@ -18680,40 +12733,58 @@ function $SceDelegateProvider() {
* won't work on all browsers. Also, loading templates from `file://` URL does not work on some
* browsers.
*
- * ## This feels like too much overhead
+ * ### This feels like too much overhead
*
* It's important to remember that SCE only applies to interpolation expressions.
*
* If your expressions are constant literals, they're automatically trusted and you don't need to
- * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g.
- * `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works.
- *
- * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them
- * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here.
+ * call `$sce.trustAs` on them (e.g.
+ * `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works (remember to include the
+ * `ngSanitize` module). The `$sceDelegate` will also use the `$sanitize` service if it is available
+ * when binding untrusted values to `$sce.HTML` context.
+ * AngularJS provides an implementation in `angular-sanitize.js`, and if you
+ * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in
+ * your application.
*
* The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load
* templates in `ng-include` from your application's domain without having to even know about SCE.
* It blocks loading templates from other domains or loading templates over http from an https
* served document. You can change these by setting your own custom {@link
- * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link
- * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs.
+ * ng.$sceDelegateProvider#trustedResourceUrlList trusted resource URL list} and {@link
+ * ng.$sceDelegateProvider#bannedResourceUrlList banned resource URL list} for matching such URLs.
*
* This significantly reduces the overhead. It is far easier to pay the small overhead and have an
* application that's secure and can be audited to verify that with much more ease than bolting
* security onto an application later.
*
* <a name="contexts"></a>
- * ## What trusted context types are supported?
+ * ### What trusted context types are supported?
*
* | Context | Notes |
* |---------------------|----------------|
* | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. |
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
- * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. |
- * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
+ * | `$sce.MEDIA_URL` | For URLs that are safe to render as media. Is automatically converted from string by sanitizing when needed. |
+ * | `$sce.URL` | For URLs that are safe to follow as links. Is automatically converted from string by sanitizing when needed. Note that `$sce.URL` makes a stronger statement about the URL than `$sce.MEDIA_URL` does and therefore contexts requiring values trusted for `$sce.URL` can be used anywhere that values trusted for `$sce.MEDIA_URL` are required.|
+ * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` or `$sce.MEDIA_URL` do and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` or `$sce.MEDIA_URL` are required. <br><br> The {@link $sceDelegateProvider#trustedResourceUrlList $sceDelegateProvider#trustedResourceUrlList()} and {@link $sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider#bannedResourceUrlList()} can be used to restrict trusted origins for `RESOURCE_URL` |
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
*
- * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a>
+ *
+ * <div class="alert alert-warning">
+ * Be aware that, before AngularJS 1.7.0, `a[href]` and `img[src]` used to sanitize their
+ * interpolated values directly rather than rely upon {@link ng.$sce#getTrusted `$sce.getTrusted`}.
+ *
+ * **As of 1.7.0, this is no longer the case.**
+ *
+ * Now such interpolations are marked as requiring `$sce.URL` (for `a[href]`) or `$sce.MEDIA_URL`
+ * (for `img[src]`), so that the sanitization happens (via `$sce.getTrusted...`) when the `$interpolate`
+ * service evaluates the expressions.
+ * </div>
+ *
+ * There are no CSS or JS context bindings in AngularJS currently, so their corresponding `$sce.trustAs`
+ * functions aren't useful yet. This might evolve.
+ *
+ * ### Format of items in {@link ng.$sceDelegateProvider#trustedResourceUrlList trustedResourceUrlList}/{@link ng.$sceDelegateProvider#bannedResourceUrlList bannedResourceUrlList} <a name="resourceUrlPatternItem"></a>
*
* Each element in these arrays must be one of the following:
*
@@ -18727,7 +12798,7 @@ function $SceDelegateProvider() {
* match themselves.
* - `*`: matches zero or more occurrences of any character other than one of the following 6
* characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use
- * in a whitelist.
+ * for matching resource URL lists.
* - `**`: matches zero or more occurrences of *any* character. As such, it's not
* appropriate for use in a scheme, domain, etc. as it would match too much. (e.g.
* http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might
@@ -18760,9 +12831,9 @@ function $SceDelegateProvider() {
*
* Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example.
*
- * ## Show me an example using SCE.
+ * ### Show me an example using SCE.
*
- * <example module="mySceApp" deps="angular-sanitize.js">
+ * <example module="mySceApp" deps="angular-sanitize.js" name="sce-service">
* <file name="index.html">
* <div ng-controller="AppController as myCtrl">
* <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br>
@@ -18783,10 +12854,10 @@ function $SceDelegateProvider() {
* <file name="script.js">
* angular.module('mySceApp', ['ngSanitize'])
* .controller('AppController', ['$http', '$templateCache', '$sce',
- * function($http, $templateCache, $sce) {
+ * function AppController($http, $templateCache, $sce) {
* var self = this;
- * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
- * self.userComments = userComments;
+ * $http.get('test_data.json', {cache: $templateCache}).then(function(response) {
+ * self.userComments = response.data;
* });
* self.explicitlyTrustedHtml = $sce.trustAsHtml(
* '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
@@ -18809,12 +12880,12 @@ function $SceDelegateProvider() {
* <file name="protractor.js" type="protractor">
* describe('SCE doc demo', function() {
* it('should sanitize untrusted values', function() {
- * expect(element.all(by.css('.htmlComment')).first().getInnerHtml())
+ * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML'))
* .toBe('<span>Is <i>anyone</i> reading this?</span>');
* });
*
* it('should NOT sanitize explicitly trusted values', function() {
- * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe(
+ * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe(
* '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
* 'sanitization.&quot;">Hover over this text.</span>');
* });
@@ -18830,20 +12901,20 @@ function $SceDelegateProvider() {
* for little coding overhead. It will be much harder to take an SCE disabled application and
* either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE
* for cases where you have a lot of existing code that was written before SCE was introduced and
- * you're migrating them a module at a time.
+ * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if
+ * you are writing a library, you will cause security bugs applications using it.
*
* That said, here's how you can completely disable SCE:
*
* ```
* angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
* // Completely disable SCE. For demonstration purposes only!
- * // Do not use in new projects.
+ * // Do not use in new projects or libraries.
* $sceProvider.enabled(false);
* });
* ```
*
*/
-/* jshint maxlen: 100 */
function $SceProvider() {
var enabled = true;
@@ -18853,8 +12924,8 @@ function $SceProvider() {
* @name $sceProvider#enabled
* @kind function
*
- * @param {boolean=} value If provided, then enables/disables SCE.
- * @return {boolean} true if SCE is enabled, false otherwise.
+ * @param {boolean=} value If provided, then enables/disables SCE application-wide.
+ * @return {boolean} True if SCE is enabled, false otherwise.
*
* @description
* Enables/disables SCE and returns the current value.
@@ -18885,7 +12956,7 @@ function $SceProvider() {
* such a value.
*
* - getTrusted(contextEnum, value)
- * This function should return the a value that is safe to use in the context specified by
+ * This function should return the value that is safe to use in the context specified by
* contextEnum or throw and exception otherwise.
*
* NOTE: This contract deliberately does NOT state that values returned by trustAs() must be
@@ -18908,13 +12979,14 @@ function $SceProvider() {
* getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value)
* will also succeed.
*
- * Inheritance happens to capture this in a natural way. In some future, we
- * may not use inheritance anymore. That is OK because no code outside of
- * sce.js and sceSpecs.js would need to be aware of this detail.
+ * Inheritance happens to capture this in a natural way. In some future, we may not use
+ * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to
+ * be aware of this detail.
*/
this.$get = ['$parse', '$sceDelegate', function(
$parse, $sceDelegate) {
+ // Support: IE 9-11 only
// Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow
// the "expression(javascript expression)" syntax which is insecure.
if (enabled && msie < 8) {
@@ -18931,8 +13003,8 @@ function $SceProvider() {
* @name $sce#isEnabled
* @kind function
*
- * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you
- * have to do it at module config time on {@link ng.$sceProvider $sceProvider}.
+ * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you
+ * have to do it at module config time on {@link ng.$sceProvider $sceProvider}.
*
* @description
* Returns a boolean indicating if SCE is enabled.
@@ -18954,19 +13026,19 @@ function $SceProvider() {
* @name $sce#parseAs
*
* @description
- * Converts Angular {@link guide/expression expression} into a function. This is like {@link
+ * Converts AngularJS {@link guide/expression expression} into a function. This is like {@link
* ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it
* wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*,
* *result*)}
*
- * @param {string} type The kind of SCE context in which this result will be used.
+ * @param {string} type The SCE context in which this result will be used.
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
sce.parseAs = function sceParseAs(type, expr) {
var parsed = $parse(expr);
@@ -18984,18 +13056,18 @@ function $SceProvider() {
* @name $sce#trustAs
*
* @description
- * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such,
- * returns an object that is trusted by angular for use in specified strict contextual
- * escaping contexts (such as ng-bind-html, ng-include, any src attribute
- * interpolation, any dom event binding attribute interpolation such as for onclick, etc.)
- * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual
- * escaping.
+ * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a
+ * wrapped object that represents your value, and the trust you have in its safety for the given
+ * context. AngularJS can then use that value as-is in bindings of the specified secure context.
+ * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute
+ * interpolations. See {@link ng.$sce $sce} for strict contextual escaping.
+ *
+ * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`,
+ * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`.
*
- * @param {string} type The kind of context in which this value is safe for use. e.g. url,
- * resourceUrl, html, js and css.
- * @param {*} value The value that that should be considered trusted/safe.
- * @returns {*} A value that can be used to stand in for the provided `value` in places
- * where Angular expects a $sce.trustAs() return value.
+ * @param {*} value The value that that should be considered trusted.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in the context you specified.
*/
/**
@@ -19006,11 +13078,23 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsHtml(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml
- * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.HTML` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.HTML` context (like `ng-bind-html`).
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAsCss
+ *
+ * @description
+ * Shorthand method. `$sce.trustAsCss(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`}
+ *
+ * @param {*} value The value to mark as trusted for `$sce.CSS` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant
+ * of your `value` in `$sce.CSS` context. This context is currently unused, so there are
+ * almost no reasons to use this function so far.
*/
/**
@@ -19021,11 +13105,10 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsUrl(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl
- * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.URL` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.URL` context. That context is currently unused, so there are almost no reasons
+ * to use this function so far.
*/
/**
@@ -19036,11 +13119,10 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsResourceUrl(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl
- * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the return
- * value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute
+ * bindings, ...)
*/
/**
@@ -19051,11 +13133,10 @@ function $SceProvider() {
* Shorthand method. `$sce.trustAsJs(value)` →
* {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`}
*
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs
- * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
+ * @param {*} value The value to mark as trusted for `$sce.JS` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to
+ * use this function so far.
*/
/**
@@ -19064,16 +13145,17 @@ function $SceProvider() {
*
* @description
* Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such,
- * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the
- * originally supplied value if the queried context type is a supertype of the created type.
- * If this condition isn't satisfied, throws an exception.
+ * takes any input, and either returns a value that's safe to use in the specified context,
+ * or throws an exception. This function is aware of trusted values created by the `trustAs`
+ * function and its shorthands, and when contexts are appropriate, returns the unwrapped value
+ * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a
+ * safe value (e.g., no sanitization is available or possible.)
*
- * @param {string} type The kind of context in which this value is to be used.
- * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`}
- * call.
- * @returns {*} The value the was originally provided to
- * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context.
- * Otherwise, throws an exception.
+ * @param {string} type The context in which this value is to be used.
+ * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs
+ * `$sce.trustAs`} call, or anything else (which will not be considered trusted.)
+ * @return {*} A version of the value that's safe to use in the given context, or throws an
+ * exception if this is impossible.
*/
/**
@@ -19085,7 +13167,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)`
*/
/**
@@ -19097,7 +13179,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)`
*/
/**
@@ -19109,7 +13191,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.URL, value)`
*/
/**
@@ -19121,7 +13203,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`}
*
* @param {*} value The value to pass to `$sceDelegate.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)`
*/
/**
@@ -19133,7 +13215,7 @@ function $SceProvider() {
* {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)`
+ * @return {*} The return value of `$sce.getTrusted($sce.JS, value)`
*/
/**
@@ -19145,12 +13227,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -19162,12 +13244,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -19179,12 +13261,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -19196,12 +13278,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
/**
@@ -19213,12 +13295,12 @@ function $SceProvider() {
* {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`}
*
* @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
+ * @return {function(context, locals)} A function which represents the compiled expression:
*
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
*/
// Shorthand delegations.
@@ -19228,13 +13310,13 @@ function $SceProvider() {
forEach(SCE_CONTEXTS, function(enumValue, name) {
var lName = lowercase(name);
- sce[camelCase("parse_as_" + lName)] = function(expr) {
+ sce[snakeToCamel('parse_as_' + lName)] = function(expr) {
return parse(enumValue, expr);
};
- sce[camelCase("get_trusted_" + lName)] = function(value) {
+ sce[snakeToCamel('get_trusted_' + lName)] = function(value) {
return getTrusted(enumValue, value);
};
- sce[camelCase("trust_as_" + lName)] = function(value) {
+ sce[snakeToCamel('trust_as_' + lName)] = function(value) {
return trustAs(enumValue, value);
};
});
@@ -19243,12 +13325,15 @@ function $SceProvider() {
}];
}
+/* exported $SnifferProvider */
+
/**
* !!! This is an undocumented "private" service !!!
*
* @name $sniffer
* @requires $window
* @requires $document
+ * @this
*
* @property {boolean} history Does the browser support html5 history api ?
* @property {boolean} transitions Does the browser support CSS transition events ?
@@ -19260,41 +13345,32 @@ function $SceProvider() {
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
- // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by
- // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index)
- isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime,
+ // Chrome Packaged Apps are not allowed to access `history.pushState`.
+ // If not sandboxed, they can be detected by the presence of `chrome.app.runtime`
+ // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by
+ // the presence of an extension runtime ID and the absence of other Chrome runtime APIs
+ // (see https://developer.chrome.com/apps/manifest/sandbox).
+ // (NW.js apps have access to Chrome APIs, but do support `history`.)
+ isNw = $window.nw && $window.nw.process,
+ isChromePackagedApp =
+ !isNw &&
+ $window.chrome &&
+ ($window.chrome.app && $window.chrome.app.runtime ||
+ !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id),
hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
android =
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
document = $document[0] || {},
- vendorPrefix,
- vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/,
bodyStyle = document.body && document.body.style,
transitions = false,
- animations = false,
- match;
+ animations = false;
if (bodyStyle) {
- for (var prop in bodyStyle) {
- if (match = vendorRegex.exec(prop)) {
- vendorPrefix = match[0];
- vendorPrefix = vendorPrefix[0].toUpperCase() + vendorPrefix.substr(1);
- break;
- }
- }
-
- if (!vendorPrefix) {
- vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit';
- }
-
- transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle));
- animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
-
- if (android && (!transitions || !animations)) {
- transitions = isString(bodyStyle.webkitTransition);
- animations = isString(bodyStyle.webkitAnimation);
- }
+ // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x
+ // Mentioned browsers need a -webkit- prefix for transitions & animations.
+ transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle);
+ animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle);
}
@@ -19307,16 +13383,15 @@ function $SnifferProvider() {
// older webkit browser (533.9) on Boxee box has exactly the same problem as Android has
// 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: !!(hasHistoryPushState && !(android < 4) && !boxee),
- // jshint +W018
hasEvent: function(event) {
+ // Support: IE 9-11 only
// 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.
// IE10+ implements 'input' event but it erroneously fires under various situations,
// e.g. when placeholder changes, or a form is focused.
- if (event === 'input' && msie <= 11) return false;
+ if (event === 'input' && msie) return false;
if (isUndefined(eventSupport[event])) {
var divElm = document.createElement('div');
@@ -19326,7 +13401,6 @@ function $SnifferProvider() {
return eventSupport[event];
},
csp: csp(),
- vendorPrefix: vendorPrefix,
transitions: transitions,
animations: animations,
android: android
@@ -19334,11 +13408,13 @@ function $SnifferProvider() {
}];
}
-var $templateRequestMinErr = minErr('$compile');
+var $templateRequestMinErr = minErr('$templateRequest');
/**
* @ngdoc provider
* @name $templateRequestProvider
+ * @this
+ *
* @description
* Used to configure the options passed to the {@link $http} service when making a template request.
*
@@ -19385,6 +13461,12 @@ function $TemplateRequestProvider() {
* If you want to pass custom options to the `$http` service, such as setting the Accept header you
* can configure this via {@link $templateRequestProvider#httpOptions}.
*
+ * `$templateRequest` is used internally by {@link $compile}, {@link ngRoute.$route}, and directives such
+ * as {@link ngInclude} to download and cache templates.
+ *
+ * 3rd party modules should use `$templateRequest` if their services or directives are loading
+ * templates.
+ *
* @param {string|TrustedResourceUrl} tpl The HTTP request template URL
* @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
*
@@ -19392,57 +13474,63 @@ function $TemplateRequestProvider() {
*
* @property {number} totalPendingRequests total amount of pending template requests being downloaded.
*/
- this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) {
+ this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce',
+ function($exceptionHandler, $templateCache, $http, $q, $sce) {
- function handleRequestFn(tpl, ignoreRequestError) {
- handleRequestFn.totalPendingRequests++;
+ function handleRequestFn(tpl, ignoreRequestError) {
+ handleRequestFn.totalPendingRequests++;
- // We consider the template cache holds only trusted templates, so
- // there's no need to go through whitelisting again for keys that already
- // are included in there. This also makes Angular accept any script
- // directive, no matter its name. However, we still need to unwrap trusted
- // types.
- if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
- tpl = $sce.getTrustedResourceUrl(tpl);
- }
+ // We consider the template cache holds only trusted templates, so
+ // there's no need to go through adding the template again to the trusted
+ // resources for keys that already are included in there. This also makes
+ // AngularJS accept any script directive, no matter its name. However, we
+ // still need to unwrap trusted types.
+ if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
+ tpl = $sce.getTrustedResourceUrl(tpl);
+ }
- var transformResponse = $http.defaults && $http.defaults.transformResponse;
+ var transformResponse = $http.defaults && $http.defaults.transformResponse;
- if (isArray(transformResponse)) {
- transformResponse = transformResponse.filter(function(transformer) {
- return transformer !== defaultHttpResponseTransform;
- });
- } else if (transformResponse === defaultHttpResponseTransform) {
- transformResponse = null;
- }
+ if (isArray(transformResponse)) {
+ transformResponse = transformResponse.filter(function(transformer) {
+ return transformer !== defaultHttpResponseTransform;
+ });
+ } else if (transformResponse === defaultHttpResponseTransform) {
+ transformResponse = null;
+ }
+
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
+ .finally(function() {
+ handleRequestFn.totalPendingRequests--;
+ })
+ .then(function(response) {
+ return $templateCache.put(tpl, response.data);
+ }, handleError);
+
+ function handleError(resp) {
+ if (!ignoreRequestError) {
+ resp = $templateRequestMinErr('tpload',
+ 'Failed to load template: {0} (HTTP status: {1} {2})',
+ tpl, resp.status, resp.statusText);
+
+ $exceptionHandler(resp);
+ }
- return $http.get(tpl, extend({
- cache: $templateCache,
- transformResponse: transformResponse
- }, httpOptions))
- ['finally'](function() {
- handleRequestFn.totalPendingRequests--;
- })
- .then(function(response) {
- $templateCache.put(tpl, response.data);
- return response.data;
- }, handleError);
-
- function handleError(resp) {
- if (!ignoreRequestError) {
- throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
- tpl, resp.status, resp.statusText);
+ return $q.reject(resp);
}
- return $q.reject(resp);
}
- }
- handleRequestFn.totalPendingRequests = 0;
+ handleRequestFn.totalPendingRequests = 0;
- return handleRequestFn;
- }];
+ return handleRequestFn;
+ }
+ ];
}
+/** @this */
function $$TestabilityProvider() {
this.$get = ['$rootScope', '$browser', '$location',
function($rootScope, $browser, $location) {
@@ -19481,7 +13569,7 @@ function $$TestabilityProvider() {
matches.push(binding);
}
} else {
- if (bindingName.indexOf(expression) != -1) {
+ if (bindingName.indexOf(expression) !== -1) {
matches.push(binding);
}
}
@@ -19546,7 +13634,15 @@ function $$TestabilityProvider() {
* @name $$testability#whenStable
*
* @description
- * Calls the callback when $timeout and $http requests are completed.
+ * Calls the callback when all pending tasks are completed.
+ *
+ * Types of tasks waited for include:
+ * - Pending timeouts (via {@link $timeout}).
+ * - Pending HTTP requests (via {@link $http}).
+ * - In-progress route transitions (via {@link $route}).
+ * - Pending tasks scheduled via {@link $rootScope#$applyAsync}.
+ * - Pending tasks scheduled via {@link $rootScope#$evalAsync}.
+ * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
*
* @param {function} callback
*/
@@ -19558,6 +13654,9 @@ function $$TestabilityProvider() {
}];
}
+var $timeoutMinErr = minErr('$timeout');
+
+/** @this */
function $TimeoutProvider() {
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
function($rootScope, $browser, $q, $$q, $exceptionHandler) {
@@ -19565,35 +13664,35 @@ function $TimeoutProvider() {
var deferreds = {};
- /**
- * @ngdoc service
- * @name $timeout
- *
- * @description
- * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
- * block and delegates any exceptions to
- * {@link ng.$exceptionHandler $exceptionHandler} service.
- *
- * The return value of calling `$timeout` is a promise, which will be resolved when
- * the delay has passed and the timeout function, if provided, is executed.
- *
- * To cancel a timeout request, call `$timeout.cancel(promise)`.
- *
- * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
- * synchronously flush the queue of deferred functions.
- *
- * If you only want a promise that will be resolved after some specified delay
- * then you can call `$timeout` without the `fn` function.
- *
- * @param {function()=} fn A function, whose execution should be delayed.
- * @param {number=} [delay=0] Delay in milliseconds.
- * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
- * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @param {...*=} Pass additional parameters to the executed function.
- * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
- * will be resolved with the return value of the `fn` function.
- *
- */
+ /**
+ * @ngdoc service
+ * @name $timeout
+ *
+ * @description
+ * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
+ * block and delegates any exceptions to
+ * {@link ng.$exceptionHandler $exceptionHandler} service.
+ *
+ * The return value of calling `$timeout` is a promise, which will be resolved when
+ * the delay has passed and the timeout function, if provided, is executed.
+ *
+ * To cancel a timeout request, call `$timeout.cancel(promise)`.
+ *
+ * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
+ * synchronously flush the queue of deferred functions.
+ *
+ * If you only want a promise that will be resolved after some specified delay
+ * then you can call `$timeout` without the `fn` function.
+ *
+ * @param {function()=} fn A function, whose execution should be delayed.
+ * @param {number=} [delay=0] Delay in milliseconds.
+ * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
+ * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @param {...*=} Pass additional parameters to the executed function.
+ * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
+ * will be resolved with the return value of the `fn` function.
+ *
+ */
function timeout(fn, delay, invokeApply) {
if (!isFunction(fn)) {
invokeApply = delay;
@@ -19613,13 +13712,12 @@ function $TimeoutProvider() {
} catch (e) {
deferred.reject(e);
$exceptionHandler(e);
- }
- finally {
+ } finally {
delete deferreds[promise.$$timeoutId];
}
if (!skipApply) $rootScope.$apply();
- }, delay);
+ }, delay, '$timeout');
promise.$$timeoutId = timeoutId;
deferreds[timeoutId] = deferred;
@@ -19628,25 +13726,37 @@ function $TimeoutProvider() {
}
- /**
- * @ngdoc method
- * @name $timeout#cancel
- *
- * @description
- * Cancels a task associated with the `promise`. As a result of this, the promise will be
- * resolved with a rejection.
- *
- * @param {Promise=} promise Promise returned by the `$timeout` function.
- * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
- * canceled.
- */
+ /**
+ * @ngdoc method
+ * @name $timeout#cancel
+ *
+ * @description
+ * Cancels a task associated with the `promise`. As a result of this, the promise will be
+ * resolved with a rejection.
+ *
+ * @param {Promise=} promise Promise returned by the `$timeout` function.
+ * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
+ * canceled.
+ */
timeout.cancel = function(promise) {
- if (promise && promise.$$timeoutId in deferreds) {
- deferreds[promise.$$timeoutId].reject('canceled');
- delete deferreds[promise.$$timeoutId];
- return $browser.defer.cancel(promise.$$timeoutId);
+ if (!promise) return false;
+
+ if (!promise.hasOwnProperty('$$timeoutId')) {
+ throw $timeoutMinErr('badprom',
+ '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.');
}
- return false;
+
+ if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false;
+
+ var id = promise.$$timeoutId;
+ var deferred = deferreds[id];
+
+ // Timeout cancels should not report an unhandled promise.
+ markQExceptionHandled(deferred.promise);
+ deferred.reject('canceled');
+ delete deferreds[id];
+
+ return $browser.defer.cancel(id);
};
return timeout;
@@ -19660,9 +13770,16 @@ function $TimeoutProvider() {
// doesn't know about mocked locations and resolves URLs to the real document - which is
// exactly the behavior needed here. There is little value is mocking these out for this
// service.
-var urlParsingNode = window.document.createElement("a");
+var urlParsingNode = window.document.createElement('a');
var originUrl = urlResolve(window.location.href);
+var baseUrlParsingNode;
+
+urlParsingNode.href = 'http://[::1]';
+// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview)
+// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets
+// when parsed out of an anchor element.
+var ipv6InBrackets = urlParsingNode.hostname === '[::1]';
/**
*
@@ -19673,7 +13790,7 @@ var originUrl = urlResolve(window.location.href);
* URL will be resolved into an absolute URL in the context of the application document.
* Parsing means that the anchor node's host, hostname, protocol, port, pathname and related
* properties are all populated to reflect the normalized URL. This approach has wide
- * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See
+ * compatibility - Safari 1+, Mozilla 1+ etc. See
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
*
* Implementation Notes for IE
@@ -19693,42 +13810,51 @@ var originUrl = urlResolve(window.location.href);
* http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
*
* @kind function
- * @param {string} url The URL to be parsed.
+ * @param {string|object} url The URL to be parsed. If `url` is not a string, it will be returned
+ * unchanged.
* @description Normalizes and parses a URL.
* @returns {object} Returns the normalized URL as a dictionary.
*
- * | member name | Description |
- * |---------------|----------------|
+ * | member name | Description |
+ * |---------------|------------------------------------------------------------------------|
* | href | A normalized version of the provided URL if it was not an absolute URL |
- * | protocol | The protocol including the trailing colon |
+ * | protocol | The protocol without the trailing colon |
* | host | The host and port (if the port is non-default) of the normalizedUrl |
* | search | The search params, minus the question mark |
- * | hash | The hash string, minus the hash symbol
- * | hostname | The hostname
- * | port | The port, without ":"
- * | pathname | The pathname, beginning with "/"
+ * | hash | The hash string, minus the hash symbol |
+ * | hostname | The hostname |
+ * | port | The port, without ":" |
+ * | pathname | The pathname, beginning with "/" |
*
*/
function urlResolve(url) {
+ if (!isString(url)) return url;
+
var href = url;
+ // Support: IE 9-11 only
if (msie) {
// Normalize before parse. Refer Implementation Notes on why this is
// done in two steps on IE.
- urlParsingNode.setAttribute("href", href);
+ urlParsingNode.setAttribute('href', href);
href = urlParsingNode.href;
}
urlParsingNode.setAttribute('href', href);
- // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
+ var hostname = urlParsingNode.hostname;
+
+ if (!ipv6InBrackets && hostname.indexOf(':') > -1) {
+ hostname = '[' + hostname + ']';
+ }
+
return {
href: urlParsingNode.href,
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
host: urlParsingNode.host,
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
- hostname: urlParsingNode.hostname,
+ hostname: hostname,
port: urlParsingNode.port,
pathname: (urlParsingNode.pathname.charAt(0) === '/')
? urlParsingNode.pathname
@@ -19737,26 +13863,107 @@ function urlResolve(url) {
}
/**
- * Parse a request URL and determine whether this is a same-origin request as the application document.
+ * Parse a request URL and determine whether this is a same-origin request as the application
+ * document.
*
* @param {string|object} requestUrl The url of the request as a string that will be resolved
* or a parsed URL object.
* @returns {boolean} Whether the request is for the same origin as the application document.
*/
function urlIsSameOrigin(requestUrl) {
- var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
- return (parsed.protocol === originUrl.protocol &&
- parsed.host === originUrl.host);
+ return urlsAreSameOrigin(requestUrl, originUrl);
+}
+
+/**
+ * Parse a request URL and determine whether it is same-origin as the current document base URL.
+ *
+ * Note: The base URL is usually the same as the document location (`location.href`) but can
+ * be overriden by using the `<base>` tag.
+ *
+ * @param {string|object} requestUrl The url of the request as a string that will be resolved
+ * or a parsed URL object.
+ * @returns {boolean} Whether the URL is same-origin as the document base URL.
+ */
+function urlIsSameOriginAsBaseUrl(requestUrl) {
+ return urlsAreSameOrigin(requestUrl, getBaseUrl());
+}
+
+/**
+ * Create a function that can check a URL's origin against a list of allowed/trusted origins.
+ * The current location's origin is implicitly trusted.
+ *
+ * @param {string[]} trustedOriginUrls - A list of URLs (strings), whose origins are trusted.
+ *
+ * @returns {Function} - A function that receives a URL (string or parsed URL object) and returns
+ * whether it is of an allowed origin.
+ */
+function urlIsAllowedOriginFactory(trustedOriginUrls) {
+ var parsedAllowedOriginUrls = [originUrl].concat(trustedOriginUrls.map(urlResolve));
+
+ /**
+ * Check whether the specified URL (string or parsed URL object) has an origin that is allowed
+ * based on a list of trusted-origin URLs. The current location's origin is implicitly
+ * trusted.
+ *
+ * @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be
+ * resolved or a parsed URL object).
+ *
+ * @returns {boolean} - Whether the specified URL is of an allowed origin.
+ */
+ return function urlIsAllowedOrigin(requestUrl) {
+ var parsedUrl = urlResolve(requestUrl);
+ return parsedAllowedOriginUrls.some(urlsAreSameOrigin.bind(null, parsedUrl));
+ };
+}
+
+/**
+ * Determine if two URLs share the same origin.
+ *
+ * @param {string|Object} url1 - First URL to compare as a string or a normalized URL in the form of
+ * a dictionary object returned by `urlResolve()`.
+ * @param {string|object} url2 - Second URL to compare as a string or a normalized URL in the form
+ * of a dictionary object returned by `urlResolve()`.
+ *
+ * @returns {boolean} - True if both URLs have the same origin, and false otherwise.
+ */
+function urlsAreSameOrigin(url1, url2) {
+ url1 = urlResolve(url1);
+ url2 = urlResolve(url2);
+
+ return (url1.protocol === url2.protocol &&
+ url1.host === url2.host);
+}
+
+/**
+ * Returns the current document base URL.
+ * @returns {string}
+ */
+function getBaseUrl() {
+ if (window.document.baseURI) {
+ return window.document.baseURI;
+ }
+
+ // `document.baseURI` is available everywhere except IE
+ if (!baseUrlParsingNode) {
+ baseUrlParsingNode = window.document.createElement('a');
+ baseUrlParsingNode.href = '.';
+
+ // Work-around for IE bug described in Implementation Notes. The fix in `urlResolve()` is not
+ // suitable here because we need to track changes to the base URL.
+ baseUrlParsingNode = baseUrlParsingNode.cloneNode(false);
+ }
+ return baseUrlParsingNode.href;
}
/**
* @ngdoc service
* @name $window
+ * @this
*
* @description
* A reference to the browser's `window` object. While `window`
* is globally available in JavaScript, it causes testability problems, because
- * it is a global variable. In angular we always refer to it through the
+ * it is a global variable. In AngularJS we always refer to it through the
* `$window` service, so it may be overridden, removed or mocked for testing.
*
* Expressions, like the one defined for the `ngClick` directive in the example
@@ -19765,7 +13972,7 @@ function urlIsSameOrigin(requestUrl) {
* expression.
*
* @example
- <example module="windowExample">
+ <example module="windowExample" name="window-service">
<file name="index.html">
<script>
angular.module('windowExample', [])
@@ -19808,6 +14015,14 @@ function $$CookieReader($document) {
var lastCookies = {};
var lastCookieString = '';
+ function safeGetCookie(rawDocument) {
+ try {
+ return rawDocument.cookie || '';
+ } catch (e) {
+ return '';
+ }
+ }
+
function safeDecodeURIComponent(str) {
try {
return decodeURIComponent(str);
@@ -19818,7 +14033,7 @@ function $$CookieReader($document) {
return function() {
var cookieArray, cookie, i, index, name;
- var currentCookieString = rawDocument.cookie || '';
+ var currentCookieString = safeGetCookie(rawDocument);
if (currentCookieString !== lastCookieString) {
lastCookieString = currentCookieString;
@@ -19845,6 +14060,7 @@ function $$CookieReader($document) {
$$CookieReader.$inject = ['$document'];
+/** @this */
function $$CookieReaderProvider() {
this.$get = $$CookieReader;
}
@@ -19870,7 +14086,7 @@ function $$CookieReaderProvider() {
* annotated with dependencies and is responsible for creating a filter function.
*
* <div class="alert alert-warning">
- * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
* Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
@@ -19913,8 +14129,8 @@ function $$CookieReaderProvider() {
* ```
*
*
- * For more information about how angular filters work, and how to create your own filters, see
- * {@link guide/filter Filters} in the Angular Developer Guide.
+ * For more information about how AngularJS filters work, and how to create your own filters, see
+ * {@link guide/filter Filters} in the AngularJS Developer Guide.
*/
/**
@@ -19924,9 +14140,15 @@ function $$CookieReaderProvider() {
* @description
* Filters are used for formatting data displayed to the user.
*
+ * They can be used in view templates, controllers or services. AngularJS comes
+ * with a collection of [built-in filters](api/ng/filter), but it is easy to
+ * define your own as well.
+ *
* The general syntax in templates is as follows:
*
- * {{ expression [| filter_name[:parameter_value] ... ] }}
+ * ```html
+ * {{ expression [| filter_name[:parameter_value] ... ] }}
+ * ```
*
* @param {String} name Name of the filter function to retrieve
* @return {Function} the filter function
@@ -19949,6 +14171,7 @@ function $$CookieReaderProvider() {
</example>
*/
$FilterProvider.$inject = ['$provide'];
+/** @this */
function $FilterProvider($provide) {
var suffix = 'Filter';
@@ -19959,7 +14182,7 @@ function $FilterProvider($provide) {
* the keys are the filter names and the values are the filter factories.
*
* <div class="alert alert-warning">
- * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
* Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
@@ -19998,7 +14221,7 @@ function $FilterProvider($provide) {
lowercaseFilter: false,
numberFilter: false,
orderByFilter: false,
- uppercaseFilter: false,
+ uppercaseFilter: false
*/
register('currency', currencyFilter);
@@ -20021,6 +14244,9 @@ function $FilterProvider($provide) {
* Selects a subset of items from `array` and returns it as a new array.
*
* @param {Array} array The source array.
+ * <div class="alert alert-info">
+ * **Note**: If the array contains objects that reference themselves, filtering is not possible.
+ * </div>
* @param {string|Object|function()} expression The predicate to be used for selecting items from
* `array`.
*
@@ -20053,9 +14279,10 @@ function $FilterProvider($provide) {
*
* The final result is an array of those elements that the predicate returned true for.
*
- * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in
- * determining if the expected value (from the filter expression) and actual value (from
- * the object in the array) should be considered a match.
+ * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in
+ * determining if values retrieved using `expression` (when it is not a function) should be
+ * considered a match based on the expected value (from the filter expression) and actual
+ * value (from the object in the array).
*
* Can be one of:
*
@@ -20066,17 +14293,18 @@ function $FilterProvider($provide) {
* - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`.
* This is essentially strict comparison of expected and actual.
*
- * - `false|undefined`: A short hand for a function which will look for a substring match in case
- * insensitive way.
+ * - `false`: A short hand for a function which will look for a substring match in a case
+ * insensitive way. Primitive values are converted to strings. Objects are not compared against
+ * primitives, unless they have a custom `toString` method (e.g. `Date` objects).
*
- * Primitive values are converted to strings. Objects are not compared against primitives,
- * unless they have a custom `toString` method (e.g. `Date` objects).
*
- * @param {string=} anyPropertyKey The special property name that matches against any property.
+ * Defaults to `false`.
+ *
+ * @param {string} [anyPropertyKey] The special property name that matches against any property.
* By default `$`.
*
* @example
- <example>
+ <example name="filter-filter">
<file name="index.html">
<div ng-init="friends = [{name:'John', phone:'555-1276'},
{name:'Mary', phone:'800-BIG-MARY'},
@@ -20168,9 +14396,8 @@ function filterFilter() {
case 'number':
case 'string':
matchAgainstAnyProp = true;
- //jshint -W086
+ // falls through
case 'object':
- //jshint +W086
predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp);
break;
default:
@@ -20238,7 +14465,10 @@ function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstA
var key;
if (matchAgainstAnyProp) {
for (key in actual) {
- if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
+ // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined
+ // See: https://github.com/angular/angular.js/issues/15644
+ if (key.charAt && (key.charAt(0) !== '$') &&
+ deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
return true;
}
}
@@ -20260,7 +14490,6 @@ function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstA
} else {
return comparator(actual, expected);
}
- break;
case 'function':
return false;
default:
@@ -20293,7 +14522,7 @@ var ZERO_CHAR = '0';
*
*
* @example
- <example module="currencyExample">
+ <example module="currencyExample" name="currency-filter">
<file name="index.html">
<script>
angular.module('currencyExample', [])
@@ -20304,7 +14533,7 @@ var ZERO_CHAR = '0';
<div ng-controller="ExampleController">
<input type="number" ng-model="amount" aria-label="amount"> <br>
default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br>
- custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span>
+ custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span><br>
no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span>
</div>
</file>
@@ -20315,7 +14544,7 @@ var ZERO_CHAR = '0';
expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235');
});
it('should update', function() {
- if (browser.params.browser == 'safari') {
+ if (browser.params.browser === 'safari') {
// Safari does not understand the minus key. See
// https://github.com/angular/protractor/issues/481
return;
@@ -20341,11 +14570,14 @@ function currencyFilter($locale) {
fractionSize = formats.PATTERNS[1].maxFrac;
}
+ // If the currency symbol is empty, trim whitespace around the symbol
+ var currencySymbolRe = !currencySymbol ? /\s*\u00A4\s*/g : /\u00A4/g;
+
// if null or undefined pass it through
return (amount == null)
? amount
: formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize).
- replace(/\u00A4/g, currencySymbol);
+ replace(currencySymbolRe, currencySymbol);
};
}
@@ -20371,7 +14603,7 @@ function currencyFilter($locale) {
* include "," group separators after each third digit).
*
* @example
- <example module="numberFilterExample">
+ <example module="numberFilterExample" name="number-filter">
<file name="index.html">
<script>
angular.module('numberFilterExample', [])
@@ -20450,16 +14682,16 @@ function parse(numStr) {
}
// Count the number of leading zeros.
- for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */}
+ for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ }
- if (i == (zeros = numStr.length)) {
+ if (i === (zeros = numStr.length)) {
// The digits are all zero.
digits = [0];
numberOfIntegerDigits = 1;
} else {
// Count the number of trailing zeros
zeros--;
- while (numStr.charAt(zeros) == ZERO_CHAR) zeros--;
+ while (numStr.charAt(zeros) === ZERO_CHAR) zeros--;
// Trailing zeros are insignificant so ignore them
numberOfIntegerDigits -= i;
@@ -20651,7 +14883,7 @@ function dateGetter(name, size, offset, trim, negWrap) {
if (offset > 0 || value > -offset) {
value += offset;
}
- if (value === 0 && offset == -12) value = 12;
+ if (value === 0 && offset === -12) value = 12;
return padNumber(value, size, trim, negWrap);
};
}
@@ -20668,7 +14900,7 @@ function dateStrGetter(name, shortForm, standAlone) {
function timeZoneGetter(date, formats, offset) {
var zone = -1 * offset;
- var paddedZone = (zone >= 0) ? "+" : "";
+ var paddedZone = (zone >= 0) ? '+' : '';
paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
padNumber(Math.abs(zone % 60), 2);
@@ -20748,8 +14980,8 @@ var DATE_FORMATS = {
GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
- NUMBER_STRING = /^\-?\d+$/;
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,
+ NUMBER_STRING = /^-?\d+$/;
/**
* @ngdoc filter
@@ -20807,6 +15039,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+
* `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence
* (e.g. `"h 'o''clock'"`).
*
+ * Any other characters in the `format` string will be output as-is.
+ *
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
* number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its
* shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is
@@ -20820,7 +15054,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
- <example>
+ <example name="filter-date">
<file name="index.html">
<span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>:
<span>{{1288323623006 | date:'medium'}}</span><br>
@@ -20836,7 +15070,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+
expect(element(by.binding("1288323623006 | date:'medium'")).getText()).
toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()).
- toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/);
+ toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/);
expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
@@ -20853,7 +15087,7 @@ function dateFilter($locale) {
// 1 2 3 4 5 6 7 8 9 10 11
function jsonStringToDate(string) {
var match;
- if (match = string.match(R_ISO8601_STR)) {
+ if ((match = string.match(R_ISO8601_STR))) {
var date = new Date(0),
tzHour = 0,
tzMin = 0,
@@ -20914,7 +15148,7 @@ function dateFilter($locale) {
forEach(parts, function(value) {
fn = DATE_FORMATS[value];
text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
- : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
+ : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
});
return text;
@@ -20939,15 +15173,15 @@ function dateFilter($locale) {
*
*
* @example
- <example>
+ <example name="filter-json">
<file name="index.html">
<pre id="default-spacing">{{ {'name':'value'} | json }}</pre>
<pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre>
</file>
<file name="protractor.js" type="protractor">
it('should jsonify filtered objects', function() {
- expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
- expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
+ expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/);
+ expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/);
});
</file>
</example>
@@ -20969,6 +15203,9 @@ function jsonFilter() {
* @kind function
* @description
* Converts string to lowercase.
+ *
+ * See the {@link ng.uppercase uppercase filter documentation} for a functionally identical example.
+ *
* @see angular.lowercase
*/
var lowercaseFilter = valueFn(lowercase);
@@ -20980,7 +15217,23 @@ var lowercaseFilter = valueFn(lowercase);
* @kind function
* @description
* Converts string to uppercase.
- * @see angular.uppercase
+ * @example
+ <example module="uppercaseFilterExample" name="filter-uppercase">
+ <file name="index.html">
+ <script>
+ angular.module('uppercaseFilterExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.title = 'This is a title';
+ }]);
+ </script>
+ <div ng-controller="ExampleController">
+ <!-- This title should be formatted normally -->
+ <h1>{{title}}</h1>
+ <!-- This title should be capitalized -->
+ <h1>{{title | uppercase}}</h1>
+ </div>
+ </file>
+ </example>
*/
var uppercaseFilter = valueFn(uppercase);
@@ -21008,7 +15261,7 @@ var uppercaseFilter = valueFn(uppercase);
* less than `limit` elements.
*
* @example
- <example module="limitToExample">
+ <example module="limitToExample" name="limit-to-filter">
<file name="index.html">
<script>
angular.module('limitToExample', [])
@@ -21090,7 +15343,7 @@ function limitToFilter() {
} else {
limit = toInt(limit);
}
- if (isNaN(limit)) return input;
+ if (isNumberNaN(limit)) return input;
if (isNumber(input)) input = input.toString();
if (!isArrayLike(input)) return input;
@@ -21132,7 +15385,7 @@ function sliceFn(input, begin, end) {
* String, etc).
*
* The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker
- * for the preceeding one. The `expression` is evaluated against each item and the output is used
+ * for the preceding one. The `expression` is evaluated against each item and the output is used
* for comparing with other items.
*
* You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in
@@ -21156,6 +15409,7 @@ function sliceFn(input, begin, end) {
* index: ...
* }
* ```
+ * **Note:** `null` values use `'null'` as their type.
* 2. The comparator function is used to sort the items, based on the derived values, types and
* indices.
*
@@ -21169,6 +15423,9 @@ function sliceFn(input, begin, end) {
* dummy predicate that returns the item's index as `value`.
* (If you are using a custom comparator, make sure it can handle this predicate as well.)
*
+ * If a custom comparator still can't distinguish between two items, then they will be sorted based
+ * on their index using the built-in comparator.
+ *
* Finally, in an attempt to simplify things, if a predicate returns an object as the extracted
* value for an item, `orderBy` will try to convert that object to a primitive value, before passing
* it to the comparator. The following rules govern the conversion:
@@ -21187,11 +15444,15 @@ function sliceFn(input, begin, end) {
*
* The default, built-in comparator should be sufficient for most usecases. In short, it compares
* numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
- * using their index in the original collection, and sorts values of different types by type.
+ * using their index in the original collection, sorts values of different types by type and puts
+ * `undefined` and `null` values at the end of the sorted list.
*
* More specifically, it follows these steps to determine the relative order of items:
*
- * 1. If the compared values are of different types, compare the types themselves alphabetically.
+ * 1. If the compared values are of different types:
+ * - If one of the values is undefined, consider it "greater than" the other.
+ * - Else if one of the values is null, consider it "greater than" the other.
+ * - Else compare the types themselves alphabetically.
* 2. If both values are of type `string`, compare them alphabetically in a case- and
* locale-insensitive way.
* 3. If both values are objects, compare their indices instead.
@@ -21202,6 +15463,10 @@ function sliceFn(input, begin, end) {
*
* **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
* saved as numbers and not strings.
+ * **Note:** For the purpose of sorting, `null` and `undefined` are considered "greater than"
+ * any other value (with undefined "greater than" null). This effectively means that `null`
+ * and `undefined` values end up at the end of a list sorted in ascending order.
+ * **Note:** `null` values use `'null'` as their type to be able to distinguish them from objects.
*
* @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
* @param {(Function|string|Array.<Function|string>)=} expression - A predicate (or list of
@@ -21211,7 +15476,7 @@ function sliceFn(input, begin, end) {
*
* - `Function`: A getter function. This function will be called with each item as argument and
* the return value will be used for sorting.
- * - `string`: An Angular expression. This expression will be evaluated against each item and the
+ * - `string`: An AngularJS expression. This expression will be evaluated against each item and the
* result will be used for sorting. For example, use `'label'` to sort by a property called
* `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label`
* property.<br />
@@ -21712,7 +15977,7 @@ function orderByFilter($parse) {
}
}
- return compare(v1.tieBreaker, v2.tieBreaker) * descending;
+ return (compare(v1.tieBreaker, v2.tieBreaker) || defaultCompare(v1.tieBreaker, v2.tieBreaker)) * descending;
}
};
@@ -21723,8 +15988,8 @@ function orderByFilter($parse) {
if (isFunction(predicate)) {
get = predicate;
} else if (isString(predicate)) {
- if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
- descending = predicate.charAt(0) == '-' ? -1 : 1;
+ if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) {
+ descending = predicate.charAt(0) === '-' ? -1 : 1;
predicate = predicate.substring(1);
}
if (predicate !== '') {
@@ -21768,8 +16033,7 @@ function orderByFilter($parse) {
function getPredicateValue(value, index) {
var type = typeof value;
if (value === null) {
- type = 'string';
- value = 'null';
+ type = 'null';
} else if (type === 'object') {
value = objectValue(value);
}
@@ -21800,7 +16064,11 @@ function orderByFilter($parse) {
result = value1 < value2 ? -1 : 1;
}
} else {
- result = type1 < type2 ? -1 : 1;
+ result = (type1 === 'undefined') ? 1 :
+ (type2 === 'undefined') ? -1 :
+ (type1 === 'null') ? 1 :
+ (type2 === 'null') ? -1 :
+ (type1 < type2) ? -1 : 1;
}
return result;
@@ -21823,12 +16091,10 @@ function ngDirective(directive) {
* @restrict E
*
* @description
- * Modifies the default behavior of the html A tag so that the default action is prevented when
+ * Modifies the default behavior of the html a tag so that the default action is prevented when
* the href attribute is empty.
*
- * This change permits the easy creation of action links with the `ngClick` directive
- * without changing the location or causing page reloads, e.g.:
- * `<a href="" ng-click="list.addItem()">Add Item</a>`
+ * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive.
*/
var htmlAnchorDirective = valueFn({
restrict: 'E',
@@ -21859,10 +16125,10 @@ var htmlAnchorDirective = valueFn({
* @priority 99
*
* @description
- * Using Angular markup like `{{hash}}` in an href attribute will
+ * Using AngularJS markup like `{{hash}}` in an href attribute will
* make the link go to the wrong URL if the user clicks it before
- * Angular has a chance to replace the `{{hash}}` markup with its
- * value. Until Angular replaces the markup the link will be broken
+ * AngularJS has a chance to replace the `{{hash}}` markup with its
+ * value. Until AngularJS replaces the markup the link will be broken
* and will most likely return a 404 error. The `ngHref` directive
* solves this problem.
*
@@ -21882,7 +16148,7 @@ var htmlAnchorDirective = valueFn({
* @example
* This example shows various combinations of `href`, `ng-href` and `ng-click` attributes
* in links and their different behaviors:
- <example>
+ <example name="ng-href">
<file name="index.html">
<input ng-model="value" /><br />
<a id="link-1" href ng-click="value = 1">link 1</a> (link, don't reload)<br />
@@ -21910,7 +16176,7 @@ var htmlAnchorDirective = valueFn({
element(by.id('link-3')).click();
- // At this point, we navigate away from an Angular page, so we need
+ // At this point, we navigate away from an AngularJS page, so we need
// to use browser.driver to get the base webdriver.
browser.wait(function() {
@@ -21939,7 +16205,7 @@ var htmlAnchorDirective = valueFn({
element(by.id('link-6')).click();
- // At this point, we navigate away from an Angular page, so we need
+ // At this point, we navigate away from an AngularJS page, so we need
// to use browser.driver to get the base webdriver.
browser.wait(function() {
return browser.driver.getCurrentUrl().then(function(url) {
@@ -21958,9 +16224,9 @@ var htmlAnchorDirective = valueFn({
* @priority 99
*
* @description
- * Using Angular markup like `{{hash}}` in a `src` attribute doesn't
+ * Using AngularJS markup like `{{hash}}` in a `src` attribute doesn't
* work right: The browser will fetch from the URL with the literal
- * text `{{hash}}` until Angular replaces the expression inside
+ * text `{{hash}}` until AngularJS replaces the expression inside
* `{{hash}}`. The `ngSrc` directive solves this problem.
*
* The buggy way to write it:
@@ -21984,9 +16250,9 @@ var htmlAnchorDirective = valueFn({
* @priority 99
*
* @description
- * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't
+ * Using AngularJS markup like `{{hash}}` in a `srcset` attribute doesn't
* work right: The browser will fetch from the URL with the literal
- * text `{{hash}}` until Angular replaces the expression inside
+ * text `{{hash}}` until AngularJS replaces the expression inside
* `{{hash}}`. The `ngSrcset` directive solves this problem.
*
* The buggy way to write it:
@@ -22011,14 +16277,15 @@ var htmlAnchorDirective = valueFn({
*
* @description
*
- * This directive sets the `disabled` attribute on the element if the
+ * This directive sets the `disabled` attribute on the element (typically a form control,
+ * e.g. `input`, `button`, `select` etc.) if the
* {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
*
* A special directive is necessary because we cannot use interpolation inside the `disabled`
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
- <example>
+ <example name="ng-disabled">
<file name="index.html">
<label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/>
<button ng-model="button" ng-disabled="checked">Button</button>
@@ -22032,7 +16299,6 @@ var htmlAnchorDirective = valueFn({
</file>
</example>
*
- * @element INPUT
* @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
* then the `disabled` attribute will be set on the element
*/
@@ -22054,16 +16320,16 @@ var htmlAnchorDirective = valueFn({
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
- <example>
+ <example name="ng-checked">
<file name="index.html">
- <label>Check me to check both: <input type="checkbox" ng-model="master"></label><br/>
- <input id="checkSlave" type="checkbox" ng-checked="master" aria-label="Slave input">
+ <label>Check me to check both: <input type="checkbox" ng-model="leader"></label><br/>
+ <input id="checkFollower" type="checkbox" ng-checked="leader" aria-label="Follower input">
</file>
<file name="protractor.js" type="protractor">
it('should check both checkBoxes', function() {
- expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy();
- element(by.model('master')).click();
- expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy();
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeFalsy();
+ element(by.model('leader')).click();
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeTruthy();
});
</file>
</example>
@@ -22090,10 +16356,10 @@ var htmlAnchorDirective = valueFn({
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
- <example>
+ <example name="ng-readonly">
<file name="index.html">
<label>Check me to make text readonly: <input type="checkbox" ng-model="checked"></label><br/>
- <input type="text" ng-readonly="checked" value="I'm Angular" aria-label="Readonly field" />
+ <input type="text" ng-readonly="checked" value="I'm AngularJS" aria-label="Readonly field" />
</file>
<file name="protractor.js" type="protractor">
it('should toggle readonly attr', function() {
@@ -22131,7 +16397,7 @@ var htmlAnchorDirective = valueFn({
* </div>
*
* @example
- <example>
+ <example name="ng-selected">
<file name="index.html">
<label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/>
<select aria-label="ngSelected demo">
@@ -22168,15 +16434,20 @@ var htmlAnchorDirective = valueFn({
*
* ## A note about browser compatibility
*
- * Edge, Firefox, and Internet Explorer do not support the `details` element, it is
+ * Internet Explorer and Edge do not support the `details` element, it is
* recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead.
*
* @example
- <example>
+ <example name="ng-open">
<file name="index.html">
- <label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/>
+ <label>Toggle details: <input type="checkbox" ng-model="open"></label><br/>
<details id="details" ng-open="open">
- <summary>Show/Hide me</summary>
+ <summary>List</summary>
+ <ul>
+ <li>Apple</li>
+ <li>Orange</li>
+ <li>Durian</li>
+ </ul>
</details>
</file>
<file name="protractor.js" type="protractor">
@@ -22198,7 +16469,7 @@ var ngAttributeAliasDirectives = {};
// boolean attrs are evaluated
forEach(BOOLEAN_ATTR, function(propName, attrName) {
// binding to multiple is not supported
- if (propName == "multiple") return;
+ if (propName === 'multiple') return;
function defaultLinkFn(scope, element, attr) {
scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
@@ -22235,10 +16506,10 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
link: function(scope, element, attr) {
//special case ngPattern when a literal regular expression value
//is used as the expression (this way we don't have to watch anything).
- if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") {
+ if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') {
var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
if (match) {
- attr.$set("ngPattern", new RegExp(match[1], match[2]));
+ attr.$set('ngPattern', new RegExp(match[1], match[2]));
return;
}
}
@@ -22254,7 +16525,7 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
// ng-src, ng-srcset, ng-href are interpolated
forEach(['src', 'srcset', 'href'], function(attrName) {
var normalized = directiveNormalize('ng-' + attrName);
- ngAttributeAliasDirectives[normalized] = function() {
+ ngAttributeAliasDirectives[normalized] = ['$sce', function($sce) {
return {
priority: 99, // it needs to run after the attributes are interpolated
link: function(scope, element, attr) {
@@ -22268,6 +16539,10 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
propName = null;
}
+ // We need to sanitize the url at least once, in case it is a constant
+ // non-interpolated attribute.
+ attr.$set(normalized, $sce.getTrustedMediaUrl(attr[normalized]));
+
attr.$observe(normalized, function(value) {
if (!value) {
if (attrName === 'href') {
@@ -22278,28 +16553,32 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
attr.$set(name, value);
- // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
+ // Support: IE 9-11 only
+ // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
- // we use attr[attrName] value since $set can sanitize the url.
+ // We use attr[attrName] value since $set might have sanitized the url.
if (msie && propName) element.prop(propName, attr[name]);
});
}
};
- };
+ }];
});
-/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
+/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS
*/
var nullFormCtrl = {
$addControl: noop,
+ $getControls: valueFn([]),
$$renameControl: nullFormRenameControl,
$removeControl: noop,
$setValidity: noop,
$setDirty: noop,
$setPristine: noop,
- $setSubmitted: noop
+ $setSubmitted: noop,
+ $$setSubmitted: noop
},
+PENDING_CLASS = 'ng-pending',
SUBMITTED_CLASS = 'ng-submitted';
function nullFormRenameControl(control, name) {
@@ -22314,17 +16593,23 @@ function nullFormRenameControl(control, name) {
* @property {boolean} $dirty True if user has already interacted with the form.
* @property {boolean} $valid True if all of the containing forms and controls are valid.
* @property {boolean} $invalid True if at least one containing control or form is invalid.
- * @property {boolean} $pending True if at least one containing control or form is pending.
* @property {boolean} $submitted True if user has submitted the form even if its invalid.
*
- * @property {Object} $error Is an object hash, containing references to controls or
- * forms with failing validators, where:
+ * @property {Object} $pending An object hash, containing references to controls or forms with
+ * pending validators, where:
+ *
+ * - keys are validations tokens (error names).
+ * - values are arrays of controls or forms that have a pending validator for the given error name.
+ *
+ * See {@link form.FormController#$error $error} for a list of built-in validation tokens.
+ *
+ * @property {Object} $error An object hash, containing references to controls or forms with failing
+ * validators, where:
*
* - keys are validation tokens (error names),
- * - values are arrays of controls or forms that have a failing validator for given error name.
+ * - values are arrays of controls or forms that have a failing validator for the given error name.
*
* Built-in validation tokens:
- *
* - `email`
* - `max`
* - `maxlength`
@@ -22350,22 +16635,28 @@ function nullFormRenameControl(control, name) {
*/
//asks for $scope to fool the BC controller module
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
-function FormController(element, attrs, $scope, $animate, $interpolate) {
- var form = this,
- controls = [];
+function FormController($element, $attrs, $scope, $animate, $interpolate) {
+ this.$$controls = [];
// init state
- form.$error = {};
- form.$$success = {};
- form.$pending = undefined;
- form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
- form.$dirty = false;
- form.$pristine = true;
- form.$valid = true;
- form.$invalid = false;
- form.$submitted = false;
- form.$$parentForm = nullFormCtrl;
+ this.$error = {};
+ this.$$success = {};
+ this.$pending = undefined;
+ this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$submitted = false;
+ this.$$parentForm = nullFormCtrl;
+
+ this.$$element = $element;
+ this.$$animate = $animate;
+
+ setupValidity(this);
+}
+FormController.prototype = {
/**
* @ngdoc method
* @name form.FormController#$rollbackViewValue
@@ -22377,11 +16668,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is typically needed by the reset button of
* a form that uses `ng-model-options` to pend updates.
*/
- form.$rollbackViewValue = function() {
- forEach(controls, function(control) {
+ $rollbackViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$rollbackViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -22394,11 +16685,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
- form.$commitViewValue = function() {
- forEach(controls, function(control) {
+ $commitViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$commitViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -22421,29 +16712,53 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* For example, if an input control is added that is already `$dirty` and has `$error` properties,
* calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
- form.$addControl = function(control) {
+ $addControl: function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
// and not added to the scope. Now we throw an error.
assertNotHasOwnProperty(control.$name, 'input');
- controls.push(control);
+ this.$$controls.push(control);
if (control.$name) {
- form[control.$name] = control;
+ this[control.$name] = control;
}
- control.$$parentForm = form;
- };
+ control.$$parentForm = this;
+ },
+
+ /**
+ * @ngdoc method
+ * @name form.FormController#$getControls
+ * @returns {Array} the controls that are currently part of this form
+ *
+ * @description
+ * This method returns a **shallow copy** of the controls that are currently part of this form.
+ * The controls can be instances of {@link form.FormController `FormController`}
+ * ({@link ngForm "child-forms"}) and of {@link ngModel.NgModelController `NgModelController`}.
+ * If you need access to the controls of child-forms, you have to call `$getControls()`
+ * recursively on them.
+ * This can be used for example to iterate over all controls to validate them.
+ *
+ * The controls can be accessed normally, but adding to, or removing controls from the array has
+ * no effect on the form. Instead, use {@link form.FormController#$addControl `$addControl()`} and
+ * {@link form.FormController#$removeControl `$removeControl()`} for this use-case.
+ * Likewise, adding a control to, or removing a control from the form is not reflected
+ * in the shallow copy. That means you should get a fresh copy from `$getControls()` every time
+ * you need access to the controls.
+ */
+ $getControls: function() {
+ return shallowCopy(this.$$controls);
+ },
// Private API: rename a form control
- form.$$renameControl = function(control, newName) {
+ $$renameControl: function(control, newName) {
var oldName = control.$name;
- if (form[oldName] === control) {
- delete form[oldName];
+ if (this[oldName] === control) {
+ delete this[oldName];
}
- form[newName] = control;
+ this[newName] = control;
control.$name = newName;
- };
+ },
/**
* @ngdoc method
@@ -22461,60 +16776,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* different from case to case. For example, removing the only `$dirty` control from a form may or
* may not mean that the form is still `$dirty`.
*/
- form.$removeControl = function(control) {
- if (control.$name && form[control.$name] === control) {
- delete form[control.$name];
- }
- forEach(form.$pending, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$error, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$$success, function(value, name) {
- form.$setValidity(name, null, control);
- });
-
- arrayRemove(controls, control);
+ $removeControl: function(control) {
+ if (control.$name && this[control.$name] === control) {
+ delete this[control.$name];
+ }
+ forEach(this.$pending, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$error, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$$success, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+
+ arrayRemove(this.$$controls, control);
control.$$parentForm = nullFormCtrl;
- };
-
-
- /**
- * @ngdoc method
- * @name form.FormController#$setValidity
- *
- * @description
- * Sets the validity of a form control.
- *
- * This method will also propagate to parent forms.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: element,
- set: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- object[property] = [controller];
- } else {
- var index = list.indexOf(controller);
- if (index === -1) {
- list.push(controller);
- }
- }
- },
- unset: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- return;
- }
- arrayRemove(list, controller);
- if (list.length === 0) {
- delete object[property];
- }
- },
- $animate: $animate
- });
+ },
/**
* @ngdoc method
@@ -22526,13 +16807,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* This method can be called to add the 'ng-dirty' class and set the form to a dirty
* state (ng-dirty class). This method will also propagate to parent forms.
*/
- form.$setDirty = function() {
- $animate.removeClass(element, PRISTINE_CLASS);
- $animate.addClass(element, DIRTY_CLASS);
- form.$dirty = true;
- form.$pristine = false;
- form.$$parentForm.$setDirty();
- };
+ $setDirty: function() {
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$parentForm.$setDirty();
+ },
/**
* @ngdoc method
@@ -22541,22 +16822,24 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* @description
* Sets the form to its pristine state.
*
- * This method can be called to remove the 'ng-dirty' class and set the form to its pristine
- * state (ng-pristine class). This method will also propagate to all the controls contained
- * in this form.
+ * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes
+ * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted`
+ * state to false.
+ *
+ * This method will also propagate to all the controls contained in this form.
*
* Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
* saving or resetting it.
*/
- form.$setPristine = function() {
- $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
- form.$dirty = false;
- form.$pristine = true;
- form.$submitted = false;
- forEach(controls, function(control) {
+ $setPristine: function() {
+ this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$submitted = false;
+ forEach(this.$$controls, function(control) {
control.$setPristine();
});
- };
+ },
/**
* @ngdoc method
@@ -22571,25 +16854,87 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* Setting a form controls back to their untouched state is often useful when setting the form
* back to its pristine state.
*/
- form.$setUntouched = function() {
- forEach(controls, function(control) {
+ $setUntouched: function() {
+ forEach(this.$$controls, function(control) {
control.$setUntouched();
});
- };
+ },
/**
* @ngdoc method
* @name form.FormController#$setSubmitted
*
* @description
- * Sets the form to its submitted state.
+ * Sets the form to its `$submitted` state. This will also set `$submitted` on all child and
+ * parent forms of the form.
*/
- form.$setSubmitted = function() {
- $animate.addClass(element, SUBMITTED_CLASS);
- form.$submitted = true;
- form.$$parentForm.$setSubmitted();
- };
-}
+ $setSubmitted: function() {
+ var rootForm = this;
+ while (rootForm.$$parentForm && (rootForm.$$parentForm !== nullFormCtrl)) {
+ rootForm = rootForm.$$parentForm;
+ }
+ rootForm.$$setSubmitted();
+ },
+
+ $$setSubmitted: function() {
+ this.$$animate.addClass(this.$$element, SUBMITTED_CLASS);
+ this.$submitted = true;
+ forEach(this.$$controls, function(control) {
+ if (control.$$setSubmitted) {
+ control.$$setSubmitted();
+ }
+ });
+ }
+};
+
+/**
+ * @ngdoc method
+ * @name form.FormController#$setValidity
+ *
+ * @description
+ * Change the validity state of the form, and notify the parent form (if any).
+ *
+ * Application developers will rarely need to call this method directly. It is used internally, by
+ * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a
+ * control's validity state to the parent `FormController`.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be
+ * assigned to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` (for
+ * unfulfilled `$asyncValidators`), so that it is available for data-binding. The
+ * `validationErrorKey` should be in camelCase and will get converted into dash-case for
+ * class name. Example: `myError` will result in `ng-valid-my-error` and
+ * `ng-invalid-my-error` classes and can be bound to as `{{ someForm.$error.myError }}`.
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending
+ * (undefined), or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by AngularJS when validators do not run because of parse errors and when
+ * `$asyncValidators` do not run because any of the `$validators` failed.
+ * @param {NgModelController | FormController} controller - The controller whose validity state is
+ * triggering the change.
+ */
+addSetValidityMethod({
+ clazz: FormController,
+ set: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ object[property] = [controller];
+ } else {
+ var index = list.indexOf(controller);
+ if (index === -1) {
+ list.push(controller);
+ }
+ }
+ },
+ unset: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ return;
+ }
+ arrayRemove(list, controller);
+ if (list.length === 0) {
+ delete object[property];
+ }
+ }
+});
/**
* @ngdoc directive
@@ -22597,16 +16942,21 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* @restrict EAC
*
* @description
- * Nestable alias of {@link ng.directive:form `form`} directive. HTML
- * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a
- * sub-group of controls needs to be determined.
+ * Helper directive that makes it possible to create control groups inside a
+ * {@link ng.directive:form `form`} directive.
+ * These "child forms" can be used, for example, to determine the validity of a sub-group of
+ * controls.
*
- * Note: the purpose of `ngForm` is to group controls,
- * but not to be a replacement for the `<form>` tag with all of its capabilities
- * (e.g. posting to the server, ...).
+ * <div class="alert alert-danger">
+ * **Note**: `ngForm` cannot be used as a replacement for `<form>`, because it lacks its
+ * [built-in HTML functionality](https://html.spec.whatwg.org/#the-form-element).
+ * Specifically, you cannot submit `ngForm` like a `<form>` tag. That means,
+ * you cannot send data to the server with `ngForm`, or integrate it with
+ * {@link ng.directive:ngSubmit `ngSubmit`}.
+ * </div>
*
- * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into
- * related scope, under this name.
+ * @param {string=} ngForm|name Name of the form. If specified, the form controller will
+ * be published into the related scope, under this name.
*
*/
@@ -22622,15 +16972,15 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* If the `name` attribute is specified, the form controller is published onto the current scope under
* this name.
*
- * # Alias: {@link ng.directive:ngForm `ngForm`}
+ * ## Alias: {@link ng.directive:ngForm `ngForm`}
*
- * In Angular, forms can be nested. This means that the outer form is valid when all of the child
+ * In AngularJS, forms can be nested. This means that the outer form is valid when all of the child
* forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so
- * Angular provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to
+ * AngularJS provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to
* `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group
* of controls needs to be determined.
*
- * # CSS classes
+ * ## CSS classes
* - `ng-valid` is set if the form is valid.
* - `ng-invalid` is set if the form is invalid.
* - `ng-pending` is set if the form is pending.
@@ -22641,14 +16991,14 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
*
- * # Submitting a form and preventing the default action
+ * ## Submitting a form and preventing the default action
*
- * Since the role of forms in client-side Angular applications is different than in classical
+ * Since the role of forms in client-side AngularJS applications is different than in classical
* roundtrip apps, it is desirable for the browser not to translate the form submission into a full
* page reload that sends the data to the server. Instead some javascript logic should be triggered
* to handle the form submission in an application-specific way.
*
- * For this reason, Angular prevents the default action (form submission to the server) unless the
+ * For this reason, AngularJS prevents the default action (form submission to the server) unless the
* `<form>` element has an `action` attribute specified.
*
* You can use one of the following two ways to specify what javascript method should be called when
@@ -22674,8 +17024,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
* to have access to the updated model.
*
- * ## Animation Hooks
- *
+ * @animations
* Animations in ngForm are triggered when any of the associated CSS classes are added and removed.
* These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any
* other validations that are performed within the form. Animations in ngForm are similar to how
@@ -22699,7 +17048,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* </pre>
*
* @example
- <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample">
+ <example name="ng-form" deps="angular-animate.js" animations="true" fixBase="true" module="formExample">
<file name="index.html">
<script>
angular.module('formExample', [])
@@ -22786,13 +17135,13 @@ var formDirectiveFactory = function(isNgForm) {
event.preventDefault();
};
- addEventListenerFn(formElement[0], 'submit', handleFormSubmission);
+ formElement[0].addEventListener('submit', handleFormSubmission);
// unregister the preventDefault listener so that we don't not leak memory but in a
// way that will achieve the prevention of the default action.
formElement.on('$destroy', function() {
$timeout(function() {
- removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
+ formElement[0].removeEventListener('submit', handleFormSubmission);
}, 0, false);
});
}
@@ -22837,13 +17186,12 @@ var formDirectiveFactory = function(isNgForm) {
var formDirective = formDirectiveFactory();
var ngFormDirective = formDirectiveFactory(true);
-/* global VALID_CLASS: false,
+/* global
+ VALID_CLASS: false,
INVALID_CLASS: false,
PRISTINE_CLASS: false,
DIRTY_CLASS: false,
- UNTOUCHED_CLASS: false,
- TOUCHED_CLASS: false,
- ngModelMinErr: false,
+ ngModelMinErr: false
*/
// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
@@ -22859,12 +17207,11 @@ var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-
// 7. Path
// 8. Query
// 9. Fragment
-// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999
-var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
-/* jshint maxlen:220 */
-var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
-/* jshint maxlen:200 */
-var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
+// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999
+var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
+// eslint-disable-next-line max-len
+var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
+var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/;
var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/;
@@ -22884,10 +17231,10 @@ var inputType = {
* @name input[text]
*
* @description
- * Standard HTML text input with angular data binding, inherited by most of the `input` elements.
+ * Standard HTML text input with AngularJS data binding, inherited by most of the `input` elements.
*
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Adds `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -22902,7 +17249,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -22910,9 +17257,9 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
* This parameter is ignored for input[type=password] controls, which will never trim the
* input.
*
@@ -22986,13 +17333,13 @@ var inputType = {
* modern browsers do not yet support this input type, it is important to provide cues to users on the
* expected input format via a placeholder or label.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
* valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute
@@ -23010,7 +17357,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23044,7 +17391,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM-dd"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -23089,13 +17435,17 @@ var inputType = {
* the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * The format of the displayed time can be adjusted with the
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
+ * and `timeStripZeroSeconds`.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
@@ -23113,7 +17463,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23147,7 +17497,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -23193,13 +17542,18 @@ var inputType = {
* local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
* Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
- * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions}. By default,
+ * this is the timezone of the browser.
+ *
+ * The format of the displayed time can be adjusted with the
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
+ * and `timeStripZeroSeconds`.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
@@ -23217,7 +17571,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23251,7 +17605,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "HH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -23296,13 +17649,17 @@ var inputType = {
* the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* week format (yyyy-W##), for example: `2013-W02`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
+ * The value of the resulting Date object will be set to Thursday at 00:00:00 of the requested week,
+ * due to ISO-8601 week numbering standards. Information on ISO's system for numbering the weeks of the
+ * year can be found at: https://en.wikipedia.org/wiki/ISO_8601#Week_dates
+ *
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
@@ -23320,7 +17677,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23356,7 +17713,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-Www"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -23399,7 +17755,7 @@ var inputType = {
* the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* month format (yyyy-MM), for example: `2009-01`.
*
- * The model must always be a Date object, otherwise Angular will throw an error.
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
* If the model is not set to the first of the month, the next view to model update will set it
* to the first of the month.
@@ -23407,7 +17763,7 @@ var inputType = {
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
@@ -23426,7 +17782,7 @@ var inputType = {
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23460,7 +17816,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -23505,12 +17860,16 @@ var inputType = {
* error if not a valid number.
*
* <div class="alert alert-warning">
- * The model must always be of type `number` otherwise Angular will throw an error.
+ * The model must always be of type `number` otherwise AngularJS will throw an error.
* Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt}
* error docs for more information and an example of how to convert your model if necessary.
* </div>
*
- * ## Issues with HTML5 constraint validation
+ *
+ *
+ * @knownIssue
+ *
+ * ### HTML5 constraint validation and `allowInvalid`
*
* In browsers that follow the
* [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29),
@@ -23519,11 +17878,32 @@ var inputType = {
* which means the view / model values in `ngModel` and subsequently the scope value
* will also be an empty string.
*
+ * @knownIssue
+ *
+ * ### Large numbers and `step` validation
+ *
+ * The `step` validation will not work correctly for very large numbers (e.g. 9999999999) due to
+ * Javascript's arithmetic limitations. If you need to handle large numbers, purpose-built
+ * libraries (e.g. https://github.com/MikeMcl/big.js/), can be included into AngularJS by
+ * {@link guide/forms#modifying-built-in-validators overwriting the validators}
+ * for `number` and / or `step`, or by {@link guide/forms#custom-validation applying custom validators}
+ * to an `input[text]` element. The source for `input[number]` type can be used as a starting
+ * point for both implementations.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * Can be interpolated.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * Can be interpolated.
+ * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint.
+ * Can be interpolated.
+ * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint,
+ * but does not trigger HTML5 native validation. Takes an expression.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -23537,7 +17917,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -23545,7 +17925,7 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23620,7 +18000,7 @@ var inputType = {
* the built-in validators (see the {@link guide/forms Forms guide})
* </div>
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -23635,7 +18015,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -23643,7 +18023,7 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23715,11 +18095,13 @@ var inputType = {
*
* <div class="alert alert-warning">
* **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex
- * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can
- * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide})
+ * used in Chromium, which may not fulfill your app's requirements.
+ * If you need stricter (e.g. requiring a top-level domain), or more relaxed validation
+ * (e.g. allowing IPv6 address literals) you can use `ng-pattern` or
+ * modify the built-in validators (see the {@link guide/forms Forms guide}).
* </div>
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -23734,7 +18116,7 @@ var inputType = {
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -23742,7 +18124,7 @@ var inputType = {
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23810,14 +18192,41 @@ var inputType = {
* @description
* HTML radio button.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * **Note:**<br>
+ * All inputs controlled by {@link ngModel ngModel} (including those of type `radio`) will use the
+ * value of their `name` attribute to determine the property under which their
+ * {@link ngModel.NgModelController NgModelController} will be published on the parent
+ * {@link form.FormController FormController}. Thus, if you use the same `name` for multiple
+ * inputs of a form (e.g. a group of radio inputs), only _one_ `NgModelController` will be
+ * published on the parent `FormController` under that name. The rest of the controllers will
+ * continue to work as expected, but you won't be able to access them as properties on the parent
+ * `FormController`.
+ *
+ * <div class="alert alert-info">
+ * <p>
+ * In plain HTML forms, the `name` attribute is used to identify groups of radio inputs, so
+ * that the browser can manage their state (checked/unchecked) based on the state of other
+ * inputs in the same group.
+ * </p>
+ * <p>
+ * In AngularJS forms, this is not necessary. The input's state will be updated based on the
+ * value of the underlying model data.
+ * </p>
+ * </div>
+ *
+ * <div class="alert alert-success">
+ * If you omit the `name` attribute on a radio input, `ngModel` will automatically assign it a
+ * unique name.
+ * </div>
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string} value The value to which the `ngModel` expression should be set when selected.
* Note that `value` only supports `string` values, i.e. the scope model needs to be a string,
* too. Use `ngValue` if you need complex models (`number`, `object`, ...).
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio
+ * @param {string} ngValue AngularJS expression to which `ngModel` will be be set when the radio
* is selected. Should be used instead of the `value` attribute if you need
* a non-string `ngModel` (`boolean`, `array`, ...).
*
@@ -23855,19 +18264,140 @@ var inputType = {
</file>
<file name="protractor.js" type="protractor">
it('should change state', function() {
+ var inputs = element.all(by.model('color.name'));
var color = element(by.binding('color.name'));
expect(color.getText()).toContain('blue');
- element.all(by.model('color.name')).get(0).click();
-
+ inputs.get(0).click();
expect(color.getText()).toContain('red');
+
+ inputs.get(1).click();
+ expect(color.getText()).toContain('green');
});
</file>
</example>
*/
'radio': radioInputType,
+ /**
+ * @ngdoc input
+ * @name input[range]
+ *
+ * @description
+ * Native range input with validation and transformation.
+ *
+ * The model for the range input must always be a `Number`.
+ *
+ * IE9 and other browsers that do not support the `range` type fall back
+ * to a text input without any default values for `min`, `max` and `step`. Model binding,
+ * validation and number parsing are nevertheless supported.
+ *
+ * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]`
+ * in a way that never allows the input to hold an invalid value. That means:
+ * - any non-numerical value is set to `(max + min) / 2`.
+ * - any numerical value that is less than the current min val, or greater than the current max val
+ * is set to the min / max val respectively.
+ * - additionally, the current `step` is respected, so the nearest value that satisfies a step
+ * is used.
+ *
+ * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range))
+ * for more info.
+ *
+ * This has the following consequences for AngularJS:
+ *
+ * Since the element value should always reflect the current model value, a range input
+ * will set the bound ngModel expression to the value that the browser has set for the
+ * input element. For example, in the following input `<input type="range" ng-model="model.value">`,
+ * if the application sets `model.value = null`, the browser will set the input to `'50'`.
+ * AngularJS will then set the model to `50`, to prevent input and model value being out of sync.
+ *
+ * That means the model for range will immediately be set to `50` after `ngModel` has been
+ * initialized. It also means a range input can never have the required error.
+ *
+ * This does not only affect changes to the model value, but also to the values of the `min`,
+ * `max`, and `step` attributes. When these change in a way that will cause the browser to modify
+ * the input value, AngularJS will also update the model value.
+ *
+ * Automatic value adjustment also means that a range input element can never have the `required`,
+ * `min`, or `max` errors.
+ *
+ * However, `step` is currently only fully implemented by Firefox. Other browsers have problems
+ * when the step value changes dynamically - they do not adjust the element value correctly, but
+ * instead may set the `stepMismatch` error. If that's the case, the AngularJS will set the `step`
+ * error on the input, and set the model to `undefined`.
+ *
+ * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do
+ * not set the `min` and `max` attributes, which means that the browser won't automatically adjust
+ * the input value based on their values, and will always assume min = 0, max = 100, and step = 1.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation to ensure that the value entered is greater
+ * than `min`. Can be interpolated.
+ * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`.
+ * Can be interpolated.
+ * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
+ * Can be interpolated.
+ * @param {expression=} ngChange AngularJS expression to be executed when the ngModel value changes due
+ * to user interaction with the input element.
+ * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the
+ * element. **Note** : `ngChecked` should not be used alongside `ngModel`.
+ * Checkout {@link ng.directive:ngChecked ngChecked} for usage.
+ *
+ * @example
+ <example name="range-input-directive" module="rangeExample">
+ <file name="index.html">
+ <script>
+ angular.module('rangeExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.value = 75;
+ $scope.min = 10;
+ $scope.max = 90;
+ }]);
+ </script>
+ <form name="myForm" ng-controller="ExampleController">
+
+ Model as range: <input type="range" name="range" ng-model="value" min="{{min}}" max="{{max}}">
+ <hr>
+ Model as number: <input type="number" ng-model="value"><br>
+ Min: <input type="number" ng-model="min"><br>
+ Max: <input type="number" ng-model="max"><br>
+ value = <code>{{value}}</code><br/>
+ myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
+ myForm.range.$error = <code>{{myForm.range.$error}}</code>
+ </form>
+ </file>
+ </example>
+
+ * ## Range Input with ngMin & ngMax attributes
+
+ * @example
+ <example name="range-input-directive-ng" module="rangeExample">
+ <file name="index.html">
+ <script>
+ angular.module('rangeExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.value = 75;
+ $scope.min = 10;
+ $scope.max = 90;
+ }]);
+ </script>
+ <form name="myForm" ng-controller="ExampleController">
+ Model as range: <input type="range" name="range" ng-model="value" ng-min="min" ng-max="max">
+ <hr>
+ Model as number: <input type="number" ng-model="value"><br>
+ Min: <input type="number" ng-model="min"><br>
+ Max: <input type="number" ng-model="max"><br>
+ value = <code>{{value}}</code><br/>
+ myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
+ myForm.range.$error = <code>{{myForm.range.$error}}</code>
+ </form>
+ </file>
+ </example>
+
+ */
+ 'range': rangeInputType,
/**
* @ngdoc input
@@ -23876,11 +18406,11 @@ var inputType = {
* @description
* HTML checkbox.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {expression=} ngTrueValue The value to which the expression should be set when selected.
* @param {expression=} ngFalseValue The value to which the expression should be set when not selected.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
@@ -23947,7 +18477,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var type = lowercase(element[0].type);
- // In composition mode, users are still inputing intermediate text buffer,
+ // In composition mode, users are still inputting intermediate text buffer,
// hold the listener until composition is done.
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
if (!$sniffer.android) {
@@ -23957,6 +18487,16 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
composing = true;
});
+ // Support: IE9+
+ element.on('compositionupdate', function(ev) {
+ // End composition when ev.data is empty string on 'compositionupdate' event.
+ // When the input de-focusses (e.g. by clicking away), IE triggers 'compositionupdate'
+ // instead of 'compositionend'.
+ if (isUndefined(ev.data) || ev.data === '') {
+ composing = false;
+ }
+ });
+
element.on('compositionend', function() {
composing = false;
listener();
@@ -24005,7 +18545,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
};
- element.on('keydown', function(event) {
+ element.on('keydown', /** @this */ function(event) {
var key = event.keyCode;
// ignore
@@ -24015,9 +18555,9 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
deferListener(event, this, this.value);
});
- // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
+ // if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it
if ($sniffer.hasEvent('paste')) {
- element.on('paste cut', deferListener);
+ element.on('paste cut drop', deferListener);
}
}
@@ -24030,7 +18570,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// For these event types, when native validators are present and the browser supports the type,
// check for validity changes on various DOM events.
if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
- element.on(PARTIAL_VALIDATION_EVENTS, function(ev) {
+ element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) {
if (!timeout) {
var validity = this[VALIDITY_STATE_PROPERTY];
var origBadInput = validity.badInput;
@@ -24087,7 +18627,7 @@ function weekParser(isoWeek, existingDate) {
}
function createDateParser(regexp, mapping) {
- return function(iso, date) {
+ return function(iso, previousDate) {
var parts, map;
if (isDate(iso)) {
@@ -24098,7 +18638,7 @@ function createDateParser(regexp, mapping) {
// When a date is JSON'ified to wraps itself inside of an extra
// set of double quotes. This makes the date parsing code unable
// to match the date string and parse it as a date.
- if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') {
+ if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') {
iso = iso.substring(1, iso.length - 1);
}
if (ISO_DATE_REGEXP.test(iso)) {
@@ -24109,15 +18649,15 @@ function createDateParser(regexp, mapping) {
if (parts) {
parts.shift();
- if (date) {
+ if (previousDate) {
map = {
- yyyy: date.getFullYear(),
- MM: date.getMonth() + 1,
- dd: date.getDate(),
- HH: date.getHours(),
- mm: date.getMinutes(),
- ss: date.getSeconds(),
- sss: date.getMilliseconds() / 1000
+ yyyy: previousDate.getFullYear(),
+ MM: previousDate.getMonth() + 1,
+ dd: previousDate.getDate(),
+ HH: previousDate.getHours(),
+ mm: previousDate.getMinutes(),
+ ss: previousDate.getSeconds(),
+ sss: previousDate.getMilliseconds() / 1000
};
} else {
map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 };
@@ -24128,7 +18668,15 @@ function createDateParser(regexp, mapping) {
map[mapping[index]] = +part;
}
});
- return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
+
+ var date = new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
+ if (map.yyyy < 100) {
+ // In the constructor, 2-digit years map to 1900-1999.
+ // Use `setFullYear()` to set the correct year.
+ date.setFullYear(map.yyyy);
+ }
+
+ return date;
}
}
@@ -24137,25 +18685,24 @@ function createDateParser(regexp, mapping) {
}
function createDateInputType(type, regexp, parseDate, format) {
- return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
- badInputChecker(scope, element, attr, ctrl);
+ return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
+ badInputChecker(scope, element, attr, ctrl, type);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
- var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
+
+ var isTimeType = type === 'time' || type === 'datetimelocal';
var previousDate;
+ var previousTimezone;
- ctrl.$$parserName = type;
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
+
if (regexp.test(value)) {
// 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) {
- parsedDate = convertTimezoneToLocal(parsedDate, timezone);
- }
- return parsedDate;
+ return parseDateAndConvertTimeZoneToLocal(value, previousDate);
}
+ ctrl.$$parserName = type;
return undefined;
});
@@ -24165,35 +18712,50 @@ function createDateInputType(type, regexp, parseDate, format) {
}
if (isValidDate(value)) {
previousDate = value;
- if (previousDate && timezone) {
+ var timezone = ctrl.$options.getOption('timezone');
+
+ if (timezone) {
+ previousTimezone = timezone;
previousDate = convertTimezoneToLocal(previousDate, timezone, true);
}
- return $filter('date')(value, format, timezone);
+
+ return formatter(value, timezone);
} else {
previousDate = null;
+ previousTimezone = null;
return '';
}
});
if (isDefined(attr.min) || attr.ngMin) {
- var minVal;
+ var minVal = attr.min || $parse(attr.ngMin)(scope);
+ var parsedMinVal = parseObservedDateValue(minVal);
+
ctrl.$validators.min = function(value) {
- return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal;
+ return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
};
attr.$observe('min', function(val) {
- minVal = parseObservedDateValue(val);
- ctrl.$validate();
+ if (val !== minVal) {
+ parsedMinVal = parseObservedDateValue(val);
+ minVal = val;
+ ctrl.$validate();
+ }
});
}
if (isDefined(attr.max) || attr.ngMax) {
- var maxVal;
+ var maxVal = attr.max || $parse(attr.ngMax)(scope);
+ var parsedMaxVal = parseObservedDateValue(maxVal);
+
ctrl.$validators.max = function(value) {
- return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
+ return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
};
attr.$observe('max', function(val) {
- maxVal = parseObservedDateValue(val);
- ctrl.$validate();
+ if (val !== maxVal) {
+ parsedMaxVal = parseObservedDateValue(val);
+ maxVal = val;
+ ctrl.$validate();
+ }
});
}
@@ -24203,30 +18765,68 @@ function createDateInputType(type, regexp, parseDate, format) {
}
function parseObservedDateValue(val) {
- return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val;
+ return isDefined(val) && !isDate(val) ? parseDateAndConvertTimeZoneToLocal(val) || undefined : val;
+ }
+
+ function parseDateAndConvertTimeZoneToLocal(value, previousDate) {
+ var timezone = ctrl.$options.getOption('timezone');
+
+ if (previousTimezone && previousTimezone !== timezone) {
+ // If the timezone has changed, adjust the previousDate to the default timezone
+ // so that the new date is converted with the correct timezone offset
+ previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone));
+ }
+
+ var parsedDate = parseDate(value, previousDate);
+
+ if (!isNaN(parsedDate) && timezone) {
+ parsedDate = convertTimezoneToLocal(parsedDate, timezone);
+ }
+ return parsedDate;
+ }
+
+ function formatter(value, timezone) {
+ var targetFormat = format;
+
+ if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) {
+ targetFormat = format
+ .replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat'))
+ .replace(/:$/, '');
+ }
+
+ var formatted = $filter('date')(value, targetFormat, timezone);
+
+ if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) {
+ formatted = formatted.replace(/(?::00)?(?:\.000)?$/, '');
+ }
+
+ return formatted;
}
};
}
-function badInputChecker(scope, element, attr, ctrl) {
+function badInputChecker(scope, element, attr, ctrl, parserName) {
var node = element[0];
var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
if (nativeValidation) {
ctrl.$parsers.push(function(value) {
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
- return validity.badInput || validity.typeMismatch ? undefined : value;
+ if (validity.badInput || validity.typeMismatch) {
+ ctrl.$$parserName = parserName;
+ return undefined;
+ }
+
+ return value;
});
}
}
-function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- badInputChecker(scope, element, attr, ctrl);
- baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
-
- ctrl.$$parserName = 'number';
+function numberFormatterParser(ctrl) {
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
if (NUMBER_REGEXP.test(value)) return parseFloat(value);
+
+ ctrl.$$parserName = 'number';
return undefined;
});
@@ -24239,37 +18839,282 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
return value;
});
+}
+
+function parseNumberAttrVal(val) {
+ if (isDefined(val) && !isNumber(val)) {
+ val = parseFloat(val);
+ }
+ return !isNumberNaN(val) ? val : undefined;
+}
+
+function isNumberInteger(num) {
+ // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066
+ // (minus the assumption that `num` is a number)
+
+ // eslint-disable-next-line no-bitwise
+ return (num | 0) === num;
+}
+
+function countDecimals(num) {
+ var numString = num.toString();
+ var decimalSymbolIndex = numString.indexOf('.');
+
+ if (decimalSymbolIndex === -1) {
+ if (-1 < num && num < 1) {
+ // It may be in the exponential notation format (`1e-X`)
+ var match = /e-(\d+)$/.exec(numString);
+
+ if (match) {
+ return Number(match[1]);
+ }
+ }
+
+ return 0;
+ }
+
+ return numString.length - decimalSymbolIndex - 1;
+}
+
+function isValidForStep(viewValue, stepBase, step) {
+ // At this point `stepBase` and `step` are expected to be non-NaN values
+ // and `viewValue` is expected to be a valid stringified number.
+ var value = Number(viewValue);
+
+ var isNonIntegerValue = !isNumberInteger(value);
+ var isNonIntegerStepBase = !isNumberInteger(stepBase);
+ var isNonIntegerStep = !isNumberInteger(step);
+
+ // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or
+ // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers.
+ if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) {
+ var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0;
+ var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0;
+ var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0;
+
+ var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals);
+ var multiplier = Math.pow(10, decimalCount);
+
+ value = value * multiplier;
+ stepBase = stepBase * multiplier;
+ step = step * multiplier;
+
+ if (isNonIntegerValue) value = Math.round(value);
+ if (isNonIntegerStepBase) stepBase = Math.round(stepBase);
+ if (isNonIntegerStep) step = Math.round(step);
+ }
+
+ return (value - stepBase) % step === 0;
+}
+
+function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
+ badInputChecker(scope, element, attr, ctrl, 'number');
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var parsedMinVal;
if (isDefined(attr.min) || attr.ngMin) {
- var minVal;
- ctrl.$validators.min = function(value) {
- return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
+ var minVal = attr.min || $parse(attr.ngMin)(scope);
+ parsedMinVal = parseNumberAttrVal(minVal);
+
+ ctrl.$validators.min = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
};
attr.$observe('min', function(val) {
- if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val);
+ if (val !== minVal) {
+ parsedMinVal = parseNumberAttrVal(val);
+ minVal = val;
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
}
- minVal = isNumber(val) && !isNaN(val) ? val : undefined;
- // TODO(matsko): implement validateLater to reduce number of validations
- ctrl.$validate();
});
}
if (isDefined(attr.max) || attr.ngMax) {
- var maxVal;
- ctrl.$validators.max = function(value) {
- return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
+ var maxVal = attr.max || $parse(attr.ngMax)(scope);
+ var parsedMaxVal = parseNumberAttrVal(maxVal);
+
+ ctrl.$validators.max = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
};
attr.$observe('max', function(val) {
- if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val);
+ if (val !== maxVal) {
+ parsedMaxVal = parseNumberAttrVal(val);
+ maxVal = val;
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
}
- maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
+ });
+ }
+
+ if (isDefined(attr.step) || attr.ngStep) {
+ var stepVal = attr.step || $parse(attr.ngStep)(scope);
+ var parsedStepVal = parseNumberAttrVal(stepVal);
+
+ ctrl.$validators.step = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
+ isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
+ };
+
+ attr.$observe('step', function(val) {
// TODO(matsko): implement validateLater to reduce number of validations
- ctrl.$validate();
+ if (val !== stepVal) {
+ parsedStepVal = parseNumberAttrVal(val);
+ stepVal = val;
+ ctrl.$validate();
+ }
+
});
+
+ }
+}
+
+function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ badInputChecker(scope, element, attr, ctrl, 'range');
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
+ minVal = supportsRange ? 0 : undefined,
+ maxVal = supportsRange ? 100 : undefined,
+ stepVal = supportsRange ? 1 : undefined,
+ validity = element[0].validity,
+ hasMinAttr = isDefined(attr.min),
+ hasMaxAttr = isDefined(attr.max),
+ hasStepAttr = isDefined(attr.step);
+
+ var originalRender = ctrl.$render;
+
+ ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
+ //Browsers that implement range will set these values automatically, but reading the adjusted values after
+ //$render would cause the min / max validators to be applied with the wrong value
+ function rangeRender() {
+ originalRender();
+ ctrl.$setViewValue(element.val());
+ } :
+ originalRender;
+
+ if (hasMinAttr) {
+ minVal = parseNumberAttrVal(attr.min);
+
+ ctrl.$validators.min = supportsRange ?
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ function noopMinValidator() { return true; } :
+ // non-support browsers validate the min val
+ function minValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
+ };
+
+ setInitialValueAndObserver('min', minChange);
+ }
+
+ if (hasMaxAttr) {
+ maxVal = parseNumberAttrVal(attr.max);
+
+ ctrl.$validators.max = supportsRange ?
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ function noopMaxValidator() { return true; } :
+ // non-support browsers validate the max val
+ function maxValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
+ };
+
+ setInitialValueAndObserver('max', maxChange);
+ }
+
+ if (hasStepAttr) {
+ stepVal = parseNumberAttrVal(attr.step);
+
+ ctrl.$validators.step = supportsRange ?
+ function nativeStepValidator() {
+ // Currently, only FF implements the spec on step change correctly (i.e. adjusting the
+ // input element value to a valid value). It's possible that other browsers set the stepMismatch
+ // validity error instead, so we can at least report an error in that case.
+ return !validity.stepMismatch;
+ } :
+ // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
+ function stepValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
+ isValidForStep(viewValue, minVal || 0, stepVal);
+ };
+
+ setInitialValueAndObserver('step', stepChange);
+ }
+
+ function setInitialValueAndObserver(htmlAttrName, changeFn) {
+ // interpolated attributes set the attribute value only after a digest, but we need the
+ // attribute value when the input is first rendered, so that the browser can adjust the
+ // input value based on the min/max value
+ element.attr(htmlAttrName, attr[htmlAttrName]);
+ var oldVal = attr[htmlAttrName];
+ attr.$observe(htmlAttrName, function wrappedObserver(val) {
+ if (val !== oldVal) {
+ oldVal = val;
+ changeFn(val);
+ }
+ });
+ }
+
+ function minChange(val) {
+ minVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the minVal is greater than the element value
+ if (minVal > elVal) {
+ elVal = minVal;
+ element.val(elVal);
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function maxChange(val) {
+ maxVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the maxVal is less than the element value
+ if (maxVal < elVal) {
+ element.val(maxVal);
+ // IE11 and Chrome don't set the value to the minVal when max < min
+ elVal = maxVal < minVal ? minVal : maxVal;
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function stepChange(val) {
+ stepVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ // Some browsers don't adjust the input value correctly, but set the stepMismatch error
+ if (!supportsRange) {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ } else if (ctrl.$viewValue !== element.val()) {
+ ctrl.$setViewValue(element.val());
+ }
}
}
@@ -24279,7 +19124,6 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
stringBasedInputType(ctrl);
- ctrl.$$parserName = 'url';
ctrl.$validators.url = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
@@ -24292,7 +19136,6 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
stringBasedInputType(ctrl);
- ctrl.$$parserName = 'email';
ctrl.$validators.email = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
@@ -24300,22 +19143,31 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
function radioInputType(scope, element, attr, ctrl) {
+ var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false';
// make the name unique, if not defined
if (isUndefined(attr.name)) {
element.attr('name', nextUid());
}
var listener = function(ev) {
+ var value;
if (element[0].checked) {
- ctrl.$setViewValue(attr.value, ev && ev.type);
+ value = attr.value;
+ if (doTrim) {
+ value = trim(value);
+ }
+ ctrl.$setViewValue(value, ev && ev.type);
}
};
- element.on('click', listener);
+ element.on('change', listener);
ctrl.$render = function() {
var value = attr.value;
- element[0].checked = (value == ctrl.$viewValue);
+ if (doTrim) {
+ value = trim(value);
+ }
+ element[0].checked = (value === ctrl.$viewValue);
};
attr.$observe('value', ctrl.$render);
@@ -24342,7 +19194,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
ctrl.$setViewValue(element[0].checked, ev && ev.type);
};
- element.on('click', listener);
+ element.on('change', listener);
ctrl.$render = function() {
element[0].checked = ctrl.$viewValue;
@@ -24371,11 +19223,11 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @restrict E
*
* @description
- * HTML textarea element control with angular data-binding. The data-binding and validation
+ * HTML textarea element control with AngularJS data-binding. The data-binding and validation
* properties of this element are exactly the same as those of the
* {@link ng.directive:input input element}.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -24387,7 +19239,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -24395,9 +19247,23 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
+ *
+ * @knownIssue
+ *
+ * When specifying the `placeholder` attribute of `<textarea>`, Internet Explorer will temporarily
+ * insert the placeholder value as the textarea's content. If the placeholder value contains
+ * interpolation (`{{ ... }}`), an error will be logged in the console when AngularJS tries to update
+ * the value of the by-then-removed text node. This doesn't affect the functionality of the
+ * textarea, but can be undesirable.
+ *
+ * You can work around this Internet Explorer issue by using `ng-attr-placeholder` instead of
+ * `placeholder` on textareas, whenever you need interpolation in the placeholder value. You can
+ * find more details on `ngAttr` in the
+ * [Interpolation](guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes) section of the
+ * Developer Guide.
*/
@@ -24416,7 +19282,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`.
* </div>
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {boolean=} ngRequired Sets `required` attribute if set to true
@@ -24426,7 +19292,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
- * value does not match a RegExp found by evaluating the Angular expression given in the attribute value.
+ * value does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -24434,9 +19300,9 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
* start at the index of the last search's match, thus not taking the whole input value into
* account.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
- * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
* This parameter is ignored for input[type=password] controls, which will never trim the
* input.
*
@@ -24555,28 +19421,70 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
}];
+var hiddenInputBrowserCacheDirective = function() {
+ var valueProperty = {
+ configurable: true,
+ enumerable: false,
+ get: function() {
+ return this.getAttribute('value') || '';
+ },
+ set: function(val) {
+ this.setAttribute('value', val);
+ }
+ };
+
+ return {
+ restrict: 'E',
+ priority: 200,
+ compile: function(_, attr) {
+ if (lowercase(attr.type) !== 'hidden') {
+ return;
+ }
+
+ return {
+ pre: function(scope, element, attr, ctrls) {
+ var node = element[0];
+
+ // Support: Edge
+ // Moving the DOM around prevents autofillling
+ if (node.parentNode) {
+ node.parentNode.insertBefore(node, node.nextSibling);
+ }
+
+ // Support: FF, IE
+ // Avoiding direct assignment to .value prevents autofillling
+ if (Object.defineProperty) {
+ Object.defineProperty(node, 'value', valueProperty);
+ }
+ }
+ };
+ }
+ };
+};
+
+
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
/**
* @ngdoc directive
* @name ngValue
+ * @restrict A
+ * @priority 100
*
* @description
- * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`},
- * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to
- * the bound value.
+ * Binds the given expression to the value of the element.
*
- * `ngValue` is useful when dynamically generating lists of radio buttons using
- * {@link ngRepeat `ngRepeat`}, as shown below.
+ * It is mainly used on {@link input[radio] `input[radio]`} and option elements,
+ * so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its
+ * {@link select `select`} parent element) is set to the bound value. It is especially useful
+ * for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below.
*
- * Likewise, `ngValue` can be used to generate `<option>` elements for
- * the {@link select `select`} element. In that case however, only strings are supported
- * for the `value `attribute, so the resulting `ngModel` will always be a string.
- * Support for `select` models with non-string values is available via `ngOptions`.
+ * It can also be used to achieve one-way binding of a given expression to an input element
+ * such as an `input[text]` or a `textarea`, when that element does not use ngModel.
*
- * @element input
- * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
- * of the `input` element
+ * @element ANY
+ * @param {string=} ngValue AngularJS expression, whose value will be bound to the `value` attribute
+ * and `value` property of the element.
*
* @example
<example name="ngValue-directive" module="valueExample">
@@ -24615,18 +19523,33 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
</example>
*/
var ngValueDirective = function() {
+ /**
+ * inputs use the value attribute as their default value if the value property is not set.
+ * Once the value property has been set (by adding input), it will not react to changes to
+ * the value attribute anymore. Setting both attribute and property fixes this behavior, and
+ * makes it possible to use ngValue as a sort of one-way bind.
+ */
+ function updateElementValue(element, attr, value) {
+ // Support: IE9 only
+ // In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`).
+ var propValue = isDefined(value) ? value : (msie === 9) ? '' : null;
+ element.prop('value', propValue);
+ attr.$set('value', value);
+ }
+
return {
restrict: 'A',
priority: 100,
compile: function(tpl, tplAttr) {
if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
return function ngValueConstantLink(scope, elm, attr) {
- attr.$set('value', scope.$eval(attr.ngValue));
+ var value = scope.$eval(attr.ngValue);
+ updateElementValue(elm, attr, value);
};
} else {
return function ngValueLink(scope, elm, attr) {
scope.$watch(attr.ngValue, function valueWatchAction(value) {
- attr.$set('value', value);
+ updateElementValue(elm, attr, value);
});
};
}
@@ -24634,3693 +19557,67 @@ var ngValueDirective = function() {
};
};
-/**
- * @ngdoc directive
- * @name ngBind
- * @restrict AC
- *
- * @description
- * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element
- * with the value of a given expression, and to update the text content when the value of that
- * expression changes.
- *
- * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like
- * `{{ expression }}` which is similar but less verbose.
- *
- * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily
- * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an
- * element attribute, it makes the bindings invisible to the user while the page is loading.
- *
- * An alternative solution to this problem would be using the
- * {@link ng.directive:ngCloak ngCloak} directive.
- *
- *
- * @element ANY
- * @param {expression} ngBind {@link guide/expression Expression} to evaluate.
- *
- * @example
- * Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
- <example module="bindExample">
- <file name="index.html">
- <script>
- angular.module('bindExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.name = 'Whirled';
- }]);
- </script>
- <div ng-controller="ExampleController">
- <label>Enter name: <input type="text" ng-model="name"></label><br>
- Hello <span ng-bind="name"></span>!
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-bind', function() {
- var nameInput = element(by.model('name'));
-
- expect(element(by.binding('name')).getText()).toBe('Whirled');
- nameInput.clear();
- nameInput.sendKeys('world');
- expect(element(by.binding('name')).getText()).toBe('world');
- });
- </file>
- </example>
- */
-var ngBindDirective = ['$compile', function($compile) {
- return {
- 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) {
- element.textContent = isUndefined(value) ? '' : value;
- });
- };
- }
- };
-}];
-
-
-/**
- * @ngdoc directive
- * @name ngBindTemplate
- *
- * @description
- * The `ngBindTemplate` directive specifies that the element
- * text content should be replaced with the interpolation of the template
- * in the `ngBindTemplate` attribute.
- * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}`
- * expressions. This directive is needed since some HTML elements
- * (such as TITLE and OPTION) cannot contain SPAN elements.
- *
- * @element ANY
- * @param {string} ngBindTemplate template of form
- * <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval.
- *
- * @example
- * Try it here: enter text in text box and watch the greeting change.
- <example module="bindExample">
- <file name="index.html">
- <script>
- angular.module('bindExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.salutation = 'Hello';
- $scope.name = 'World';
- }]);
- </script>
- <div ng-controller="ExampleController">
- <label>Salutation: <input type="text" ng-model="salutation"></label><br>
- <label>Name: <input type="text" ng-model="name"></label><br>
- <pre ng-bind-template="{{salutation}} {{name}}!"></pre>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-bind', function() {
- var salutationElem = element(by.binding('salutation'));
- var salutationInput = element(by.model('salutation'));
- var nameInput = element(by.model('name'));
-
- expect(salutationElem.getText()).toBe('Hello World!');
-
- salutationInput.clear();
- salutationInput.sendKeys('Greetings');
- nameInput.clear();
- nameInput.sendKeys('user');
-
- expect(salutationElem.getText()).toBe('Greetings user!');
- });
- </file>
- </example>
- */
-var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) {
- return {
- 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.textContent = isUndefined(value) ? '' : value;
- });
- };
- }
- };
-}];
-
-
-/**
- * @ngdoc directive
- * @name ngBindHtml
- *
- * @description
- * Evaluates the expression and inserts the resulting HTML into the element in a secure way. By default,
- * the resulting HTML 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). 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#show-me-an-example-using-sce- 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.)
- *
- * @element ANY
- * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate.
- *
- * @example
-
- <example module="bindHtmlExample" deps="angular-sanitize.js">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <p ng-bind-html="myHTML"></p>
- </div>
- </file>
-
- <file name="script.js">
- angular.module('bindHtmlExample', ['ngSanitize'])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.myHTML =
- 'I am an <code>HTML</code>string with ' +
- '<a href="#">links!</a> and other <em>stuff</em>';
- }]);
- </file>
-
- <file name="protractor.js" type="protractor">
- it('should check ng-bind-html', function() {
- expect(element(by.binding('myHTML')).getText()).toBe(
- 'I am an HTMLstring with links! and other stuff');
- });
- </file>
- </example>
- */
-var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) {
- return {
- restrict: 'A',
- compile: function ngBindHtmlCompile(tElement, tAttrs) {
- var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
- var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) {
- // Unwrap the value to compare the actual inner safe value, not the wrapper object.
- return $sce.valueOf(val);
- });
- $compile.$$addBindingClass(tElement);
-
- return function ngBindHtmlLink(scope, element, attr) {
- $compile.$$addBindingInfo(element, attr.ngBindHtml);
-
- scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
- // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter.
- var value = ngBindHtmlGetter(scope);
- element.html($sce.getTrustedHtml(value) || '');
- });
- };
- }
- };
-}];
-
-/**
- * @ngdoc directive
- * @name ngChange
- *
- * @description
- * Evaluate the given expression when the user changes the input.
- * The expression is evaluated immediately, unlike the JavaScript onchange event
- * which only triggers at the end of a change (usually, when the user leaves the
- * form element or presses the return key).
- *
- * The `ngChange` expression is only evaluated when a change in the input value causes
- * a new value to be committed to the model.
- *
- * It will not be evaluated:
- * * if the value returned from the `$parsers` transformation pipeline has not changed
- * * if the input has continued to be invalid since the model will stay `null`
- * * if the model is changed programmatically and not by a change to the input value
- *
- *
- * Note, this directive requires `ngModel` to be present.
- *
- * @element input
- * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
- * in input value.
- *
- * @example
- * <example name="ngChange-directive" module="changeExample">
- * <file name="index.html">
- * <script>
- * angular.module('changeExample', [])
- * .controller('ExampleController', ['$scope', function($scope) {
- * $scope.counter = 0;
- * $scope.change = function() {
- * $scope.counter++;
- * };
- * }]);
- * </script>
- * <div ng-controller="ExampleController">
- * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" />
- * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" />
- * <label for="ng-change-example2">Confirmed</label><br />
- * <tt>debug = {{confirmed}}</tt><br/>
- * <tt>counter = {{counter}}</tt><br/>
- * </div>
- * </file>
- * <file name="protractor.js" type="protractor">
- * var counter = element(by.binding('counter'));
- * var debug = element(by.binding('confirmed'));
- *
- * it('should evaluate the expression if changing from view', function() {
- * expect(counter.getText()).toContain('0');
- *
- * element(by.id('ng-change-example1')).click();
- *
- * expect(counter.getText()).toContain('1');
- * expect(debug.getText()).toContain('true');
- * });
- *
- * it('should not evaluate the expression if changing from model', function() {
- * element(by.id('ng-change-example2')).click();
-
- * expect(counter.getText()).toContain('0');
- * expect(debug.getText()).toContain('true');
- * });
- * </file>
- * </example>
- */
-var ngChangeDirective = valueFn({
- restrict: 'A',
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- ctrl.$viewChangeListeners.push(function() {
- scope.$eval(attr.ngChange);
- });
- }
-});
-
-function classDirective(name, selector) {
- name = 'ngClass' + name;
- return ['$animate', function($animate) {
- return {
- restrict: 'AC',
- link: function(scope, element, attr) {
- var oldVal;
-
- scope.$watch(attr[name], ngClassWatchAction, true);
-
- attr.$observe('class', function(value) {
- ngClassWatchAction(scope.$eval(attr[name]));
- });
-
-
- if (name !== 'ngClass') {
- scope.$watch('$index', function($index, old$index) {
- // jshint bitwise: false
- var mod = $index & 1;
- if (mod !== (old$index & 1)) {
- var classes = arrayClasses(scope.$eval(attr[name]));
- mod === selector ?
- addClasses(classes) :
- removeClasses(classes);
- }
- });
- }
-
- function addClasses(classes) {
- var newClasses = digestClassCounts(classes, 1);
- attr.$addClass(newClasses);
- }
-
- function removeClasses(classes) {
- var newClasses = digestClassCounts(classes, -1);
- attr.$removeClass(newClasses);
- }
-
- function digestClassCounts(classes, count) {
- // Use createMap() to prevent class assumptions involving property
- // names in Object.prototype
- var classCounts = element.data('$classCounts') || createMap();
- var classesToUpdate = [];
- forEach(classes, function(className) {
- if (count > 0 || classCounts[className]) {
- classCounts[className] = (classCounts[className] || 0) + count;
- if (classCounts[className] === +(count > 0)) {
- classesToUpdate.push(className);
- }
- }
- });
- element.data('$classCounts', classCounts);
- return classesToUpdate.join(' ');
- }
-
- function updateClasses(oldClasses, newClasses) {
- var toAdd = arrayDifference(newClasses, oldClasses);
- var toRemove = arrayDifference(oldClasses, newClasses);
- toAdd = digestClassCounts(toAdd, 1);
- toRemove = digestClassCounts(toRemove, -1);
- if (toAdd && toAdd.length) {
- $animate.addClass(element, toAdd);
- }
- if (toRemove && toRemove.length) {
- $animate.removeClass(element, toRemove);
- }
- }
-
- function ngClassWatchAction(newVal) {
- // jshint bitwise: false
- if (selector === true || (scope.$index & 1) === selector) {
- // jshint bitwise: true
- var newClasses = arrayClasses(newVal || []);
- if (!oldVal) {
- addClasses(newClasses);
- } else if (!equals(newVal,oldVal)) {
- var oldClasses = arrayClasses(oldVal);
- updateClasses(oldClasses, newClasses);
- }
- }
- if (isArray(newVal)) {
- oldVal = newVal.map(function(v) { return shallowCopy(v); });
- } else {
- oldVal = shallowCopy(newVal);
- }
- }
- }
- };
-
- function arrayDifference(tokens1, tokens2) {
- var values = [];
-
- outer:
- for (var i = 0; i < tokens1.length; i++) {
- var token = tokens1[i];
- for (var j = 0; j < tokens2.length; j++) {
- if (token == tokens2[j]) continue outer;
- }
- values.push(token);
- }
- return values;
- }
-
- function arrayClasses(classVal) {
- var classes = [];
- if (isArray(classVal)) {
- forEach(classVal, function(v) {
- classes = classes.concat(arrayClasses(v));
- });
- return classes;
- } else if (isString(classVal)) {
- return classVal.split(' ');
- } else if (isObject(classVal)) {
- forEach(classVal, function(v, k) {
- if (v) {
- classes = classes.concat(k.split(' '));
- }
- });
- return classes;
- }
- return classVal;
- }
- }];
-}
-
-/**
- * @ngdoc directive
- * @name ngClass
- * @restrict AC
- *
- * @description
- * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding
- * an expression that represents all classes to be added.
- *
- * The directive operates in three different ways, depending on which of three types the expression
- * evaluates to:
- *
- * 1. If the expression evaluates to a string, the string should be one or more space-delimited class
- * names.
- *
- * 2. If the expression evaluates to an object, then for each key-value pair of the
- * object with a truthy value the corresponding key is used as a class name.
- *
- * 3. If the expression evaluates to an array, each element of the array should either be a string as in
- * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array
- * to give you more control over what CSS classes appear. See the code below for an example of this.
- *
- *
- * The directive won't add duplicate classes if a particular class was already set.
- *
- * When the expression changes, the previously added classes are removed and only then are the
- * new classes added.
- *
- * @knownIssue
- * You should not use {@link guide/interpolation interpolation} in the value of the `class`
- * attribute, when using the `ngClass` directive on the same element.
- * See {@link guide/interpolation#known-issues here} for more info.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
- * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
- *
- * @element ANY
- * @param {expression} ngClass {@link guide/expression Expression} to eval. The result
- * of the evaluation can be a string representing space delimited class
- * names, an array, or a map of class names to boolean values. In the case of a map, the
- * names of the properties whose values are truthy will be added as css classes to the
- * element.
- *
- * @example Example that demonstrates basic bindings via ngClass directive.
- <example>
- <file name="index.html">
- <p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p>
- <label>
- <input type="checkbox" ng-model="deleted">
- deleted (apply "strike" class)
- </label><br>
- <label>
- <input type="checkbox" ng-model="important">
- important (apply "bold" class)
- </label><br>
- <label>
- <input type="checkbox" ng-model="error">
- error (apply "has-error" class)
- </label>
- <hr>
- <p ng-class="style">Using String Syntax</p>
- <input type="text" ng-model="style"
- placeholder="Type: bold strike red" aria-label="Type: bold strike red">
- <hr>
- <p ng-class="[style1, style2, style3]">Using Array Syntax</p>
- <input ng-model="style1"
- placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red"><br>
- <input ng-model="style2"
- placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 2"><br>
- <input ng-model="style3"
- placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 3"><br>
- <hr>
- <p ng-class="[style4, {orange: warning}]">Using Array and Map Syntax</p>
- <input ng-model="style4" placeholder="Type: bold, strike" aria-label="Type: bold, strike"><br>
- <label><input type="checkbox" ng-model="warning"> warning (apply "orange" class)</label>
- </file>
- <file name="style.css">
- .strike {
- text-decoration: line-through;
- }
- .bold {
- font-weight: bold;
- }
- .red {
- color: red;
- }
- .has-error {
- color: red;
- background-color: yellow;
- }
- .orange {
- color: orange;
- }
- </file>
- <file name="protractor.js" type="protractor">
- var ps = element.all(by.css('p'));
-
- it('should let you toggle the class', function() {
-
- expect(ps.first().getAttribute('class')).not.toMatch(/bold/);
- expect(ps.first().getAttribute('class')).not.toMatch(/has-error/);
-
- element(by.model('important')).click();
- expect(ps.first().getAttribute('class')).toMatch(/bold/);
-
- element(by.model('error')).click();
- expect(ps.first().getAttribute('class')).toMatch(/has-error/);
- });
-
- it('should let you toggle string example', function() {
- expect(ps.get(1).getAttribute('class')).toBe('');
- element(by.model('style')).clear();
- element(by.model('style')).sendKeys('red');
- expect(ps.get(1).getAttribute('class')).toBe('red');
- });
-
- it('array example should have 3 classes', function() {
- expect(ps.get(2).getAttribute('class')).toBe('');
- element(by.model('style1')).sendKeys('bold');
- element(by.model('style2')).sendKeys('strike');
- element(by.model('style3')).sendKeys('red');
- expect(ps.get(2).getAttribute('class')).toBe('bold strike red');
- });
-
- it('array with map example should have 2 classes', function() {
- expect(ps.last().getAttribute('class')).toBe('');
- element(by.model('style4')).sendKeys('bold');
- element(by.model('warning')).click();
- expect(ps.last().getAttribute('class')).toBe('bold orange');
- });
- </file>
- </example>
-
- ## Animations
-
- The example below demonstrates how to perform animations using ngClass.
-
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
- <file name="index.html">
- <input id="setbtn" type="button" value="set" ng-click="myVar='my-class'">
- <input id="clearbtn" type="button" value="clear" ng-click="myVar=''">
- <br>
- <span class="base-class" ng-class="myVar">Sample Text</span>
- </file>
- <file name="style.css">
- .base-class {
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- }
-
- .base-class.my-class {
- color: red;
- font-size:3em;
- }
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-class', function() {
- expect(element(by.css('.base-class')).getAttribute('class')).not.
- toMatch(/my-class/);
-
- element(by.id('setbtn')).click();
-
- expect(element(by.css('.base-class')).getAttribute('class')).
- toMatch(/my-class/);
-
- element(by.id('clearbtn')).click();
-
- expect(element(by.css('.base-class')).getAttribute('class')).not.
- toMatch(/my-class/);
- });
- </file>
- </example>
-
-
- ## ngClass and pre-existing CSS3 Transitions/Animations
- The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
- Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder
- any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure
- to view the step by step details of {@link $animate#addClass $animate.addClass} and
- {@link $animate#removeClass $animate.removeClass}.
- */
-var ngClassDirective = classDirective('', true);
-
-/**
- * @ngdoc directive
- * @name ngClassOdd
- * @restrict AC
- *
- * @description
- * The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except they work in
- * conjunction with `ngRepeat` and take effect only on odd (even) rows.
- *
- * This directive can be applied only within the scope of an
- * {@link ng.directive:ngRepeat ngRepeat}.
- *
- * @element ANY
- * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result
- * of the evaluation can be a string representing space delimited class names or an array.
- *
- * @example
- <example>
- <file name="index.html">
- <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']">
- <li ng-repeat="name in names">
- <span ng-class-odd="'odd'" ng-class-even="'even'">
- {{name}}
- </span>
- </li>
- </ol>
- </file>
- <file name="style.css">
- .odd {
- color: red;
- }
- .even {
- color: blue;
- }
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-class-odd and ng-class-even', function() {
- expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
- toMatch(/odd/);
- expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
- toMatch(/even/);
- });
- </file>
- </example>
- */
-var ngClassOddDirective = classDirective('Odd', 0);
-
-/**
- * @ngdoc directive
- * @name ngClassEven
- * @restrict AC
- *
- * @description
- * The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except they work in
- * conjunction with `ngRepeat` and take effect only on odd (even) rows.
- *
- * This directive can be applied only within the scope of an
- * {@link ng.directive:ngRepeat ngRepeat}.
- *
- * @element ANY
- * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The
- * result of the evaluation can be a string representing space delimited class names or an array.
- *
- * @example
- <example>
- <file name="index.html">
- <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']">
- <li ng-repeat="name in names">
- <span ng-class-odd="'odd'" ng-class-even="'even'">
- {{name}} &nbsp; &nbsp; &nbsp;
- </span>
- </li>
- </ol>
- </file>
- <file name="style.css">
- .odd {
- color: red;
- }
- .even {
- color: blue;
- }
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-class-odd and ng-class-even', function() {
- expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
- toMatch(/odd/);
- expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
- toMatch(/even/);
- });
- </file>
- </example>
- */
-var ngClassEvenDirective = classDirective('Even', 1);
-
-/**
- * @ngdoc directive
- * @name ngCloak
- * @restrict AC
- *
- * @description
- * The `ngCloak` directive is used to prevent the Angular html template from being briefly
- * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this
- * directive to avoid the undesirable flicker effect caused by the html template display.
- *
- * The directive can be applied to the `<body>` element, but the preferred usage is to apply
- * multiple `ngCloak` directives to small portions of the page to permit progressive rendering
- * of the browser view.
- *
- * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and
- * `angular.min.js`.
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
- *
- * ```css
- * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
- * display: none !important;
- * }
- * ```
- *
- * When this css rule is loaded by the browser, all html elements (including their children) that
- * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive
- * during the compilation of the template it deletes the `ngCloak` element attribute, making
- * the compiled element visible.
- *
- * For the best result, the `angular.js` script must be loaded in the head section of the html
- * document; alternatively, the css rule above must be included in the external stylesheet of the
- * application.
- *
- * @element ANY
- *
- * @example
- <example>
- <file name="index.html">
- <div id="template1" ng-cloak>{{ 'hello' }}</div>
- <div id="template2" class="ng-cloak">{{ 'world' }}</div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should remove the template directive and css class', function() {
- expect($('#template1').getAttribute('ng-cloak')).
- toBeNull();
- expect($('#template2').getAttribute('ng-cloak')).
- toBeNull();
- });
- </file>
- </example>
- *
- */
-var ngCloakDirective = ngDirective({
- compile: function(element, attr) {
- attr.$set('ngCloak', undefined);
- element.removeClass('ng-cloak');
- }
-});
-
-/**
- * @ngdoc directive
- * @name ngController
- *
- * @description
- * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular
- * supports the principles behind the Model-View-Controller design pattern.
- *
- * MVC components in angular:
- *
- * * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties
- * are accessed through bindings.
- * * View — The template (HTML with data bindings) that is rendered into the View.
- * * Controller — The `ngController` directive specifies a Controller class; the class contains business
- * logic behind the application to decorate the scope with functions and values
- *
- * Note that you can also attach controllers to the DOM by declaring it in a route definition
- * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller
- * again using `ng-controller` in the template itself. This will cause the controller to be attached
- * and executed twice.
- *
- * @element ANY
- * @scope
- * @priority 500
- * @param {expression} ngController Name of a constructor function registered with the current
- * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression}
- * that on the current scope evaluates to a constructor function.
- *
- * The controller instance can be published into a scope property by specifying
- * `ng-controller="as propertyName"`.
- *
- * If the current `$controllerProvider` is configured to use globals (via
- * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may
- * also be the name of a globally accessible constructor function (not recommended).
- *
- * @example
- * Here is a simple form for editing user contact information. Adding, removing, clearing, and
- * greeting are methods declared on the controller (see source tab). These methods can
- * easily be called from the angular markup. Any changes to the data are automatically reflected
- * in the View without the need for a manual update.
- *
- * Two different declaration styles are included below:
- *
- * * one binds methods and properties directly onto the controller using `this`:
- * `ng-controller="SettingsController1 as settings"`
- * * one injects `$scope` into the controller:
- * `ng-controller="SettingsController2"`
- *
- * The second option is more common in the Angular community, and is generally used in boilerplates
- * and in this guide. However, there are advantages to binding properties directly to the controller
- * and avoiding scope.
- *
- * * Using `controller as` makes it obvious which controller you are accessing in the template when
- * multiple controllers apply to an element.
- * * If you are writing your controllers as classes you have easier access to the properties and
- * methods, which will appear on the scope, from inside the controller code.
- * * Since there is always a `.` in the bindings, you don't have to worry about prototypal
- * inheritance masking primitives.
- *
- * This example demonstrates the `controller as` syntax.
- *
- * <example name="ngControllerAs" module="controllerAsExample">
- * <file name="index.html">
- * <div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings">
- * <label>Name: <input type="text" ng-model="settings.name"/></label>
- * <button ng-click="settings.greet()">greet</button><br/>
- * Contact:
- * <ul>
- * <li ng-repeat="contact in settings.contacts">
- * <select ng-model="contact.type" aria-label="Contact method" id="select_{{$index}}">
- * <option>phone</option>
- * <option>email</option>
- * </select>
- * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
- * <button ng-click="settings.clearContact(contact)">clear</button>
- * <button ng-click="settings.removeContact(contact)" aria-label="Remove">X</button>
- * </li>
- * <li><button ng-click="settings.addContact()">add</button></li>
- * </ul>
- * </div>
- * </file>
- * <file name="app.js">
- * angular.module('controllerAsExample', [])
- * .controller('SettingsController1', SettingsController1);
- *
- * function SettingsController1() {
- * this.name = "John Smith";
- * this.contacts = [
- * {type: 'phone', value: '408 555 1212'},
- * {type: 'email', value: 'john.smith@example.org'} ];
- * }
- *
- * SettingsController1.prototype.greet = function() {
- * alert(this.name);
- * };
- *
- * SettingsController1.prototype.addContact = function() {
- * this.contacts.push({type: 'email', value: 'yourname@example.org'});
- * };
- *
- * SettingsController1.prototype.removeContact = function(contactToRemove) {
- * var index = this.contacts.indexOf(contactToRemove);
- * this.contacts.splice(index, 1);
- * };
- *
- * SettingsController1.prototype.clearContact = function(contact) {
- * contact.type = 'phone';
- * contact.value = '';
- * };
- * </file>
- * <file name="protractor.js" type="protractor">
- * it('should check controller as', function() {
- * var container = element(by.id('ctrl-as-exmpl'));
- * expect(container.element(by.model('settings.name'))
- * .getAttribute('value')).toBe('John Smith');
- *
- * var firstRepeat =
- * container.element(by.repeater('contact in settings.contacts').row(0));
- * var secondRepeat =
- * container.element(by.repeater('contact in settings.contacts').row(1));
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('408 555 1212');
- *
- * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('john.smith@example.org');
- *
- * firstRepeat.element(by.buttonText('clear')).click();
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('');
- *
- * container.element(by.buttonText('add')).click();
- *
- * expect(container.element(by.repeater('contact in settings.contacts').row(2))
- * .element(by.model('contact.value'))
- * .getAttribute('value'))
- * .toBe('yourname@example.org');
- * });
- * </file>
- * </example>
- *
- * This example demonstrates the "attach to `$scope`" style of controller.
- *
- * <example name="ngController" module="controllerExample">
- * <file name="index.html">
- * <div id="ctrl-exmpl" ng-controller="SettingsController2">
- * <label>Name: <input type="text" ng-model="name"/></label>
- * <button ng-click="greet()">greet</button><br/>
- * Contact:
- * <ul>
- * <li ng-repeat="contact in contacts">
- * <select ng-model="contact.type" id="select_{{$index}}">
- * <option>phone</option>
- * <option>email</option>
- * </select>
- * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
- * <button ng-click="clearContact(contact)">clear</button>
- * <button ng-click="removeContact(contact)">X</button>
- * </li>
- * <li>[ <button ng-click="addContact()">add</button> ]</li>
- * </ul>
- * </div>
- * </file>
- * <file name="app.js">
- * angular.module('controllerExample', [])
- * .controller('SettingsController2', ['$scope', SettingsController2]);
- *
- * function SettingsController2($scope) {
- * $scope.name = "John Smith";
- * $scope.contacts = [
- * {type:'phone', value:'408 555 1212'},
- * {type:'email', value:'john.smith@example.org'} ];
- *
- * $scope.greet = function() {
- * alert($scope.name);
- * };
- *
- * $scope.addContact = function() {
- * $scope.contacts.push({type:'email', value:'yourname@example.org'});
- * };
- *
- * $scope.removeContact = function(contactToRemove) {
- * var index = $scope.contacts.indexOf(contactToRemove);
- * $scope.contacts.splice(index, 1);
- * };
- *
- * $scope.clearContact = function(contact) {
- * contact.type = 'phone';
- * contact.value = '';
- * };
- * }
- * </file>
- * <file name="protractor.js" type="protractor">
- * it('should check controller', function() {
- * var container = element(by.id('ctrl-exmpl'));
- *
- * expect(container.element(by.model('name'))
- * .getAttribute('value')).toBe('John Smith');
- *
- * var firstRepeat =
- * container.element(by.repeater('contact in contacts').row(0));
- * var secondRepeat =
- * container.element(by.repeater('contact in contacts').row(1));
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('408 555 1212');
- * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('john.smith@example.org');
- *
- * firstRepeat.element(by.buttonText('clear')).click();
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('');
- *
- * container.element(by.buttonText('add')).click();
- *
- * expect(container.element(by.repeater('contact in contacts').row(2))
- * .element(by.model('contact.value'))
- * .getAttribute('value'))
- * .toBe('yourname@example.org');
- * });
- * </file>
- *</example>
-
- */
-var ngControllerDirective = [function() {
- return {
- restrict: 'A',
- scope: true,
- controller: '@',
- priority: 500
- };
-}];
-
-/**
- * @ngdoc directive
- * @name ngCsp
- *
- * @element html
- * @description
- *
- * Angular has some features that can break certain
- * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
- *
- * If you intend to implement these rules then you must tell Angular not to use these features.
- *
- * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
- *
- *
- * The following rules affect Angular:
- *
- * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions
- * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30%
- * increase in the speed of evaluating Angular expressions.
- *
- * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular
- * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}).
- * To make these directives work when a CSP rule is blocking inline styles, you must link to the
- * `angular-csp.css` in your HTML manually.
- *
- * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval
- * and automatically deactivates this feature in the {@link $parse} service. This autodetection,
- * however, triggers a CSP error to be logged in the console:
- *
- * ```
- * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of
- * script in the following Content Security Policy directive: "default-src 'self'". Note that
- * 'script-src' was not explicitly set, so 'default-src' is used as a fallback.
- * ```
- *
- * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp`
- * directive on an element of the HTML document that appears before the `<script>` tag that loads
- * the `angular.js` file.
- *
- * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.*
- *
- * You can specify which of the CSP related Angular features should be deactivated by providing
- * a value for the `ng-csp` attribute. The options are as follows:
- *
- * * no-inline-style: this stops Angular from injecting CSS styles into the DOM
- *
- * * no-unsafe-eval: this stops Angular from optimizing $parse with unsafe eval of strings
- *
- * You can use these values in the following combinations:
- *
- *
- * * No declaration means that Angular will assume that you can do inline styles, but it will do
- * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous versions
- * of Angular.
- *
- * * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline
- * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions
- * of Angular.
- *
- * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject
- * inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
- *
- * * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
- * run eval - no automatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
- *
- * * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
- * styles nor use eval, which is the same as an empty: ng-csp.
- * E.g.`<body ng-csp="no-inline-style;no-unsafe-eval">`
- *
- * @example
- * This example shows how to apply the `ngCsp` directive to the `html` tag.
- ```html
- <!doctype html>
- <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() fn that looks for the `ng-csp` attribute anywhere in the current doc
-
-/**
- * @ngdoc directive
- * @name ngClick
- *
- * @description
- * The ngClick directive allows you to specify custom behavior when
- * an element is clicked.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon
- * click. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-click="count = count + 1" ng-init="count=0">
- Increment
- </button>
- <span>
- count: {{count}}
- </span>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-click', function() {
- expect(element(by.binding('count')).getText()).toMatch('0');
- element(by.css('button')).click();
- expect(element(by.binding('count')).getText()).toMatch('1');
- });
- </file>
- </example>
- */
-/*
- * A collection of directives that allows creation of custom event handlers that are defined as
- * angular expressions and are compiled and executed within the current scope.
- */
-var ngEventDirectives = {};
-
-// For events that might fire synchronously during DOM manipulation
-// we need to execute their event handlers asynchronously using $evalAsync,
-// so that they are not executed in an inconsistent state.
-var forceAsyncEvents = {
- 'blur': true,
- 'focus': true
-};
-forEach(
- 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
- function(eventName) {
- var directiveName = directiveNormalize('ng-' + eventName);
- ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
- return {
- restrict: 'A',
- compile: function($element, attr) {
- // We expose the powerful $event object on the scope that provides access to the Window,
- // etc. that isn't protected by the fast paths in $parse. We explicitly request better
- // checks at the cost of speed since event handler expressions are not executed as
- // frequently as regular change detection.
- var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
- return function ngEventHandler(scope, element) {
- element.on(eventName, function(event) {
- var callback = function() {
- fn(scope, {$event:event});
- };
- if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
- scope.$evalAsync(callback);
- } else {
- scope.$apply(callback);
- }
- });
- };
- }
- };
- }];
- }
-);
-
-/**
- * @ngdoc directive
- * @name ngDblclick
- *
- * @description
- * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon
- * a dblclick. (The Event object is available as `$event`)
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-dblclick="count = count + 1" ng-init="count=0">
- Increment (on double click)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMousedown
- *
- * @description
- * The ngMousedown directive allows you to specify custom behavior on mousedown event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon
- * mousedown. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mousedown="count = count + 1" ng-init="count=0">
- Increment (on mouse down)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMouseup
- *
- * @description
- * Specify custom behavior on mouseup event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon
- * mouseup. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseup="count = count + 1" ng-init="count=0">
- Increment (on mouse up)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngMouseover
- *
- * @description
- * Specify custom behavior on mouseover event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon
- * mouseover. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseover="count = count + 1" ng-init="count=0">
- Increment (when mouse is over)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMouseenter
- *
- * @description
- * Specify custom behavior on mouseenter event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon
- * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseenter="count = count + 1" ng-init="count=0">
- Increment (when mouse enters)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMouseleave
- *
- * @description
- * Specify custom behavior on mouseleave event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon
- * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mouseleave="count = count + 1" ng-init="count=0">
- Increment (when mouse leaves)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngMousemove
- *
- * @description
- * Specify custom behavior on mousemove event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon
- * mousemove. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <button ng-mousemove="count = count + 1" ng-init="count=0">
- Increment (when mouse moves)
- </button>
- count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngKeydown
- *
- * @description
- * Specify custom behavior on keydown event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon
- * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-keydown="count = count + 1" ng-init="count=0">
- key down count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngKeyup
- *
- * @description
- * Specify custom behavior on keyup event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon
- * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
- *
- * @example
- <example>
- <file name="index.html">
- <p>Typing in the input box below updates the key count</p>
- <input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}}
-
- <p>Typing in the input box below updates the keycode</p>
- <input ng-keyup="event=$event">
- <p>event keyCode: {{ event.keyCode }}</p>
- <p>event altKey: {{ event.altKey }}</p>
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngKeypress
- *
- * @description
- * Specify custom behavior on keypress event.
- *
- * @element ANY
- * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon
- * keypress. ({@link guide/expression#-event- Event object is available as `$event`}
- * and can be interrogated for keyCode, altKey, etc.)
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-keypress="count = count + 1" ng-init="count=0">
- key press count: {{count}}
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc directive
- * @name ngSubmit
- *
- * @description
- * Enables binding angular expressions to onsubmit events.
- *
- * Additionally it prevents the default action (which for form means sending the request to the
- * server and reloading the current page), but only if the form does not contain `action`,
- * `data-action`, or `x-action` attributes.
- *
- * <div class="alert alert-warning">
- * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and
- * `ngSubmit` handlers together. See the
- * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation}
- * for a detailed discussion of when `ngSubmit` may be triggered.
- * </div>
- *
- * @element form
- * @priority 0
- * @param {expression} ngSubmit {@link guide/expression Expression} to eval.
- * ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example module="submitExample">
- <file name="index.html">
- <script>
- angular.module('submitExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.list = [];
- $scope.text = 'hello';
- $scope.submit = function() {
- if ($scope.text) {
- $scope.list.push(this.text);
- $scope.text = '';
- }
- };
- }]);
- </script>
- <form ng-submit="submit()" ng-controller="ExampleController">
- Enter text and hit enter:
- <input type="text" ng-model="text" name="text" />
- <input type="submit" id="submit" value="Submit" />
- <pre>list={{list}}</pre>
- </form>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-submit', function() {
- expect(element(by.binding('list')).getText()).toBe('list=[]');
- element(by.css('#submit')).click();
- expect(element(by.binding('list')).getText()).toContain('hello');
- expect(element(by.model('text')).getAttribute('value')).toBe('');
- });
- it('should ignore empty strings', function() {
- expect(element(by.binding('list')).getText()).toBe('list=[]');
- element(by.css('#submit')).click();
- element(by.css('#submit')).click();
- expect(element(by.binding('list')).getText()).toContain('hello');
- });
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngFocus
- *
- * @description
- * Specify custom behavior on focus event.
- *
- * Note: As the `focus` event is executed synchronously when calling `input.focus()`
- * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
- * during an `$apply` to ensure a consistent state.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
- * focus. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-/**
- * @ngdoc directive
- * @name ngBlur
- *
- * @description
- * Specify custom behavior on blur event.
- *
- * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when
- * an element has lost focus.
- *
- * Note: As the `blur` event is executed synchronously also during DOM manipulations
- * (e.g. removing a focussed input),
- * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
- * during an `$apply` to ensure a consistent state.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon
- * blur. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-/**
- * @ngdoc directive
- * @name ngCopy
- *
- * @description
- * Specify custom behavior on copy event.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon
- * copy. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-copy="copied=true" ng-init="copied=false; value='copy me'" ng-model="value">
- copied: {{copied}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngCut
- *
- * @description
- * Specify custom behavior on cut event.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon
- * cut. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-cut="cut=true" ng-init="cut=false; value='cut me'" ng-model="value">
- cut: {{cut}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngPaste
- *
- * @description
- * Specify custom behavior on paste event.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon
- * paste. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
- <example>
- <file name="index.html">
- <input ng-paste="paste=true" ng-init="paste=false" placeholder='paste here'>
- pasted: {{paste}}
- </file>
- </example>
- */
-
-/**
- * @ngdoc directive
- * @name ngIf
- * @restrict A
- * @multiElement
- *
- * @description
- * The `ngIf` directive removes or recreates a portion of the DOM tree based on an
- * {expression}. If the expression assigned to `ngIf` evaluates to a false
- * value then the element is removed from the DOM, otherwise a clone of the
- * element is reinserted into the DOM.
- *
- * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the
- * element in the DOM rather than changing its visibility via the `display` css property. A common
- * case when this difference is significant is when using css selectors that rely on an element's
- * 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/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
- * is if an element's class attribute is directly modified after it's compiled, using something like
- * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element
- * the added class will be lost because the original compiled state is used to regenerate the element.
- *
- * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter`
- * and `leave` effects.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container |
- * | {@link ng.$animate#leave leave} | 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
- * the element is removed from the DOM tree. If it is truthy a copy of the compiled
- * element is added to the DOM tree.
- *
- * @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
- <file name="index.html">
- <label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/>
- Show when checked:
- <span ng-if="checked" class="animate-if">
- This is removed when the checkbox is unchecked.
- </span>
- </file>
- <file name="animations.css">
- .animate-if {
- background:white;
- border:1px solid black;
- padding:10px;
- }
-
- .animate-if.ng-enter, .animate-if.ng-leave {
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- }
-
- .animate-if.ng-enter,
- .animate-if.ng-leave.ng-leave-active {
- opacity:0;
- }
-
- .animate-if.ng-leave,
- .animate-if.ng-enter.ng-enter-active {
- opacity:1;
- }
- </file>
- </example>
- */
-var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
- return {
- multiElement: true,
- transclude: 'element',
- priority: 600,
- terminal: true,
- restrict: 'A',
- $$tlb: true,
- link: function($scope, $element, $attr, ctrl, $transclude) {
- var block, childScope, previousElements;
- $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
-
- if (value) {
- if (!childScope) {
- $transclude(function(clone, newScope) {
- childScope = newScope;
- clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
- // Note: We only need the first/last node of the cloned nodes.
- // However, we need to keep the reference to the jqlite wrapper as it might be changed later
- // by a directive with templateUrl when its template arrives.
- block = {
- clone: clone
- };
- $animate.enter(clone, $element.parent(), $element);
- });
- }
- } else {
- if (previousElements) {
- previousElements.remove();
- previousElements = null;
- }
- if (childScope) {
- childScope.$destroy();
- childScope = null;
- }
- if (block) {
- previousElements = getBlockNodes(block.clone);
- $animate.leave(previousElements).then(function() {
- previousElements = null;
- });
- block = null;
- }
- }
- });
- }
- };
-}];
-
-/**
- * @ngdoc directive
- * @name ngInclude
- * @restrict ECA
- *
- * @description
- * Fetches, compiles and includes an external HTML fragment.
- *
- * By default, the template URL is restricted to the same domain and protocol as the
- * application document. This is done by calling {@link $sce#getTrustedResourceUrl
- * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols
- * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or
- * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link
- * ng.$sce Strict Contextual Escaping}.
- *
- * In addition, the browser's
- * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
- * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
- * policy may further restrict whether the template is successfully loaded.
- * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://`
- * access on some browsers.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link ng.$animate#enter enter} | when the expression changes, on the new include |
- * | {@link ng.$animate#leave leave} | when the expression changes, on the old include |
- *
- * The enter and leave animation occur concurrently.
- *
- * @scope
- * @priority 400
- *
- * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
- * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
- * @param {string=} onload Expression to evaluate when a new partial is loaded.
- * <div class="alert alert-warning">
- * **Note:** When using onload on SVG elements in IE11, the browser will try to call
- * a function with the name on the window element, which will usually throw a
- * "function is undefined" error. To fix this, you can instead use `data-onload` or a
- * different form that {@link guide/directive#normalization matches} `onload`.
- * </div>
- *
- * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
- * $anchorScroll} to scroll the viewport after the content is loaded.
- *
- * - If the attribute is not set, disable scrolling.
- * - If the attribute is set without value, enable scrolling.
- * - Otherwise enable scrolling only if the expression evaluates to truthy value.
- *
- * @example
- <example module="includeExample" deps="angular-animate.js" animations="true">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <select ng-model="template" ng-options="t.name for t in templates">
- <option value="">(blank)</option>
- </select>
- url of the template: <code>{{template.url}}</code>
- <hr/>
- <div class="slide-animate-container">
- <div class="slide-animate" ng-include="template.url"></div>
- </div>
- </div>
- </file>
- <file name="script.js">
- angular.module('includeExample', ['ngAnimate'])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.templates =
- [ { name: 'template1.html', url: 'template1.html'},
- { name: 'template2.html', url: 'template2.html'} ];
- $scope.template = $scope.templates[0];
- }]);
- </file>
- <file name="template1.html">
- Content of template1.html
- </file>
- <file name="template2.html">
- Content of template2.html
- </file>
- <file name="animations.css">
- .slide-animate-container {
- position:relative;
- background:white;
- border:1px solid black;
- height:40px;
- overflow:hidden;
- }
-
- .slide-animate {
- padding:10px;
- }
-
- .slide-animate.ng-enter, .slide-animate.ng-leave {
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-
- position:absolute;
- top:0;
- left:0;
- right:0;
- bottom:0;
- display:block;
- padding:10px;
- }
-
- .slide-animate.ng-enter {
- top:-50px;
- }
- .slide-animate.ng-enter.ng-enter-active {
- top:0;
- }
-
- .slide-animate.ng-leave {
- top:0;
- }
- .slide-animate.ng-leave.ng-leave-active {
- top:50px;
- }
- </file>
- <file name="protractor.js" type="protractor">
- var templateSelect = element(by.model('template'));
- var includeElem = element(by.css('[ng-include]'));
-
- it('should load template1.html', function() {
- expect(includeElem.getText()).toMatch(/Content of template1.html/);
- });
-
- it('should load template2.html', function() {
- if (browser.params.browser == 'firefox') {
- // Firefox can't handle using selects
- // See https://github.com/angular/protractor/issues/480
- return;
- }
- templateSelect.click();
- templateSelect.all(by.css('option')).get(2).click();
- expect(includeElem.getText()).toMatch(/Content of template2.html/);
- });
-
- it('should change to blank', function() {
- if (browser.params.browser == 'firefox') {
- // Firefox can't handle using selects
- return;
- }
- templateSelect.click();
- templateSelect.all(by.css('option')).get(0).click();
- expect(includeElem.isPresent()).toBe(false);
- });
- </file>
- </example>
- */
-
-
-/**
- * @ngdoc event
- * @name ngInclude#$includeContentRequested
- * @eventType emit on the scope ngInclude was declared in
- * @description
- * Emitted every time the ngInclude content is requested.
- *
- * @param {Object} angularEvent Synthetic event object.
- * @param {String} src URL of content to load.
- */
-
-
-/**
- * @ngdoc event
- * @name ngInclude#$includeContentLoaded
- * @eventType emit on the current ngInclude scope
- * @description
- * Emitted every time the ngInclude content is reloaded.
- *
- * @param {Object} angularEvent Synthetic event object.
- * @param {String} src URL of content to load.
- */
-
-
-/**
- * @ngdoc event
- * @name ngInclude#$includeContentError
- * @eventType emit on the scope ngInclude was declared in
- * @description
- * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299)
- *
- * @param {Object} angularEvent Synthetic event object.
- * @param {String} src URL of content to load.
- */
-var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
- function($templateRequest, $anchorScroll, $animate) {
- return {
- restrict: 'ECA',
- priority: 400,
- terminal: true,
- transclude: 'element',
- controller: angular.noop,
- compile: function(element, attr) {
- var srcExp = attr.ngInclude || attr.src,
- onloadExp = attr.onload || '',
- autoScrollExp = attr.autoscroll;
-
- return function(scope, $element, $attr, ctrl, $transclude) {
- var changeCounter = 0,
- currentScope,
- previousElement,
- currentElement;
-
- var cleanupLastIncludeContent = function() {
- if (previousElement) {
- previousElement.remove();
- previousElement = null;
- }
- if (currentScope) {
- currentScope.$destroy();
- currentScope = null;
- }
- if (currentElement) {
- $animate.leave(currentElement).then(function() {
- previousElement = null;
- });
- previousElement = currentElement;
- currentElement = null;
- }
- };
-
- scope.$watch(srcExp, function ngIncludeWatchAction(src) {
- var afterAnimation = function() {
- if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
- $anchorScroll();
- }
- };
- var thisChangeId = ++changeCounter;
-
- if (src) {
- //set the 2nd param to true to ignore the template request error so that the inner
- //contents and scope can be cleaned up.
- $templateRequest(src, true).then(function(response) {
- if (scope.$$destroyed) return;
-
- if (thisChangeId !== changeCounter) return;
- var newScope = scope.$new();
- ctrl.template = response;
-
- // Note: This will also link all children of ng-include that were contained in the original
- // html. If that content contains controllers, ... they could pollute/change the scope.
- // However, using ng-include on an element with additional content does not make sense...
- // Note: We can't remove them in the cloneAttchFn of $transclude as that
- // function is called before linking the content, which would apply child
- // directives to non existing elements.
- var clone = $transclude(newScope, function(clone) {
- cleanupLastIncludeContent();
- $animate.enter(clone, null, $element).then(afterAnimation);
- });
-
- currentScope = newScope;
- currentElement = clone;
-
- currentScope.$emit('$includeContentLoaded', src);
- scope.$eval(onloadExp);
- }, function() {
- if (scope.$$destroyed) return;
-
- if (thisChangeId === changeCounter) {
- cleanupLastIncludeContent();
- scope.$emit('$includeContentError', src);
- }
- });
- scope.$emit('$includeContentRequested', src);
- } else {
- cleanupLastIncludeContent();
- ctrl.template = null;
- }
- });
- };
- }
- };
-}];
-
-// This directive is called during the $transclude call of the first `ngInclude` directive.
-// It will replace and compile the content of the element with the loaded template.
-// We need this directive so that the element content is already filled when
-// the link function of another directive on the same element as ngInclude
-// is called.
-var ngIncludeFillContentDirective = ['$compile',
- function($compile) {
- return {
- restrict: 'ECA',
- priority: -400,
- require: 'ngInclude',
- link: function(scope, $element, $attr, ctrl) {
- if (toString.call($element[0]).match(/SVG/)) {
- // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
- // support innerHTML, so detect this here and try to generate the contents
- // specially.
- $element.empty();
- $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope,
- function namespaceAdaptedClone(clone) {
- $element.append(clone);
- }, {futureParentElement: $element});
- return;
- }
-
- $element.html(ctrl.template);
- $compile($element.contents())(scope);
- }
- };
- }];
-
-/**
- * @ngdoc directive
- * @name ngInit
- * @restrict AC
- *
- * @description
- * The `ngInit` directive allows you to evaluate an expression in the
- * current scope.
- *
- * <div class="alert alert-danger">
- * This directive can be abused to add unnecessary amounts of logic into your templates.
- * There are only a few appropriate uses of `ngInit`, such as for aliasing special properties of
- * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below; and for injecting data via
- * server side scripting. Besides these few cases, you should use {@link guide/controller controllers}
- * rather than `ngInit` to initialize values on a scope.
- * </div>
- *
- * <div class="alert alert-warning">
- * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make
- * sure you have parentheses to ensure correct operator precedence:
- * <pre class="prettyprint">
- * `<div ng-init="test1 = ($index | toString)"></div>`
- * </pre>
- * </div>
- *
- * @priority 450
- *
- * @element ANY
- * @param {expression} ngInit {@link guide/expression Expression} to eval.
- *
- * @example
- <example module="initExample">
- <file name="index.html">
- <script>
- angular.module('initExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.list = [['a', 'b'], ['c', 'd']];
- }]);
- </script>
- <div ng-controller="ExampleController">
- <div ng-repeat="innerList in list" ng-init="outerIndex = $index">
- <div ng-repeat="value in innerList" ng-init="innerIndex = $index">
- <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span>
- </div>
- </div>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should alias index positions', function() {
- var elements = element.all(by.css('.example-init'));
- expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;');
- expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;');
- expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;');
- expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;');
- });
- </file>
- </example>
- */
-var ngInitDirective = ngDirective({
- priority: 450,
- compile: function() {
- return {
- pre: function(scope, element, attrs) {
- scope.$eval(attrs.ngInit);
- }
- };
- }
-});
-
-/**
- * @ngdoc directive
- * @name ngList
- *
- * @description
- * Text input that converts between a delimited string and an array of strings. The default
- * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom
- * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`.
- *
- * The behaviour of the directive is affected by the use of the `ngTrim` attribute.
- * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
- * list item is respected. This implies that the user of the directive is responsible for
- * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
- * tab or newline character.
- * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
- * when joining the list items back together) and whitespace around each list item is stripped
- * before it is added to the model.
- *
- * ### Example with Validation
- *
- * <example name="ngList-directive" module="listExample">
- * <file name="app.js">
- * angular.module('listExample', [])
- * .controller('ExampleController', ['$scope', function($scope) {
- * $scope.names = ['morpheus', 'neo', 'trinity'];
- * }]);
- * </file>
- * <file name="index.html">
- * <form name="myForm" ng-controller="ExampleController">
- * <label>List: <input name="namesInput" ng-model="names" ng-list required></label>
- * <span role="alert">
- * <span class="error" ng-show="myForm.namesInput.$error.required">
- * Required!</span>
- * </span>
- * <br>
- * <tt>names = {{names}}</tt><br/>
- * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
- * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
- * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
- * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
- * </form>
- * </file>
- * <file name="protractor.js" type="protractor">
- * var listInput = element(by.model('names'));
- * var names = element(by.exactBinding('names'));
- * var valid = element(by.binding('myForm.namesInput.$valid'));
- * var error = element(by.css('span.error'));
- *
- * it('should initialize to model', function() {
- * expect(names.getText()).toContain('["morpheus","neo","trinity"]');
- * expect(valid.getText()).toContain('true');
- * expect(error.getCssValue('display')).toBe('none');
- * });
- *
- * it('should be invalid if empty', function() {
- * listInput.clear();
- * listInput.sendKeys('');
- *
- * expect(names.getText()).toContain('');
- * expect(valid.getText()).toContain('false');
- * expect(error.getCssValue('display')).not.toBe('none');
- * });
- * </file>
- * </example>
- *
- * ### Example - splitting on newline
- * <example name="ngList-directive-newlines">
- * <file name="index.html">
- * <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
- * <pre>{{ list | json }}</pre>
- * </file>
- * <file name="protractor.js" type="protractor">
- * it("should split the text by newlines", function() {
- * var listInput = element(by.model('list'));
- * var output = element(by.binding('list | json'));
- * listInput.sendKeys('abc\ndef\nghi');
- * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
- * });
- * </file>
- * </example>
- *
- * @element input
- * @param {string=} ngList optional delimiter that should be used to split the value.
- */
-var ngListDirective = function() {
- return {
- restrict: 'A',
- priority: 100,
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- // We want to control whitespace trimming so we use this convoluted approach
- // to access the ngList attribute, which doesn't pre-trim the attribute
- var ngList = element.attr(attr.$attr.ngList) || ', ';
- var trimValues = attr.ngTrim !== 'false';
- var separator = trimValues ? trim(ngList) : ngList;
-
- var parse = function(viewValue) {
- // If the viewValue is invalid (say required but empty) it will be `undefined`
- if (isUndefined(viewValue)) return;
-
- var list = [];
-
- if (viewValue) {
- forEach(viewValue.split(separator), function(value) {
- if (value) list.push(trimValues ? trim(value) : value);
- });
- }
-
- return list;
- };
-
- ctrl.$parsers.push(parse);
- ctrl.$formatters.push(function(value) {
- if (isArray(value)) {
- return value.join(ngList);
- }
-
- return undefined;
- });
-
- // Override the standard $isEmpty because an empty array means the input is empty.
- ctrl.$isEmpty = function(value) {
- return !value || !value.length;
- };
- }
- };
-};
-
-/* global VALID_CLASS: true,
- INVALID_CLASS: true,
- PRISTINE_CLASS: true,
- DIRTY_CLASS: true,
- UNTOUCHED_CLASS: true,
- TOUCHED_CLASS: true,
-*/
-
-var VALID_CLASS = 'ng-valid',
- INVALID_CLASS = 'ng-invalid',
- PRISTINE_CLASS = 'ng-pristine',
- DIRTY_CLASS = 'ng-dirty',
- UNTOUCHED_CLASS = 'ng-untouched',
- TOUCHED_CLASS = 'ng-touched',
- PENDING_CLASS = 'ng-pending',
- EMPTY_CLASS = 'ng-empty',
- NOT_EMPTY_CLASS = 'ng-not-empty';
-
-var ngModelMinErr = minErr('ngModel');
-
-/**
- * @ngdoc type
- * @name ngModel.NgModelController
- *
- * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a
- * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue
- * is set.
- * @property {*} $modelValue The value in the model that the control is bound to.
- * @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
- the control reads value from the DOM. The functions are called in array order, each passing
- its return value through to the next. The last return value is forwarded to the
- {@link ngModel.NgModelController#$validators `$validators`} collection.
-
-Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
-`$viewValue`}.
-
-Returning `undefined` from a parser means a parse error occurred. In that case,
-no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
-will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
-is set to `true`. The parse error is stored in `ngModel.$error.parse`.
-
- *
- * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever
- the model value changes. The functions are called in reverse array order, each passing the value through to the
- next. The last return value is used as the actual DOM value.
- Used to format / convert values for display in the control.
- * ```js
- * function formatter(value) {
- * if (value) {
- * return value.toUpperCase();
- * }
- * }
- * ngModel.$formatters.push(formatter);
- * ```
- *
- * @property {Object.<string, function>} $validators A collection of validators that are applied
- * whenever the model value changes. The key value within the object refers to the name of the
- * validator while the function refers to the validation operation. The validation operation is
- * provided with the model value as an argument and must return a true or false value depending
- * on the response of that validation.
- *
- * ```js
- * ngModel.$validators.validCharacters = function(modelValue, viewValue) {
- * var value = modelValue || viewValue;
- * return /[0-9]+/.test(value) &&
- * /[a-z]+/.test(value) &&
- * /[A-Z]+/.test(value) &&
- * /\W+/.test(value);
- * };
- * ```
- *
- * @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to
- * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
- * is expected to return a promise when it is run during the model validation process. Once the promise
- * is delivered then the validation status will be set to true when fulfilled and false when rejected.
- * When the asynchronous validators are triggered, each of the validators will run in parallel and the model
- * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator
- * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators
- * will only run once all synchronous validators have passed.
- *
- * Please note that if $http is used then it is important that the server returns a success HTTP response code
- * in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
- *
- * ```js
- * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
- * var value = modelValue || viewValue;
- *
- * // Lookup user by username
- * return $http.get('/api/users/' + value).
- * then(function resolved() {
- * //username exists, this means validation fails
- * return $q.reject('exists');
- * }, function rejected() {
- * //username does not exist, therefore this validation passes
- * return true;
- * });
- * };
- * ```
- *
- * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
- * view value has changed. It is called with no arguments, and its return value is ignored.
- * This can be used in place of additional $watches against the model value.
- *
- * @property {Object} $error An object hash with all failing validator ids as keys.
- * @property {Object} $pending An object hash with all pending validator ids as keys.
- *
- * @property {boolean} $untouched True if control has not lost focus yet.
- * @property {boolean} $touched True if control has lost focus.
- * @property {boolean} $pristine True if user has not interacted with the control yet.
- * @property {boolean} $dirty True if user has already interacted with the control.
- * @property {boolean} $valid True if there is no error.
- * @property {boolean} $invalid True if at least one error on the control.
- * @property {string} $name The name attribute of the control.
- *
- * @description
- *
- * `NgModelController` provides API for the {@link ngModel `ngModel`} directive.
- * The controller contains services for data-binding, validation, CSS updates, and value formatting
- * and parsing. It purposefully does not contain any logic which deals with DOM rendering or
- * listening to DOM events.
- * Such DOM related logic should be provided by other directives which make use of
- * `NgModelController` for data-binding to control elements.
- * Angular provides this DOM logic for most {@link input `input`} elements.
- * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example
- * custom control example} that uses `ngModelController` to bind to `contenteditable` elements.
- *
- * @example
- * ### Custom Control Example
- * This example shows how to use `NgModelController` with a custom control to achieve
- * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
- * collaborate together to achieve the desired result.
- *
- * `contenteditable` is an HTML5 attribute, which tells the browser to let the element
- * contents be edited in place by the user.
- *
- * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize}
- * module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`).
- * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks
- * that content using the `$sce` service.
- *
- * <example name="NgModelController" module="customControl" deps="angular-sanitize.js">
- <file name="style.css">
- [contenteditable] {
- border: 1px solid black;
- background-color: white;
- min-height: 20px;
- }
-
- .ng-invalid {
- border: 1px solid red;
- }
-
- </file>
- <file name="script.js">
- angular.module('customControl', ['ngSanitize']).
- directive('contenteditable', ['$sce', function($sce) {
- return {
- restrict: 'A', // only activate on element attribute
- require: '?ngModel', // get a hold of NgModelController
- link: function(scope, element, attrs, ngModel) {
- if (!ngModel) return; // do nothing if no ng-model
-
- // Specify how UI should be updated
- ngModel.$render = function() {
- element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
- };
-
- // Listen for change events to enable binding
- element.on('blur keyup change', function() {
- scope.$evalAsync(read);
- });
- read(); // initialize
-
- // Write data to the model
- function read() {
- var html = element.html();
- // When we clear the content editable the browser leaves a <br> behind
- // If strip-br attribute is provided then we strip this out
- if ( attrs.stripBr && html == '<br>' ) {
- html = '';
- }
- ngModel.$setViewValue(html);
- }
- }
- };
- }]);
- </file>
- <file name="index.html">
- <form name="myForm">
- <div contenteditable
- name="myWidget" ng-model="userContent"
- strip-br="true"
- required>Change me!</div>
- <span ng-show="myForm.myWidget.$error.required">Required!</span>
- <hr>
- <textarea ng-model="userContent" aria-label="Dynamic textarea"></textarea>
- </form>
- </file>
- <file name="protractor.js" type="protractor">
- it('should data-bind and become invalid', function() {
- if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
- // SafariDriver can't handle contenteditable
- // and Firefox driver can't clear contenteditables very well
- return;
- }
- var contentEditable = element(by.css('[contenteditable]'));
- var content = 'Change me!';
-
- expect(contentEditable.getText()).toEqual(content);
-
- contentEditable.clear();
- contentEditable.sendKeys(protractor.Key.BACK_SPACE);
- expect(contentEditable.getText()).toEqual('');
- expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
- });
- </file>
- * </example>
- *
- *
- */
-var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
- function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
- this.$viewValue = Number.NaN;
- this.$modelValue = Number.NaN;
- this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
- this.$validators = {};
- this.$asyncValidators = {};
- this.$parsers = [];
- this.$formatters = [];
- this.$viewChangeListeners = [];
- this.$untouched = true;
- this.$touched = false;
- this.$pristine = true;
- this.$dirty = false;
- this.$valid = true;
- this.$invalid = false;
- this.$error = {}; // keep invalid keys here
- this.$$success = {}; // keep valid keys here
- this.$pending = undefined; // keep pending keys here
- this.$name = $interpolate($attr.name || '', false)($scope);
- this.$$parentForm = nullFormCtrl;
-
- var parsedNgModel = $parse($attr.ngModel),
- parsedNgModelAssign = parsedNgModel.assign,
- ngModelGet = parsedNgModel,
- ngModelSet = parsedNgModelAssign,
- pendingDebounce = null,
- parserValid,
- ctrl = this;
-
- this.$$setOptions = function(options) {
- ctrl.$options = options;
- if (options && options.getterSetter) {
- var invokeModelGetter = $parse($attr.ngModel + '()'),
- invokeModelSetter = $parse($attr.ngModel + '($$$p)');
-
- ngModelGet = function($scope) {
- var modelValue = parsedNgModel($scope);
- if (isFunction(modelValue)) {
- modelValue = invokeModelGetter($scope);
- }
- return modelValue;
- };
- ngModelSet = function($scope, newValue) {
- if (isFunction(parsedNgModel($scope))) {
- invokeModelSetter($scope, {$$$p: newValue});
- } else {
- parsedNgModelAssign($scope, newValue);
- }
- };
- } else if (!parsedNgModel.assign) {
- throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
- $attr.ngModel, startingTag($element));
- }
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$render
- *
- * @description
- * Called when the view needs to be updated. It is expected that the user of the ng-model
- * directive will implement this method.
- *
- * The `$render()` method is invoked in the following situations:
- *
- * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last
- * committed value then `$render()` is called to update the input control.
- * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and
- * the `$viewValue` are different from last time.
- *
- * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
- * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue`
- * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
- * invoked if you only change a property on the objects.
- */
- this.$render = noop;
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$isEmpty
- *
- * @description
- * This is called when we need to determine if the value of an input is empty.
- *
- * For instance, the required directive does this to work out if the input has data or not.
- *
- * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
- *
- * You can override this for input directives whose concept of being empty is different from the
- * default. The `checkboxInputType` directive does this because in its case a value of `false`
- * implies empty.
- *
- * @param {*} value The value of the input to check for emptiness.
- * @returns {boolean} True if `value` is "empty".
- */
- this.$isEmpty = function(value) {
- return isUndefined(value) || value === '' || value === null || value !== value;
- };
-
- this.$$updateEmptyClasses = function(value) {
- if (ctrl.$isEmpty(value)) {
- $animate.removeClass($element, NOT_EMPTY_CLASS);
- $animate.addClass($element, EMPTY_CLASS);
- } else {
- $animate.removeClass($element, EMPTY_CLASS);
- $animate.addClass($element, NOT_EMPTY_CLASS);
- }
- };
-
-
- var currentValidationRunId = 0;
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setValidity
- *
- * @description
- * Change the validity state, and notify the form.
- *
- * This method can be called within $parsers/$formatters or a custom validation implementation.
- * However, in most cases it should be sufficient to use the `ngModel.$validators` and
- * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
- *
- * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
- * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
- * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
- * The `validationErrorKey` should be in camelCase and will get converted into dash-case
- * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
- * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
- * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
- * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
- * Skipped is used by Angular when validators do not run because of parse errors and
- * when `$asyncValidators` do not run because any of the `$validators` failed.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: $element,
- set: function(object, property) {
- object[property] = true;
- },
- unset: function(object, property) {
- delete object[property];
- },
- $animate: $animate
- });
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setPristine
- *
- * @description
- * Sets the control to its pristine state.
- *
- * This method can be called to remove the `ng-dirty` class and set the control to its pristine
- * state (`ng-pristine` class). A model is considered to be pristine when the control
- * has not been changed from when first compiled.
- */
- this.$setPristine = function() {
- ctrl.$dirty = false;
- ctrl.$pristine = true;
- $animate.removeClass($element, DIRTY_CLASS);
- $animate.addClass($element, PRISTINE_CLASS);
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setDirty
- *
- * @description
- * Sets the control to its dirty state.
- *
- * This method can be called to remove the `ng-pristine` class and set the control to its dirty
- * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
- * from when first compiled.
- */
- this.$setDirty = function() {
- ctrl.$dirty = true;
- ctrl.$pristine = false;
- $animate.removeClass($element, PRISTINE_CLASS);
- $animate.addClass($element, DIRTY_CLASS);
- ctrl.$$parentForm.$setDirty();
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setUntouched
- *
- * @description
- * Sets the control to its untouched state.
- *
- * This method can be called to remove the `ng-touched` class and set the control to its
- * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched
- * by default, however this function can be used to restore that state if the model has
- * already been touched by the user.
- */
- this.$setUntouched = function() {
- ctrl.$touched = false;
- ctrl.$untouched = true;
- $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setTouched
- *
- * @description
- * Sets the control to its touched state.
- *
- * This method can be called to remove the `ng-untouched` class and set the control to its
- * touched state (`ng-touched` class). A model is considered to be touched when the user has
- * first focused the control element and then shifted focus away from the control (blur event).
- */
- this.$setTouched = function() {
- ctrl.$touched = true;
- ctrl.$untouched = false;
- $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$rollbackViewValue
- *
- * @description
- * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
- * which may be caused by a pending debounced event or because the input is waiting for a some
- * future event.
- *
- * If you have an input that uses `ng-model-options` to set up debounced updates or updates that
- * depend on special events such as blur, you can have a situation where there is a period when
- * the `$viewValue` is out of sync with the ngModel's `$modelValue`.
- *
- * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update
- * and reset the input to the last committed view value.
- *
- * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue`
- * programmatically before these debounced/future events have resolved/occurred, because Angular's
- * dirty checking mechanism is not able to tell whether the model has actually changed or not.
- *
- * The `$rollbackViewValue()` method should be called before programmatically changing the model of an
- * input which may have such events pending. This is important in order to make sure that the
- * input field will be updated with the new model value and any pending operations are cancelled.
- *
- * <example name="ng-model-cancel-update" module="cancel-update-example">
- * <file name="app.js">
- * angular.module('cancel-update-example', [])
- *
- * .controller('CancelUpdateController', ['$scope', function($scope) {
- * $scope.model = {};
- *
- * $scope.setEmpty = function(e, value, rollback) {
- * if (e.keyCode == 27) {
- * e.preventDefault();
- * if (rollback) {
- * $scope.myForm[value].$rollbackViewValue();
- * }
- * $scope.model[value] = '';
- * }
- * };
- * }]);
- * </file>
- * <file name="index.html">
- * <div ng-controller="CancelUpdateController">
- * <p>Both of these inputs are only updated if they are blurred. Hitting escape should
- * empty them. Follow these steps and observe the difference:</p>
- * <ol>
- * <li>Type something in the input. You will see that the model is not yet updated</li>
- * <li>Press the Escape key.
- * <ol>
- * <li> In the first example, nothing happens, because the model is already '', and no
- * update is detected. If you blur the input, the model will be set to the current view.
- * </li>
- * <li> In the second example, the pending update is cancelled, and the input is set back
- * to the last committed view value (''). Blurring the input does nothing.
- * </li>
- * </ol>
- * </li>
- * </ol>
- *
- * <form name="myForm" ng-model-options="{ updateOn: 'blur' }">
- * <div>
- * <p id="inputDescription1">Without $rollbackViewValue():</p>
- * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1"
- * ng-keydown="setEmpty($event, 'value1')">
- * value1: "{{ model.value1 }}"
- * </div>
- *
- * <div>
- * <p id="inputDescription2">With $rollbackViewValue():</p>
- * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2"
- * ng-keydown="setEmpty($event, 'value2', true)">
- * value2: "{{ model.value2 }}"
- * </div>
- * </form>
- * </div>
- * </file>
- <file name="style.css">
- div {
- display: table-cell;
- }
- div:nth-child(1) {
- padding-right: 30px;
- }
-
- </file>
- * </example>
- */
- this.$rollbackViewValue = function() {
- $timeout.cancel(pendingDebounce);
- ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
- ctrl.$render();
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$validate
- *
- * @description
- * Runs each of the registered validators (first synchronous validators and then
- * asynchronous validators).
- * If the validity changes to invalid, the model will be set to `undefined`,
- * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`.
- * If the validity changes to valid, it will set the model to the last available valid
- * `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
- */
- this.$validate = function() {
- // ignore $validate before model is initialized
- if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
- return;
- }
-
- var viewValue = ctrl.$$lastCommittedViewValue;
- // Note: we use the $$rawModelValue as $modelValue might have been
- // set to undefined during a view -> model update that found validation
- // errors. We can't parse the view here, since that could change
- // the model although neither viewValue nor the model on the scope changed
- var modelValue = ctrl.$$rawModelValue;
-
- var prevValid = ctrl.$valid;
- var prevModelValue = ctrl.$modelValue;
-
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
-
- ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
- // If there was no change in validity, don't update the model
- // This prevents changing an invalid modelValue to undefined
- if (!allowInvalid && prevValid !== allValid) {
- // Note: Don't check ctrl.$valid here, as we could have
- // external validators (e.g. calculated on the server),
- // that just call $setValidity and need the model value
- // to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
-
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
- }
- }
- });
-
- };
-
- this.$$runValidators = function(modelValue, viewValue, doneCallback) {
- currentValidationRunId++;
- var localValidationRunId = currentValidationRunId;
-
- // check parser error
- if (!processParseErrors()) {
- validationDone(false);
- return;
- }
- if (!processSyncValidators()) {
- validationDone(false);
- return;
- }
- processAsyncValidators();
-
- function processParseErrors() {
- var errorKey = ctrl.$$parserName || 'parse';
- if (isUndefined(parserValid)) {
- setValidity(errorKey, null);
- } else {
- if (!parserValid) {
- forEach(ctrl.$validators, function(v, name) {
- setValidity(name, null);
- });
- forEach(ctrl.$asyncValidators, function(v, name) {
- setValidity(name, null);
- });
- }
- // Set the parse error last, to prevent unsetting it, should a $validators key == parserName
- setValidity(errorKey, parserValid);
- return parserValid;
- }
- return true;
- }
-
- function processSyncValidators() {
- var syncValidatorsValid = true;
- forEach(ctrl.$validators, function(validator, name) {
- var result = validator(modelValue, viewValue);
- syncValidatorsValid = syncValidatorsValid && result;
- setValidity(name, result);
- });
- if (!syncValidatorsValid) {
- forEach(ctrl.$asyncValidators, function(v, name) {
- setValidity(name, null);
- });
- return false;
- }
- return true;
- }
-
- function processAsyncValidators() {
- var validatorPromises = [];
- var allValid = true;
- forEach(ctrl.$asyncValidators, function(validator, name) {
- var promise = validator(modelValue, viewValue);
- if (!isPromiseLike(promise)) {
- throw ngModelMinErr('nopromise',
- "Expected asynchronous validator to return a promise but got '{0}' instead.", promise);
- }
- setValidity(name, undefined);
- validatorPromises.push(promise.then(function() {
- setValidity(name, true);
- }, function() {
- allValid = false;
- setValidity(name, false);
- }));
- });
- if (!validatorPromises.length) {
- validationDone(true);
- } else {
- $q.all(validatorPromises).then(function() {
- validationDone(allValid);
- }, noop);
- }
- }
-
- function setValidity(name, isValid) {
- if (localValidationRunId === currentValidationRunId) {
- ctrl.$setValidity(name, isValid);
- }
- }
-
- function validationDone(allValid) {
- if (localValidationRunId === currentValidationRunId) {
-
- doneCallback(allValid);
- }
- }
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$commitViewValue
- *
- * @description
- * Commit a pending update to the `$modelValue`.
- *
- * Updates may be pending by a debounced event or because the input is waiting for a some future
- * event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
- * usually handles calling this in response to input events.
- */
- this.$commitViewValue = function() {
- var viewValue = ctrl.$viewValue;
-
- $timeout.cancel(pendingDebounce);
-
- // If the view value has not changed then we should just exit, except in the case where there is
- // a native validator on the element. In this case the validation state may have changed even though
- // the viewValue has stayed empty.
- if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
- return;
- }
- ctrl.$$updateEmptyClasses(viewValue);
- ctrl.$$lastCommittedViewValue = viewValue;
-
- // change to dirty
- if (ctrl.$pristine) {
- this.$setDirty();
- }
- this.$$parseAndValidate();
- };
-
- this.$$parseAndValidate = function() {
- var viewValue = ctrl.$$lastCommittedViewValue;
- var modelValue = viewValue;
- 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($scope);
- }
- var prevModelValue = ctrl.$modelValue;
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
- ctrl.$$rawModelValue = modelValue;
-
- if (allowInvalid) {
- ctrl.$modelValue = modelValue;
- writeToModelIfNeeded();
- }
-
- // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
- // This can happen if e.g. $setViewValue is called from inside a parser
- ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
- if (!allowInvalid) {
- // Note: Don't check ctrl.$valid here, as we could have
- // external validators (e.g. calculated on the server),
- // that just call $setValidity and need the model value
- // to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
- writeToModelIfNeeded();
- }
- });
-
- function writeToModelIfNeeded() {
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
- }
- }
- };
-
- this.$$writeModelToScope = function() {
- ngModelSet($scope, ctrl.$modelValue);
- forEach(ctrl.$viewChangeListeners, function(listener) {
- try {
- listener();
- } catch (e) {
- $exceptionHandler(e);
- }
- });
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setViewValue
- *
- * @description
- * Update the view value.
- *
- * This method should be called when a control wants to change the view value; typically,
- * this is done from within a DOM event handler. For example, the {@link ng.directive:input input}
- * directive calls it when the value of the input changes and {@link ng.directive:select select}
- * calls it when an option is selected.
- *
- * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers`
- * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
- * value sent directly for processing, finally to be applied to `$modelValue` and then the
- * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners,
- * in the `$viewChangeListeners` list, are called.
- *
- * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
- * and the `default` trigger is not listed, all those actions will remain pending until one of the
- * `updateOn` events is triggered on the DOM element.
- * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
- * directive is used with a custom debounce for this particular event.
- * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce`
- * is specified, once the timer runs out.
- *
- * When used with standard inputs, the view value will always be a string (which is in some cases
- * parsed into another type, such as a `Date` object for `input[date]`.)
- * However, custom controls might also pass objects to this method. In this case, we should make
- * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not
- * perform a deep watch of objects, it only looks for a change of identity. If you only change
- * the property of the object then ngModel will not realize that the object has changed and
- * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should
- * not change properties of the copy once it has been passed to `$setViewValue`.
- * Otherwise you may cause the model value on the scope to change incorrectly.
- *
- * <div class="alert alert-info">
- * In any case, the value passed to the method should always reflect the current value
- * of the control. For example, if you are calling `$setViewValue` for an input element,
- * you should pass the input DOM value. Otherwise, the control and the scope model become
- * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change
- * the control's DOM value in any way. If we want to change the control's DOM value
- * programmatically, we should update the `ngModel` scope expression. Its new value will be
- * picked up by the model controller, which will run it through the `$formatters`, `$render` it
- * to update the DOM, and finally call `$validate` on it.
- * </div>
- *
- * @param {*} value value from the view.
- * @param {string} trigger Event that triggered the update.
- */
- this.$setViewValue = function(value, trigger) {
- ctrl.$viewValue = value;
- if (!ctrl.$options || ctrl.$options.updateOnDefault) {
- ctrl.$$debounceViewValueCommit(trigger);
- }
- };
-
- this.$$debounceViewValueCommit = function(trigger) {
- var debounceDelay = 0,
- options = ctrl.$options,
- debounce;
-
- if (options && isDefined(options.debounce)) {
- debounce = options.debounce;
- if (isNumber(debounce)) {
- debounceDelay = debounce;
- } else if (isNumber(debounce[trigger])) {
- debounceDelay = debounce[trigger];
- } else if (isNumber(debounce['default'])) {
- debounceDelay = debounce['default'];
- }
- }
-
- $timeout.cancel(pendingDebounce);
- if (debounceDelay) {
- pendingDebounce = $timeout(function() {
- ctrl.$commitViewValue();
- }, debounceDelay);
- } else if ($rootScope.$$phase) {
- ctrl.$commitViewValue();
- } else {
- $scope.$apply(function() {
- ctrl.$commitViewValue();
- });
- }
- };
-
- // model -> value
- // Note: we cannot use a normal scope.$watch as we want to detect the following:
- // 1. scope value is 'a'
- // 2. user enters 'b'
- // 3. ng-change kicks in and reverts scope value to 'a'
- // -> scope value did not change since the last digest as
- // ng-change executes in apply phase
- // 4. view should be changed back to 'a'
- $scope.$watch(function ngModelWatch() {
- var modelValue = ngModelGet($scope);
-
- // if scope model value and ngModel value are out of sync
- // TODO(perf): why not move this to the action fn?
- if (modelValue !== ctrl.$modelValue &&
- // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
- (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
- ) {
- ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
- parserValid = undefined;
-
- var formatters = ctrl.$formatters,
- idx = formatters.length;
-
- var viewValue = modelValue;
- while (idx--) {
- viewValue = formatters[idx](viewValue);
- }
- if (ctrl.$viewValue !== viewValue) {
- ctrl.$$updateEmptyClasses(viewValue);
- ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
- ctrl.$render();
-
- ctrl.$$runValidators(modelValue, viewValue, noop);
- }
- }
-
- return modelValue;
- });
-}];
-
-
-/**
- * @ngdoc directive
- * @name ngModel
- *
- * @element input
- * @priority 1
- *
- * @description
- * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
- * property on the scope using {@link ngModel.NgModelController NgModelController},
- * which is created and exposed by this directive.
- *
- * `ngModel` is responsible for:
- *
- * - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
- * require.
- * - Providing validation behavior (i.e. required, number, email, url).
- * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
- * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`,
- * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations.
- * - Registering the control with its parent {@link ng.directive:form form}.
- *
- * Note: `ngModel` will try to bind to the property given by evaluating the expression on the
- * current scope. If the property doesn't already exist on this scope, it will be created
- * implicitly and added to the scope.
- *
- * For best practices on using `ngModel`, see:
- *
- * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes)
- *
- * For basic examples, how to use `ngModel`, see:
- *
- * - {@link ng.directive:input input}
- * - {@link input[text] text}
- * - {@link input[checkbox] checkbox}
- * - {@link input[radio] radio}
- * - {@link input[number] number}
- * - {@link input[email] email}
- * - {@link input[url] url}
- * - {@link input[date] date}
- * - {@link input[datetime-local] datetime-local}
- * - {@link input[time] time}
- * - {@link input[month] month}
- * - {@link input[week] week}
- * - {@link ng.directive:select select}
- * - {@link ng.directive:textarea textarea}
- *
- * # Complex Models (objects or collections)
- *
- * By default, `ngModel` watches the model by reference, not value. This is important to know when
- * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the
- * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered.
- *
- * The model must be assigned an entirely new object or collection before a re-rendering will occur.
- *
- * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression
- * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or
- * if the select is given the `multiple` attribute.
- *
- * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the
- * first level of the object (or only changing the properties of an item in the collection if it's an array) will still
- * not trigger a re-rendering of the model.
- *
- * # CSS classes
- * The following CSS classes are added and removed on the associated input/select/textarea element
- * depending on the validity of the model.
- *
- * - `ng-valid`: the model is valid
- * - `ng-invalid`: the model is invalid
- * - `ng-valid-[key]`: for each valid key added by `$setValidity`
- * - `ng-invalid-[key]`: for each invalid key added by `$setValidity`
- * - `ng-pristine`: the control hasn't been interacted with yet
- * - `ng-dirty`: the control has been interacted with
- * - `ng-touched`: the control has been blurred
- * - `ng-untouched`: the control hasn't been blurred
- * - `ng-pending`: any `$asyncValidators` are unfulfilled
- * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined
- * by the {@link ngModel.NgModelController#$isEmpty} method
- * - `ng-not-empty`: the view contains a non-empty value
- *
- * Keep in mind that ngAnimate can detect each of these classes when added and removed.
- *
- * ## Animation Hooks
- *
- * Animations within models are triggered when any of the associated CSS classes are added and removed
- * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`,
- * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
- * The animations that are triggered within ngModel are similar to how they work in ngClass and
- * animations can be hooked into using CSS transitions, keyframes as well as JS animations.
- *
- * The following example shows a simple way to utilize CSS transitions to style an input element
- * that has been rendered as invalid after it has been validated:
- *
- * <pre>
- * //be sure to include ngAnimate as a module to hook into more
- * //advanced animations
- * .my-input {
- * transition:0.5s linear all;
- * background: white;
- * }
- * .my-input.ng-invalid {
- * background: red;
- * color:white;
- * }
- * </pre>
- *
- * @example
- * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample">
- <file name="index.html">
- <script>
- angular.module('inputExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.val = '1';
- }]);
- </script>
- <style>
- .my-input {
- transition:all linear 0.5s;
- background: transparent;
- }
- .my-input.ng-invalid {
- color:white;
- background: red;
- }
- </style>
- <p id="inputDescription">
- Update input to see transitions when valid/invalid.
- Integer is a valid value.
- </p>
- <form name="testForm" ng-controller="ExampleController">
- <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input"
- aria-describedby="inputDescription" />
- </form>
- </file>
- * </example>
- *
- * ## Binding to a getter/setter
- *
- * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
- * function that returns a representation of the model when called with zero arguments, and sets
- * the internal state of a model when called with an argument. It's sometimes useful to use this
- * for models that have an internal representation that's different from what the model exposes
- * to the view.
- *
- * <div class="alert alert-success">
- * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more
- * frequently than other parts of your code.
- * </div>
- *
- * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that
- * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to
- * a `<form>`, which will enable this behavior for all `<input>`s within it. See
- * {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
- *
- * The following example shows how to use `ngModel` with a getter/setter:
- *
- * @example
- * <example name="ngModel-getter-setter" module="getterSetterExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ getterSetter: true }" />
- </label>
- </form>
- <pre>user.name = <span ng-bind="user.name()"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('getterSetterExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- var _name = 'Brian';
- $scope.user = {
- name: function(newName) {
- // Note that newName can be undefined for two reasons:
- // 1. Because it is called as a getter and thus called with no arguments
- // 2. Because the property should actually be set to undefined. This happens e.g. if the
- // input is invalid
- return arguments.length ? (_name = newName) : _name;
- }
- };
- }]);
- </file>
- * </example>
- */
-var ngModelDirective = ['$rootScope', function($rootScope) {
- return {
- restrict: 'A',
- require: ['ngModel', '^?form', '^?ngModelOptions'],
- controller: NgModelController,
- // 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);
-
- return {
- pre: function ngModelPreLink(scope, element, attr, ctrls) {
- var modelCtrl = ctrls[0],
- formCtrl = ctrls[1] || modelCtrl.$$parentForm;
-
- modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
-
- // notify others, especially parent forms
- formCtrl.$addControl(modelCtrl);
-
- attr.$observe('name', function(newValue) {
- if (modelCtrl.$name !== newValue) {
- modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue);
- }
- });
-
- scope.$on('$destroy', function() {
- modelCtrl.$$parentForm.$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);
- });
- }
-
- element.on('blur', function() {
- if (modelCtrl.$touched) return;
-
- if ($rootScope.$$phase) {
- scope.$evalAsync(modelCtrl.$setTouched);
- } else {
- scope.$apply(modelCtrl.$setTouched);
- }
- });
- }
- };
- }
- };
-}];
-
-var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
-
-/**
- * @ngdoc directive
- * @name ngModelOptions
- *
- * @description
- * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
- * events that will trigger a model update and/or a debouncing delay so that the actual update only
- * takes place when a timer expires; this timer will be reset after another change takes place.
- *
- * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
- * be different from the value in the actual model. This means that if you update the model you
- * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
- * order to make sure it is synchronized with the model and that any debounced action is canceled.
- *
- * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
- * method is by making sure the input is placed inside a form that has a `name` attribute. This is
- * important because `form` controllers are published to the related scope under the name in their
- * `name` attribute.
- *
- * Any pending changes will take place immediately when an enclosing form is submitted via the
- * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
- * to have access to the updated model.
- *
- * `ngModelOptions` has an effect on the element it's declared on and its descendants.
- *
- * @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
- * - `updateOn`: string specifying which event should the input be bound to. You can set several
- * events using an space delimited list. There is a special event called `default` that
- * matches the default events belonging of the control.
- * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
- * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
- * custom value for each event. For example:
- * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
- * - `allowInvalid`: boolean value which indicates that the model can be set with values that did
- * not validate correctly instead of the default behavior of setting the model to undefined.
- * - `getterSetter`: boolean value which determines whether or not to treat functions bound to
- `ngModel` as getters/setters.
- * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
- * `<input type="date">`, `<input type="time">`, ... . It understands UTC/GMT and the
- * continental US time zone abbreviations, but for general use, use a time zone offset, for
- * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
- * If not specified, the timezone of the browser will be used.
- *
- * @example
-
- The following example shows how to override immediate updates. Changes on the inputs within the
- form will update the model only when the control loses focus (blur event). If `escape` key is
- pressed while the input field is focused, the value is reset to the value in the current model.
-
- <example name="ngModelOptions-directive-blur" module="optionsExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ updateOn: 'blur' }"
- ng-keyup="cancel($event)" />
- </label><br />
- <label>Other data:
- <input type="text" ng-model="user.data" />
- </label><br />
- </form>
- <pre>user.name = <span ng-bind="user.name"></span></pre>
- <pre>user.data = <span ng-bind="user.data"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'John', data: '' };
-
- $scope.cancel = function(e) {
- if (e.keyCode == 27) {
- $scope.userForm.userName.$rollbackViewValue();
- }
- };
- }]);
- </file>
- <file name="protractor.js" type="protractor">
- var model = element(by.binding('user.name'));
- var input = element(by.model('user.name'));
- var other = element(by.model('user.data'));
-
- it('should allow custom events', function() {
- input.sendKeys(' Doe');
- input.click();
- expect(model.getText()).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John Doe');
- });
-
- it('should $rollbackViewValue when model changes', function() {
- input.sendKeys(' Doe');
- expect(input.getAttribute('value')).toEqual('John Doe');
- input.sendKeys(protractor.Key.ESCAPE);
- expect(input.getAttribute('value')).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John');
- });
- </file>
- </example>
-
- This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
- If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
-
- <example name="ngModelOptions-directive-debounce" module="optionsExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ debounce: 1000 }" />
- </label>
- <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button>
- <br />
- </form>
- <pre>user.name = <span ng-bind="user.name"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'Igor' };
- }]);
- </file>
- </example>
-
- This one shows how to bind to getter/setters:
-
- <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ getterSetter: true }" />
- </label>
- </form>
- <pre>user.name = <span ng-bind="user.name()"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('getterSetterExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- var _name = 'Brian';
- $scope.user = {
- name: function(newName) {
- // Note that newName can be undefined for two reasons:
- // 1. Because it is called as a getter and thus called with no arguments
- // 2. Because the property should actually be set to undefined. This happens e.g. if the
- // input is invalid
- return arguments.length ? (_name = newName) : _name;
- }
- };
- }]);
- </file>
- </example>
- */
-var ngModelOptionsDirective = function() {
- return {
- restrict: 'A',
- controller: ['$scope', '$attrs', function($scope, $attrs) {
- var that = this;
- this.$options = copy($scope.$eval($attrs.ngModelOptions));
- // Allow adding/overriding bound events
- if (isDefined(this.$options.updateOn)) {
- this.$options.updateOnDefault = false;
- // extract "default" pseudo-event from list of events that can trigger a model update
- this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
- that.$options.updateOnDefault = true;
- return ' ';
- }));
- } else {
- this.$options.updateOnDefault = true;
- }
- }]
- };
-};
-
-
-
-// helper methods
function addSetValidityMethod(context) {
- var ctrl = context.ctrl,
- $element = context.$element,
- classCache = {},
+ var clazz = context.clazz,
set = context.set,
- unset = context.unset,
- $animate = context.$animate;
-
- classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
+ unset = context.unset;
- ctrl.$setValidity = setValidity;
-
- function setValidity(validationErrorKey, state, controller) {
+ clazz.prototype.$setValidity = function(validationErrorKey, state, controller) {
if (isUndefined(state)) {
- createAndSet('$pending', validationErrorKey, controller);
+ createAndSet(this, '$pending', validationErrorKey, controller);
} else {
- unsetAndCleanup('$pending', validationErrorKey, controller);
+ unsetAndCleanup(this, '$pending', validationErrorKey, controller);
}
if (!isBoolean(state)) {
- unset(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
+ unset(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
} else {
if (state) {
- unset(ctrl.$error, validationErrorKey, controller);
- set(ctrl.$$success, validationErrorKey, controller);
+ unset(this.$error, validationErrorKey, controller);
+ set(this.$$success, validationErrorKey, controller);
} else {
- set(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
+ set(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
}
}
- if (ctrl.$pending) {
- cachedToggleClass(PENDING_CLASS, true);
- ctrl.$valid = ctrl.$invalid = undefined;
- toggleValidationCss('', null);
+ if (this.$pending) {
+ cachedToggleClass(this, PENDING_CLASS, true);
+ this.$valid = this.$invalid = undefined;
+ toggleValidationCss(this, '', null);
} else {
- cachedToggleClass(PENDING_CLASS, false);
- ctrl.$valid = isObjectEmpty(ctrl.$error);
- ctrl.$invalid = !ctrl.$valid;
- toggleValidationCss('', ctrl.$valid);
+ cachedToggleClass(this, PENDING_CLASS, false);
+ this.$valid = isObjectEmpty(this.$error);
+ this.$invalid = !this.$valid;
+ toggleValidationCss(this, '', this.$valid);
}
// re-read the state as the set/unset methods could have
- // combined state in ctrl.$error[validationError] (used for forms),
+ // combined state in this.$error[validationError] (used for forms),
// where setting/unsetting only increments/decrements the value,
// and does not replace it.
var combinedState;
- if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
+ if (this.$pending && this.$pending[validationErrorKey]) {
combinedState = undefined;
- } else if (ctrl.$error[validationErrorKey]) {
+ } else if (this.$error[validationErrorKey]) {
combinedState = false;
- } else if (ctrl.$$success[validationErrorKey]) {
+ } else if (this.$$success[validationErrorKey]) {
combinedState = true;
} else {
combinedState = null;
}
- toggleValidationCss(validationErrorKey, combinedState);
- ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
- }
+ toggleValidationCss(this, validationErrorKey, combinedState);
+ this.$$parentForm.$setValidity(validationErrorKey, combinedState, this);
+ };
- function createAndSet(name, value, controller) {
+ function createAndSet(ctrl, name, value, controller) {
if (!ctrl[name]) {
ctrl[name] = {};
}
set(ctrl[name], value, controller);
}
- function unsetAndCleanup(name, value, controller) {
+ function unsetAndCleanup(ctrl, name, value, controller) {
if (ctrl[name]) {
unset(ctrl[name], value, controller);
}
@@ -28329,21 +19626,21 @@ function addSetValidityMethod(context) {
}
}
- function cachedToggleClass(className, switchValue) {
- if (switchValue && !classCache[className]) {
- $animate.addClass($element, className);
- classCache[className] = true;
- } else if (!switchValue && classCache[className]) {
- $animate.removeClass($element, className);
- classCache[className] = false;
+ function cachedToggleClass(ctrl, className, switchValue) {
+ if (switchValue && !ctrl.$$classCache[className]) {
+ ctrl.$$animate.addClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = true;
+ } else if (!switchValue && ctrl.$$classCache[className]) {
+ ctrl.$$animate.removeClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = false;
}
}
- function toggleValidationCss(validationErrorKey, isValid) {
+ function toggleValidationCss(ctrl, validationErrorKey, isValid) {
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
- cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
- cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
+ cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true);
+ cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false);
}
}
@@ -28363,35 +19660,36 @@ function isObjectEmpty(obj) {
* @name ngNonBindable
* @restrict AC
* @priority 1000
+ * @element ANY
*
* @description
- * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current
- * DOM element. This is useful if the element contains what appears to be Angular directives and
- * bindings but which should be ignored by Angular. This could be the case if you have a site that
- * displays snippets of code, for instance.
- *
- * @element ANY
+ * The `ngNonBindable` directive tells AngularJS not to compile or bind the contents of the current
+ * DOM element, including directives on the element itself that have a lower priority than
+ * `ngNonBindable`. This is useful if the element contains what appears to be AngularJS directives
+ * and bindings but which should be ignored by AngularJS. This could be the case if you have a site
+ * that displays snippets of code, for instance.
*
* @example
* In this example there are two locations where a simple interpolation binding (`{{}}`) is present,
* but the one wrapped in `ngNonBindable` is left alone.
*
- * @example
- <example>
- <file name="index.html">
- <div>Normal: {{1 + 2}}</div>
- <div ng-non-bindable>Ignored: {{1 + 2}}</div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-non-bindable', function() {
- expect(element(by.binding('1 + 2')).getText()).toContain('3');
- expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/);
- });
- </file>
- </example>
+ <example name="ng-non-bindable">
+ <file name="index.html">
+ <div>Normal: {{1 + 2}}</div>
+ <div ng-non-bindable>Ignored: {{1 + 2}}</div>
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ng-non-bindable', function() {
+ expect(element(by.binding('1 + 2')).getText()).toContain('3');
+ expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/);
+ });
+ </file>
+ </example>
*/
var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+/* exported ngOptionsDirective */
+
/* global jqLiteRemove */
var ngOptionsMinErr = minErr('ngOptions');
@@ -28407,13 +19705,12 @@ var ngOptionsMinErr = minErr('ngOptions');
* elements for the `<select>` element using the array or object obtained by evaluating the
* `ngOptions` comprehension expression.
*
- * In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a
- * similar result. However, `ngOptions` provides some benefits such as reducing memory and
- * increasing speed by not creating a new scope for each repeated instance, as well as providing
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
- * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound
- * to a non-string value. This is because an option element can only be bound to string values at
- * present.
+ * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `<option>` elements instead of
+ * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
*
* When an item in the `<select>` menu is selected, the array element or object property
* represented by the selected option will be bound to the model identified by the `ngModel`
@@ -28502,13 +19799,8 @@ var ngOptionsMinErr = minErr('ngOptions');
* is not matched against any `<option>` and the `<select>` appears as having no selected value.
*
*
- * @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
- * `required` when you want to data-bind to the `required` attribute.
- * @param {comprehension_expression=} ngOptions in one of the following forms:
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {comprehension_expression} ngOptions in one of the following forms:
*
* * for array data sources:
* * `label` **`for`** `value` **`in`** `array`
@@ -28547,9 +19839,16 @@ var ngOptionsMinErr = minErr('ngOptions');
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
* even when the options are recreated (e.g. reloaded from the server).
+ * @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
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
* @example
- <example module="selectExample">
+ <example module="selectExample" name="select">
<file name="index.html">
<script>
angular.module('selectExample', [])
@@ -28622,9 +19921,9 @@ var ngOptionsMinErr = minErr('ngOptions');
</example>
*/
-// jshint maxlen: false
-// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
-var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
+/* eslint-disable max-len */
+// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555000000000666666666666600000007777777777777000000000000000888888888800000000000000000009999999999
+var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
// 1: value expression (valueFn)
// 2: label expression (displayFn)
// 3: group by expression (groupByFn)
@@ -28634,7 +19933,7 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s
// 7: object item value variable name
// 8: collection expression
// 9: track by expression
-// jshint maxlen: 100
+/* eslint-enable */
var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
@@ -28644,9 +19943,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var match = optionsExp.match(NG_OPTIONS_REGEXP);
if (!(match)) {
throw ngOptionsMinErr('iexp',
- "Expected expression in form of " +
- "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '{0}'. Element: {1}",
+ 'Expected expression in form of ' +
+ '\'_select_ (as _label_)? for (_key_,)?_value_ in _collection_\'' +
+ ' but got \'{0}\'. Element: {1}',
optionsExp, startingTag(selectElement));
}
@@ -28788,7 +20087,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
getViewValueFromOption: function(option) {
// If the viewValue could be an object that may be mutated by the application,
// we need to make a copy and not return the reference to the value on the option.
- return trackBy ? angular.copy(option.viewValue) : option.viewValue;
+ return trackBy ? copy(option.viewValue) : option.viewValue;
}
};
}
@@ -28796,7 +20095,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
}
- // we can't just jqLite('<option>') since jqLite is not smart enough
+ // Support: IE 9 only
+ // We can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
var optionTemplate = window.document.createElement('option'),
optGroupTemplate = window.document.createElement('optgroup');
@@ -28809,15 +20109,18 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// The emptyOption allows the application developer to provide their own custom "empty"
// option when the viewValue does not match any of the option values.
- var emptyOption;
for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
if (children[i].value === '') {
- emptyOption = children.eq(i);
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = children.eq(i);
break;
}
}
- var providedEmptyOption = !!emptyOption;
+ // The empty option will be compiled and rendered before we first generate the options
+ selectElement.empty();
+
+ var providedEmptyOption = !!selectCtrl.emptyOption;
var unknownOption = jqLite(optionTemplate.cloneNode(false));
unknownOption.val('?');
@@ -28829,39 +20132,25 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// we only need to create it once.
var listFragment = $document[0].createDocumentFragment();
- var renderEmptyOption = function() {
- if (!providedEmptyOption) {
- selectElement.prepend(emptyOption);
- }
- selectElement.val('');
- emptyOption.prop('selected', true); // needed for IE
- emptyOption.attr('selected', true);
- };
-
- var removeEmptyOption = function() {
- if (!providedEmptyOption) {
- emptyOption.remove();
- }
- };
-
-
- var renderUnknownOption = function() {
- selectElement.prepend(unknownOption);
- selectElement.val('?');
- unknownOption.prop('selected', true); // needed for IE
- unknownOption.attr('selected', true);
- };
-
- var removeUnknownOption = function() {
- unknownOption.remove();
+ // Overwrite the implementation. ngOptions doesn't use hashes
+ selectCtrl.generateUnknownOptionValue = function(val) {
+ return '?';
};
// Update the controller methods for multiple selectable options
if (!multiple) {
selectCtrl.writeValue = function writeNgOptionsValue(value) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+
+ var selectedOption = selectElement[0].options[selectElement[0].selectedIndex];
var option = options.getOptionFromViewValue(value);
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ if (selectedOption) selectedOption.removeAttribute('selected');
+
if (option) {
// Don't update the option when it is already selected.
// For example, the browser will select the first option by default. In that case,
@@ -28869,8 +20158,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// set always
if (selectElement[0].value !== option.selectValue) {
- removeUnknownOption();
- removeEmptyOption();
+ selectCtrl.removeUnknownOption();
selectElement[0].value = option.selectValue;
option.element.selected = true;
@@ -28878,13 +20166,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
option.element.setAttribute('selected', 'selected');
} else {
- if (value === null || providedEmptyOption) {
- removeUnknownOption();
- renderEmptyOption();
- } else {
- removeEmptyOption();
- renderUnknownOption();
- }
+ selectCtrl.selectUnknownOrEmptyOption(value);
}
};
@@ -28893,8 +20175,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var selectedOption = options.selectValueMap[selectElement.val()];
if (selectedOption && !selectedOption.disabled) {
- removeEmptyOption();
- removeUnknownOption();
+ selectCtrl.unselectEmptyOption();
+ selectCtrl.removeUnknownOption();
return options.getViewValueFromOption(selectedOption);
}
return null;
@@ -28902,6 +20184,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// If we are using `track by` then we must watch the tracked value on the model
// since ngModel only watches for object identity change
+ // FIXME: When a user selects an option, this watch will fire needlessly
if (ngOptions.trackBy) {
scope.$watch(
function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
@@ -28911,22 +20194,19 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
} else {
- ngModelCtrl.$isEmpty = function(value) {
- return !value || value.length === 0;
- };
+ selectCtrl.writeValue = function writeNgOptionsMultiple(values) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+ // Only set `<option>.selected` if necessary, in order to prevent some browsers from
+ // scrolling to `<option>` elements that are outside the `<select>` element's viewport.
+ var selectedOptions = values && values.map(getAndUpdateSelectedOption) || [];
- selectCtrl.writeValue = function writeNgOptionsMultiple(value) {
options.items.forEach(function(option) {
- option.element.selected = false;
+ if (option.element.selected && !includes(selectedOptions, option)) {
+ option.element.selected = false;
+ }
});
-
- if (value) {
- value.forEach(function(item) {
- var option = options.getOptionFromViewValue(item);
- if (option) option.element.selected = true;
- });
- }
};
@@ -28959,28 +20239,47 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
}
}
-
if (providedEmptyOption) {
- // we need to remove it before calling selectElement.empty() because otherwise IE will
- // remove the label from the element. wtf?
- emptyOption.remove();
-
// compile the element since there might be bindings in it
- $compile(emptyOption)(scope);
+ $compile(selectCtrl.emptyOption)(scope);
- // remove the class, which is added automatically because we recompile the element and it
- // becomes the compilation root
- emptyOption.removeClass('ng-scope');
- } else {
- emptyOption = jqLite(optionTemplate.cloneNode(false));
- }
+ selectElement.prepend(selectCtrl.emptyOption);
- selectElement.empty();
+ if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
+ // This means the empty option has currently no actual DOM node, probably because
+ // it has been modified by a transclusion directive.
+ selectCtrl.hasEmptyOption = false;
- // We need to do this here to ensure that the options object is defined
- // when we first hit it in writeNgOptionsValue
- updateOptions();
+ // Redefine the registerOption function, which will catch
+ // options that are added by ngIf etc. (rendering of the node is async because of
+ // lazy transclusion)
+ selectCtrl.registerOption = function(optionScope, optionEl) {
+ if (optionEl.val() === '') {
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = optionEl;
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ // This ensures the new empty option is selected if previously no option was selected
+ ngModelCtrl.$render();
+
+ optionEl.on('$destroy', function() {
+ var needsRerender = selectCtrl.$isEmptyOptionSelected();
+
+ selectCtrl.hasEmptyOption = false;
+ selectCtrl.emptyOption = undefined;
+
+ if (needsRerender) ngModelCtrl.$render();
+ });
+ }
+ };
+
+ } else {
+ // remove the class, which is added automatically because we recompile the element and it
+ // becomes the compilation root
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ }
+
+ }
// We will re-render the option elements if the option values or labels change
scope.$watchCollection(ngOptions.getWatchables, updateOptions);
@@ -28993,11 +20292,20 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
updateOptionElement(option, optionElement);
}
+ function getAndUpdateSelectedOption(viewValue) {
+ var option = options.getOptionFromViewValue(viewValue);
+ var element = option && option.element;
+
+ if (element && !element.selected) element.selected = true;
+
+ return option;
+ }
function updateOptionElement(option, element) {
option.element = element;
element.disabled = option.disabled;
- // NOTE: The label must be set before the value, otherwise IE10/11/EDGE create unresponsive
+ // Support: IE 11 only, Edge 12-13 only
+ // NOTE: The label must be set before the value, otherwise IE 11 & Edge create unresponsive
// selects in certain circumstances when multiple selects are next to each other and display
// the option list in listbox style, i.e. the select is [multiple], or specifies a [size].
// See https://github.com/angular/angular.js/issues/11314 for more info.
@@ -29006,7 +20314,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
element.label = option.label;
element.textContent = option.label;
}
- if (option.value !== element.value) element.value = option.selectValue;
+ element.value = option.selectValue;
}
function updateOptions() {
@@ -29033,11 +20341,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var groupElementMap = {};
- // Ensure that the empty option is always there if it was explicitly provided
- if (providedEmptyOption) {
- selectElement.prepend(emptyOption);
- }
-
options.items.forEach(function addOption(option) {
var groupElement;
@@ -29082,7 +20385,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
ngModelCtrl.$render();
}
}
-
}
}
@@ -29110,27 +20412,27 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* @description
* `ngPluralize` is a directive that displays messages according to en-US localization rules.
* These rules are bundled with angular.js, but can be overridden
- * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive
+ * (see {@link guide/i18n AngularJS i18n} dev guide). You configure ngPluralize directive
* by specifying the mappings between
* [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
* and the strings to be displayed.
*
- * # Plural categories and explicit number rules
+ * ## Plural categories and explicit number rules
* There are two
* [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
- * in Angular's default en-US locale: "one" and "other".
+ * in AngularJS's default en-US locale: "one" and "other".
*
* While a plural category may match many numbers (for example, in en-US locale, "other" can match
* any number that is not 1), an explicit number rule can only match one number. For example, the
* explicit number rule for "3" matches the number 3. There are examples of plural categories
* and explicit number rules throughout the rest of this documentation.
*
- * # Configuring ngPluralize
+ * ## Configuring ngPluralize
* You configure ngPluralize by providing 2 attributes: `count` and `when`.
* You can also provide an optional attribute, `offset`.
*
* The value of the `count` attribute can be either a string or an {@link guide/expression
- * Angular expression}; these are evaluated on the current scope for its bound value.
+ * AngularJS expression}; these are evaluated on the current scope for its bound value.
*
* The `when` attribute specifies the mappings between plural categories and the actual
* string to be displayed. The value of the attribute should be a JSON object.
@@ -29152,14 +20454,14 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* show "a dozen people are viewing".
*
* You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted
- * into pluralized strings. In the previous example, Angular will replace `{}` with
+ * into pluralized strings. In the previous example, AngularJS will replace `{}` with
* <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder
* for <span ng-non-bindable>{{numberExpression}}</span>.
*
* If no rule is defined for a category, then an empty string is displayed and a warning is generated.
* Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`.
*
- * # Configuring ngPluralize with offset
+ * ## Configuring ngPluralize with offset
* The `offset` attribute allows further customization of pluralized text, which can result in
* a better user experience. For example, instead of the message "4 people are viewing this document",
* you might display "John, Kate and 2 others are viewing this document".
@@ -29180,7 +20482,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* three explicit number rules 0, 1 and 2.
* When one person, perhaps John, views the document, "John is viewing" will be shown.
* When three people view the document, no explicit number rule is found, so
- * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category.
+ * an offset of 2 is taken off 3, and AngularJS uses 1 to decide the plural category.
* In this case, plural category 'one' is matched and "John, Mary and one other person are viewing"
* is shown.
*
@@ -29194,7 +20496,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
* @param {number=} offset Offset to deduct from the total number.
*
* @example
- <example module="pluralizeExample">
+ <example module="pluralizeExample" name="ng-pluralize">
<file name="index.html">
<script>
angular.module('pluralizeExample', [])
@@ -29308,7 +20610,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) {
var count = parseFloat(newVal);
- var countIsNaN = isNaN(count);
+ var countIsNaN = isNumberNaN(count);
if (!countIsNaN && !(count in whens)) {
// If an explicit number rule such as 1, 2, 3... is defined, just use it.
@@ -29318,12 +20620,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
// If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
// In JS `NaN !== NaN`, so we have to explicitly check.
- if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
+ if ((count !== lastCount) && !(countIsNaN && isNumberNaN(lastCount))) {
watchRemover();
var whenExpFn = whensExpFns[count];
if (isUndefined(whenExpFn)) {
if (newVal != null) {
- $log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp);
+ $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp);
}
watchRemover = noop;
updateElementText();
@@ -29341,10 +20643,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
};
}];
+/* exported ngRepeatDirective */
+
/**
* @ngdoc directive
* @name ngRepeat
* @multiElement
+ * @restrict A
*
* @description
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
@@ -29368,7 +20673,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* </div>
*
*
- * # Iterating over object properties
+ * ## Iterating over object properties
*
* It is possible to get `ngRepeat` to iterate over the properties of an object using the following
* syntax:
@@ -29377,17 +20682,17 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* <div ng-repeat="(key, value) in myObj"> ... </div>
* ```
*
- * However, there are a limitations compared to array iteration:
+ * However, there are a few limitations compared to array iteration:
*
* - The JavaScript specification does not define the order of keys
- * returned for an object, so Angular relies on the order returned by the browser
+ * returned for an object, so AngularJS relies on the order returned by the browser
* when running `for key in myObj`. Browsers generally follow the strategy of providing
* keys in the order in which they were defined, although there are exceptions when keys are deleted
* and reinstated. See the
* [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
*
* - `ngRepeat` will silently *ignore* object keys starting with `$`, because
- * it's a prefix used by Angular for public (`$`) and private (`$$`) properties.
+ * it's a prefix used by AngularJS for public (`$`) and private (`$$`) properties.
*
* - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
* objects, and will throw an error if used with one.
@@ -29398,10 +20703,10 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* or implement a `$watch` on the object yourself.
*
*
- * # Tracking and Duplicates
+ * ## Tracking and Duplicates
*
* `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
- * the collection. When a change happens, ngRepeat then makes the corresponding changes to the DOM:
+ * the collection. When a change happens, `ngRepeat` then makes the corresponding changes to the DOM:
*
* * When an item is added, a new instance of the template is added to the DOM.
* * When an item is removed, its template instance is removed from the DOM.
@@ -29409,64 +20714,153 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
*
* To minimize creation of DOM elements, `ngRepeat` uses a function
* to "keep track" of all items in the collection and their corresponding DOM elements.
- * For example, if an item is added to the collection, ngRepeat will know that all other items
+ * For example, if an item is added to the collection, `ngRepeat` will know that all other items
* already have DOM elements, and will not re-render them.
*
- * The default tracking function (which tracks items by their identity) does not allow
- * duplicate items in arrays. This is because when there are duplicates, it is not possible
- * to maintain a one-to-one mapping between collection items and DOM elements.
+ * All different types of tracking functions, their syntax, and their support for duplicate
+ * items in collections can be found in the
+ * {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
*
- * If you do need to repeat duplicate items, you can substitute the default tracking behavior
- * with your own using the `track by` expression.
+ * <div class="alert alert-success">
+ * **Best Practice:** If you are working with objects that have a unique identifier property, you
+ * should track by this identifier instead of the object instance,
+ * e.g. `item in items track by item.id`.
+ * Should you reload your data later, `ngRepeat` will not have to rebuild the DOM elements for items
+ * it has already rendered, even if the JavaScript objects in the collection have been substituted
+ * for new ones. For large collections, this significantly improves rendering performance.
+ * </div>
*
- * For example, you may track items by the index of each item in the collection, using the
- * special scope property `$index`:
- * ```html
- * <div ng-repeat="n in [42, 42, 43, 43] track by $index">
- * {{n}}
- * </div>
- * ```
+ * ### Effects of DOM Element re-use
*
- * You may also use arbitrary expressions in `track by`, including references to custom functions
- * on the scope:
- * ```html
- * <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
- * {{n}}
- * </div>
- * ```
+ * When DOM elements are re-used, ngRepeat updates the scope for the element, which will
+ * automatically update any active bindings on the template. However, other
+ * functionality will not be updated, because the element is not re-created:
*
- * <div class="alert alert-success">
- * If you are working with objects that have an identifier property, you should track
- * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
- * will not have to rebuild the DOM elements for items it has already rendered, even if the
- * JavaScript objects in the collection have been substituted for new ones. For large collections,
- * this significantly improves rendering performance. If you don't have a unique identifier,
- * `track by $index` can also provide a performance boost.
- * </div>
- * ```html
- * <div ng-repeat="model in collection track by model.id">
- * {{model.name}}
- * </div>
- * ```
+ * - Directives are not re-compiled
+ * - {@link guide/expression#one-time-binding one-time expressions} on the repeated template are not
+ * updated if they have stabilized.
*
- * When no `track by` expression is provided, it is equivalent to tracking by the built-in
- * `$id` function, which tracks items by their identity:
- * ```html
- * <div ng-repeat="obj in collection track by $id(obj)">
- * {{obj.prop}}
- * </div>
- * ```
+ * The above affects all kinds of element re-use due to tracking, but may be especially visible
+ * when tracking by `$index` due to the way ngRepeat re-uses elements.
*
- * <div class="alert alert-warning">
- * **Note:** `track by` must always be the last expression:
- * </div>
- * ```
- * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id">
- * {{model.name}}
- * </div>
- * ```
+ * The following example shows the effects of different actions with tracking:
+
+ <example module="ngRepeat" name="ngRepeat-tracking" deps="angular-animate.js" animations="true">
+ <file name="script.js">
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ var friends = [
+ {name:'John', age:25},
+ {name:'Mary', age:40},
+ {name:'Peter', age:85}
+ ];
+
+ $scope.removeFirst = function() {
+ $scope.friends.shift();
+ };
+
+ $scope.updateAge = function() {
+ $scope.friends.forEach(function(el) {
+ el.age = el.age + 5;
+ });
+ };
+
+ $scope.copy = function() {
+ $scope.friends = angular.copy($scope.friends);
+ };
+
+ $scope.reset = function() {
+ $scope.friends = angular.copy(friends);
+ };
+
+ $scope.reset();
+ });
+ </file>
+ <file name="index.html">
+ <div ng-controller="repeatController">
+ <ol>
+ <li>When you click "Update Age", only the first list updates the age, because all others have
+ a one-time binding on the age property. If you then click "Copy", the current friend list
+ is copied, and now the second list updates the age, because the identity of the collection items
+ has changed and the list must be re-rendered. The 3rd and 4th list stay the same, because all the
+ items are already known according to their tracking functions.
+ </li>
+ <li>When you click "Remove First", the 4th list has the wrong age on both remaining items. This is
+ due to tracking by $index: when the first collection item is removed, ngRepeat reuses the first
+ DOM element for the new first collection item, and so on. Since the age property is one-time
+ bound, the value remains from the collection item which was previously at this index.
+ </li>
+ </ol>
+
+ <button ng-click="removeFirst()">Remove First</button>
+ <button ng-click="updateAge()">Update Age</button>
+ <button ng-click="copy()">Copy</button>
+ <br><button ng-click="reset()">Reset List</button>
+ <br>
+ <code>track by $id(friend)</code> (default):
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends">
+ {{friend.name}} is {{friend.age}} years old.
+ </li>
+ </ul>
+ <code>track by $id(friend)</code> (default), with age one-time binding:
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends">
+ {{friend.name}} is {{::friend.age}} years old.
+ </li>
+ </ul>
+ <code>track by friend.name</code>, with age one-time binding:
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends track by friend.name">
+ {{friend.name}} is {{::friend.age}} years old.
+ </li>
+ </ul>
+ <code>track by $index</code>, with age one-time binding:
+ <ul class="example-animate-container">
+ <li class="animate-repeat" ng-repeat="friend in friends track by $index">
+ {{friend.name}} is {{::friend.age}} years old.
+ </li>
+ </ul>
+ </div>
+ </file>
+ <file name="animations.css">
+ .example-animate-container {
+ background:white;
+ border:1px solid black;
+ list-style:none;
+ margin:0;
+ padding:0 10px;
+ }
+
+ .animate-repeat {
+ line-height:30px;
+ list-style:none;
+ box-sizing:border-box;
+ }
+
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter,
+ .animate-repeat.ng-leave {
+ transition:all linear 0.5s;
+ }
+
+ .animate-repeat.ng-leave.ng-leave-active,
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter {
+ opacity:0;
+ max-height:0;
+ }
+
+ .animate-repeat.ng-leave,
+ .animate-repeat.ng-move.ng-move-active,
+ .animate-repeat.ng-enter.ng-enter-active {
+ opacity:1;
+ max-height:30px;
+ }
+ </file>
+ </example>
+
*
- * # Special repeat start and end points
+ * ## Special repeat start and end points
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
* the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
* The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on)
@@ -29541,22 +20935,38 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
* mapped to the same DOM element, which is not possible.)
*
- * Note that the tracking expression must come last, after any filters, and the alias expression.
- *
- * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements
- * will be associated by item identity in the array.
- *
- * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
- * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
- * with the corresponding item in the array by identity. Moving the same object in array would move the DOM
- * element in the same way in the DOM.
- *
- * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
- * case the object identity does not matter. Two objects are considered equivalent as long as their `id`
- * property is same.
+ * *Default tracking: $id()*: `item in items` is equivalent to `item in items track by $id(item)`.
+ * This implies that the DOM elements will be associated by item identity in the collection.
+ *
+ * The built-in `$id()` function can be used to assign a unique
+ * `$$hashKey` property to each item in the collection. This property is then used as a key to associated DOM elements
+ * with the corresponding item in the collection by identity. Moving the same object would move
+ * the DOM element in the same way in the DOM.
+ * Note that the default id function does not support duplicate primitive values (`number`, `string`),
+ * but supports duplictae non-primitive values (`object`) that are *equal* in shape.
+ *
+ * *Custom Expression*: It is possible to use any AngularJS expression to compute the tracking
+ * id, for example with a function, or using a property on the collection items.
+ * `item in items track by item.id` is a typical pattern when the items have a unique identifier,
+ * e.g. database id. In this case the object identity does not matter. Two objects are considered
+ * equivalent as long as their `id` property is same.
+ * Tracking by unique identifier is the most performant way and should be used whenever possible.
+ *
+ * *$index*: This special property tracks the collection items by their index, and
+ * re-uses the DOM elements that match that index, e.g. `item in items track by $index`. This can
+ * be used for a performance improvement if no unique identfier is available and the identity of
+ * the collection items cannot be easily computed. It also allows duplicates.
+ *
+ * <div class="alert alert-warning">
+ * <strong>Note:</strong> Re-using DOM elements can have unforeseen effects. Read the
+ * {@link ngRepeat#tracking-and-duplicates section on tracking and duplicates} for
+ * more info.
+ * </div>
*
- * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
- * to items in conjunction with a tracking expression.
+ * <div class="alert alert-warning">
+ * <strong>Note:</strong> the `track by` expression must come last - after any filters, and the alias expression:
+ * `item in items | filter:searchText as results track by item.id`
+ * </div>
*
* * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
* intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
@@ -29565,24 +20975,24 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
* the items have been processed through the filter.
*
- * Please note that `as [variable name] is not an operator but rather a part of ngRepeat micro-syntax so it can be used only at the end
- * (and not as operator, inside an expression).
+ * Please note that `as [variable name]` is not an operator but rather a part of ngRepeat
+ * micro-syntax so it can be used only after all filters (and not as operator, inside an expression).
*
- * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` .
+ * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results track by item.id` .
*
* @example
* This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
- * results by name. New (entering) and removed (leaving) items are animated.
+ * results by name or by age. New (entering) and removed (leaving) items are animated.
<example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true">
<file name="index.html">
<div ng-controller="repeatController">
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
<ul class="example-animate-container">
- <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
+ <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results track by friend.name">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
</li>
- <li class="animate-repeat" ng-if="results.length == 0">
+ <li class="animate-repeat" ng-if="results.length === 0">
<strong>No results found...</strong>
</li>
</ul>
@@ -29675,9 +21085,8 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
scope.$first = (index === 0);
scope.$last = (index === (arrayLength - 1));
scope.$middle = !(scope.$first || scope.$last);
- // jshint bitwise: false
- scope.$odd = !(scope.$even = (index&1) === 0);
- // jshint bitwise: true
+ // eslint-disable-next-line no-bitwise
+ scope.$odd = !(scope.$even = (index & 1) === 0);
};
var getBlockStart = function(block) {
@@ -29688,6 +21097,13 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
return block.clone[block.clone.length - 1];
};
+ var trackByIdArrayFn = function($scope, key, value) {
+ return hashKey(value);
+ };
+
+ var trackByIdObjFn = function($scope, key) {
+ return key;
+ };
return {
restrict: 'A',
@@ -29703,7 +21119,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if (!match) {
- throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
+ throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.',
expression);
}
@@ -29712,10 +21128,10 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
var aliasAs = match[3];
var trackByExp = match[4];
- match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);
+ match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);
if (!match) {
- throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
+ throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.',
lhs);
}
var valueIdentifier = match[3] || match[1];
@@ -29723,40 +21139,31 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
- throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.",
+ throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.',
aliasAs);
}
- var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
- var hashFnLocals = {$id: hashKey};
+ var trackByIdExpFn;
if (trackByExp) {
- trackByExpGetter = $parse(trackByExp);
- } else {
- trackByIdArrayFn = function(key, value) {
- return hashKey(value);
- };
- trackByIdObjFn = function(key) {
- return key;
+ var hashFnLocals = {$id: hashKey};
+ var trackByExpGetter = $parse(trackByExp);
+
+ trackByIdExpFn = function($scope, key, value, index) {
+ // assign key, value, and $index to the locals so that they can be used in hash functions
+ if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
+ hashFnLocals[valueIdentifier] = value;
+ hashFnLocals.$index = index;
+ return trackByExpGetter($scope, hashFnLocals);
};
}
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
- if (trackByExpGetter) {
- trackByIdExpFn = function(key, value, index) {
- // assign key, value, and $index to the locals so that they can be used in hash functions
- if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
- hashFnLocals[valueIdentifier] = value;
- hashFnLocals.$index = index;
- return trackByExpGetter($scope, hashFnLocals);
- };
- }
-
// Store a list of elements from previous run. This is a hash where key is the item from the
// iterator, and the value is objects with following properties.
// - scope: bound scope
- // - element: previous element.
+ // - clone: previous element.
// - index: position
//
// We are using no-proto object so that we don't need to guard against inherited props via
@@ -29806,7 +21213,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
for (index = 0; index < collectionLength; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
- trackById = trackByIdFn(key, value, index);
+ trackById = trackByIdFn($scope, key, value, index);
if (lastBlockMap[trackById]) {
// found previously seen block
block = lastBlockMap[trackById];
@@ -29819,7 +21226,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
if (block && block.scope) lastBlockMap[block.id] = block;
});
throw ngRepeatMinErr('dupes',
- "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
+ 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}',
expression, trackById, value);
} else {
// new never before seen block
@@ -29828,6 +21235,12 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
}
}
+ // Clear the value property from the hashFnLocals object to prevent a reference to the last value
+ // being leaked into the ngRepeatCompile function scope
+ if (hashFnLocals) {
+ hashFnLocals[valueIdentifier] = undefined;
+ }
+
// remove leftover items
for (var blockKey in lastBlockMap) {
block = lastBlockMap[blockKey];
@@ -29860,7 +21273,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
nextNode = nextNode.nextSibling;
} while (nextNode && nextNode[NG_REMOVED]);
- if (getBlockStart(block) != nextNode) {
+ if (getBlockStart(block) !== nextNode) {
// existing item which got moved
$animate.move(getBlockNodes(block.clone), null, previousNode);
}
@@ -29900,11 +21313,13 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* @multiElement
*
* @description
- * The `ngShow` directive shows or hides the given HTML element based on the expression
- * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding
- * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngShow` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngShow` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
* <!-- when $scope.myValue is truthy (element is visible) -->
@@ -29914,31 +21329,32 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* <div ng-show="myValue" class="ng-hide"></div>
* ```
*
- * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class
- * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When truthy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
- * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
- * class CSS. Note that the selector that needs to be used is actually `.ng-hide:not(.ng-hide-animate)` to cope
- * with extra animation classes that can be added.
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
* ```css
* .ng-hide:not(.ng-hide-animate) {
- * /&#42; this is just another form of hiding an element &#42;/
+ * /&#42; These are just alternative ways of hiding an element &#42;/
* display: block!important;
* position: absolute;
* top: -9999px;
@@ -29946,29 +21362,24 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* }
* ```
*
- * By default you don't need to override in CSS anything and the animations will work around the display style.
+ * By default you don't need to override anything in CSS and the animations will work around the
+ * display style.
*
- * ## A note about animations with `ngShow`
+ * @animations
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngShow` expression evaluates to a truthy value and just before contents are set to visible. |
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass except that
- * you must also include the !important flag to override the display property
- * so that you can perform an animation when the element is hidden during the time of the animation.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /&#42; A working example can be found at the bottom of this page. &#42;/
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * /&#42; this is required as of 1.3x to properly
- * apply all styling in a show/hide animation &#42;/
- * transition: 0s linear all;
- * }
- *
- * .my-element.ng-hide-add-active,
- * .my-element.ng-hide-remove-active {
- * /&#42; the transition is defined in the active class &#42;/
- * transition: 1s linear all;
+ * transition: all 0.5s linear;
* }
*
* .my-element.ng-hide-add { ... }
@@ -29977,79 +21388,124 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
- * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden |
- * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngShow` expression evaluates to a truthy value and just before contents are set to visible |
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngShow If the {@link guide/expression expression} is truthy
- * then the element is shown or hidden respectively.
+ * @param {expression} ngShow If the {@link guide/expression expression} is truthy/falsy then the
+ * element is shown/hidden respectively.
*
* @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * A simple example, animating the element's opacity:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-simple">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/>
- <div>
- Show:
- <div class="check-element animate-show" ng-show="checked">
- <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked.
- </div>
- </div>
- <div>
- Hide:
- <div class="check-element animate-show" ng-hide="checked">
- <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked.
- </div>
+ Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br />
+ <div class="check-element animate-show-hide" ng-show="checked">
+ I show up when your checkbox is checked.
</div>
</file>
- <file name="glyphicons.css">
- @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
- </file>
<file name="animations.css">
- .animate-show {
- line-height: 20px;
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
+ transition: all linear 0.5s;
+ }
+
+ .check-element {
+ border: 1px solid black;
opacity: 1;
padding: 10px;
- border: 1px solid black;
- background: white;
}
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- .animate-show.ng-hide-add, .animate-show.ng-hide-remove {
- transition: all linear 0.5s;
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
+ });
+ </file>
+ </example>
+ *
+ * <hr />
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-complex">
+ <file name="index.html">
+ Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br />
+ <div class="check-element funky-show-hide" ng-show="checked">
+ I show up when your checkbox is checked.
+ </div>
+ </file>
+ <file name="animations.css">
+ body {
+ overflow: hidden;
+ perspective: 1000px;
}
- .animate-show.ng-hide {
- line-height: 0;
- opacity: 0;
- padding: 0 10px;
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding: 10px;
border: 1px solid black;
- background: white;
+ opacity: 1;
+ padding: 10px;
}
</file>
<file name="protractor.js" type="protractor">
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
-
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- element(by.model('checked')).click();
-
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
});
</file>
</example>
+ *
+ * @knownIssue
+ *
+ * ### Flickering when using ngShow to toggle between elements
+ *
+ * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can
+ * happen that both the element to show and the element to hide are visible for a very short time.
+ *
+ * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations
+ * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than
+ * other browsers.
+ *
+ * There are several way to mitigate this problem:
+ *
+ * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}.
+ * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}.
+ * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements.
+ * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}.
+ * - Define an animation on the affected elements.
*/
var ngShowDirective = ['$animate', function($animate) {
return {
@@ -30076,11 +21532,13 @@ var ngShowDirective = ['$animate', function($animate) {
* @multiElement
*
* @description
- * The `ngHide` directive shows or hides the given HTML element based on the expression
- * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding
- * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngHide` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngHide` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
* <!-- when $scope.myValue is truthy (element is hidden) -->
@@ -30090,30 +21548,32 @@ var ngShowDirective = ['$animate', function($animate) {
* <div ng-hide="myValue"></div>
* ```
*
- * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class
- * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When falsy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
- * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
- * class in CSS:
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
* ```css
- * .ng-hide {
- * /&#42; this is just another form of hiding an element &#42;/
+ * .ng-hide:not(.ng-hide-animate) {
+ * /&#42; These are just alternative ways of hiding an element &#42;/
* display: block!important;
* position: absolute;
* top: -9999px;
@@ -30121,20 +21581,24 @@ var ngShowDirective = ['$animate', function($animate) {
* }
* ```
*
- * By default you don't need to override in CSS anything and the animations will work around the display style.
+ * By default you don't need to override in CSS anything and the animations will work around the
+ * display style.
*
- * ## A note about animations with `ngHide`
+ * @animations
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible. |
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide`
- * CSS class is added and removed for you instead of your own CSS class.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /&#42; A working example can be found at the bottom of this page. &#42;/
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * transition: 0.5s linear all;
+ * transition: all 0.5s linear;
* }
*
* .my-element.ng-hide-add { ... }
@@ -30143,77 +21607,124 @@ var ngShowDirective = ['$animate', function($animate) {
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
- * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
- *
- * @animations
- * | Animation | Occurs |
- * |----------------------------------|-------------------------------------|
- * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden |
- * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible |
- *
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngHide If the {@link guide/expression expression} is truthy then
- * the element is shown or hidden respectively.
+ * @param {expression} ngHide If the {@link guide/expression expression} is truthy/falsy then the
+ * element is hidden/shown respectively.
*
* @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * A simple example, animating the element's opacity:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-simple">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/>
- <div>
- Show:
- <div class="check-element animate-hide" ng-show="checked">
- <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked.
- </div>
- </div>
- <div>
- Hide:
- <div class="check-element animate-hide" ng-hide="checked">
- <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked.
- </div>
+ Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br />
+ <div class="check-element animate-show-hide" ng-hide="checked">
+ I hide when your checkbox is checked.
</div>
</file>
- <file name="glyphicons.css">
- @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
- </file>
<file name="animations.css">
- .animate-hide {
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
transition: all linear 0.5s;
- line-height: 20px;
+ }
+
+ .check-element {
+ border: 1px solid black;
opacity: 1;
padding: 10px;
- border: 1px solid black;
- background: white;
}
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- .animate-hide.ng-hide {
- line-height: 0;
- opacity: 0;
- padding: 0 10px;
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
+ });
+ </file>
+ </example>
+ *
+ * <hr />
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-complex">
+ <file name="index.html">
+ Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br />
+ <div class="check-element funky-show-hide" ng-hide="checked">
+ I hide when your checkbox is checked.
+ </div>
+ </file>
+ <file name="animations.css">
+ body {
+ overflow: hidden;
+ perspective: 1000px;
+ }
+
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding: 10px;
border: 1px solid black;
- background: white;
+ opacity: 1;
+ padding: 10px;
}
</file>
<file name="protractor.js" type="protractor">
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
-
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- element(by.model('checked')).click();
-
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
});
</file>
</example>
+ *
+ * @knownIssue
+ *
+ * ### Flickering when using ngHide to toggle between elements
+ *
+ * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can
+ * happen that both the element to show and the element to hide are visible for a very short time.
+ *
+ * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations
+ * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than
+ * other browsers.
+ *
+ * There are several way to mitigate this problem:
+ *
+ * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}.
+ * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}.
+ * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements.
+ * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}.
+ * - Define an animation on the affected elements.
*/
var ngHideDirective = ['$animate', function($animate) {
return {
@@ -30231,6 +21742,7 @@ var ngHideDirective = ['$animate', function($animate) {
};
}];
+
/**
* @ngdoc directive
* @name ngStyle
@@ -30255,7 +21767,7 @@ var ngHideDirective = ['$animate', function($animate) {
* See the 'background-color' style in the example below.
*
* @example
- <example>
+ <example name="ng-style">
<file name="index.html">
<input type="button" value="set color" ng-click="myStyle={color:'red'}">
<input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}">
@@ -30273,22 +21785,22 @@ var ngHideDirective = ['$animate', function($animate) {
var colorSpan = element(by.css('span'));
it('should check ng-style', function() {
- expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)');
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(0, 0, 0, 1\)|rgb\(0, 0, 0\)/);
element(by.css('input[value=\'set color\']')).click();
- expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)');
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(255, 0, 0, 1\)|rgb\(255, 0, 0\)/);
element(by.css('input[value=clear]')).click();
- expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)');
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(0, 0, 0, 1\)|rgb\(0, 0, 0\)/);
});
</file>
</example>
*/
var ngStyleDirective = ngDirective(function(scope, element, attr) {
- scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
+ scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
- forEach(oldStyles, function(val, style) { element.css(style, '');});
+ forEach(oldStyles, function(val, style) { element.css(style, ''); });
}
if (newStyles) element.css(newStyles);
- }, true);
+ });
});
/**
@@ -30341,14 +21853,18 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
*
* * `ngSwitchWhen`: the case statement to match against. If match then this
* case will be displayed. If the same match appears multiple times, all the
- * elements will be displayed.
+ * elements will be displayed. It is possible to associate multiple values to
+ * the same `ngSwitchWhen` by defining the optional attribute
+ * `ngSwitchWhenSeparator`. The separator will be used to split the value of
+ * the `ngSwitchWhen` attribute into multiple tokens, and the element will show
+ * if any of the `ngSwitch` evaluates to any of these tokens.
* * `ngSwitchDefault`: the default case when no other case match. If there
* are multiple default cases, all of them will be displayed when no other
* case match.
*
*
* @example
- <example module="switchExample" deps="angular-animate.js" animations="true">
+ <example module="switchExample" deps="angular-animate.js" animations="true" name="ng-switch">
<file name="index.html">
<div ng-controller="ExampleController">
<select ng-model="selection" ng-options="item for item in items">
@@ -30357,7 +21873,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
<hr/>
<div class="animate-switch-container"
ng-switch on="selection">
- <div class="animate-switch" ng-switch-when="settings">Settings Div</div>
+ <div class="animate-switch" ng-switch-when="settings|options" ng-switch-when-separator="|">Settings Div</div>
<div class="animate-switch" ng-switch-when="home">Home Span</div>
<div class="animate-switch" ng-switch-default>default</div>
</div>
@@ -30366,7 +21882,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
<file name="script.js">
angular.module('switchExample', ['ngAnimate'])
.controller('ExampleController', ['$scope', function($scope) {
- $scope.items = ['settings', 'home', 'other'];
+ $scope.items = ['settings', 'home', 'options', 'other'];
$scope.selection = $scope.items[0];
}]);
</file>
@@ -30413,8 +21929,12 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
select.all(by.css('option')).get(1).click();
expect(switchElem.getText()).toMatch(/Home Span/);
});
- it('should select default', function() {
+ it('should change to settings via "options"', function() {
select.all(by.css('option')).get(2).click();
+ expect(switchElem.getText()).toMatch(/Settings Div/);
+ });
+ it('should select default', function() {
+ select.all(by.css('option')).get(3).click();
expect(switchElem.getText()).toMatch(/default/);
});
</file>
@@ -30425,7 +21945,7 @@ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
require: 'ngSwitch',
// asks for $scope to fool the BC controller module
- controller: ['$scope', function ngSwitchController() {
+ controller: ['$scope', function NgSwitchController() {
this.cases = {};
}],
link: function(scope, element, attr, ngSwitchController) {
@@ -30436,21 +21956,24 @@ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
selectedScopes = [];
var spliceFactory = function(array, index) {
- return function() { array.splice(index, 1); };
+ return function(response) {
+ if (response !== false) array.splice(index, 1);
+ };
};
scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
var i, ii;
- for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
- $animate.cancel(previousLeaveAnimations[i]);
+
+ // Start with the last, in case the array is modified during the loop
+ while (previousLeaveAnimations.length) {
+ $animate.cancel(previousLeaveAnimations.pop());
}
- previousLeaveAnimations.length = 0;
for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
var selected = getBlockNodes(selectedElements[i].clone);
selectedScopes[i].$destroy();
- var promise = previousLeaveAnimations[i] = $animate.leave(selected);
- promise.then(spliceFactory(previousLeaveAnimations, i));
+ var runner = previousLeaveAnimations[i] = $animate.leave(selected);
+ runner.done(spliceFactory(previousLeaveAnimations, i));
}
selectedElements.length = 0;
@@ -30480,8 +22003,16 @@ var ngSwitchWhenDirective = ngDirective({
require: '^ngSwitch',
multiElement: true,
link: function(scope, element, attrs, ctrl, $transclude) {
- ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []);
- ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element });
+
+ var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter(
+ // Filter duplicate cases
+ function(element, index, array) { return array[index - 1] !== element; }
+ );
+
+ forEach(cases, function(whenCase) {
+ ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []);
+ ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element });
+ });
}
});
@@ -30509,8 +22040,8 @@ var ngSwitchDefaultDirective = ngDirective({
*
* If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing
* content of this element will be removed before the transcluded content is inserted.
- * If the transcluded content is empty, the existing content is left intact. This lets you provide fallback content in the case
- * that no transcluded content is provided.
+ * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback
+ * content in the case that no transcluded content is provided.
*
* @element ANY
*
@@ -30543,7 +22074,7 @@ var ngSwitchDefaultDirective = ngDirective({
* <div ng-controller="ExampleController">
* <input ng-model="title" aria-label="title"> <br/>
* <textarea ng-model="text" aria-label="text"></textarea> <br/>
- * <pane title="{{title}}">{{text}}</pane>
+ * <pane title="{{title}}"><span>{{text}}</span></pane>
* </div>
* </file>
* <file name="protractor.js" type="protractor">
@@ -30565,7 +22096,7 @@ var ngSwitchDefaultDirective = ngDirective({
* This example shows how to use `NgTransclude` with fallback content, that
* is displayed if no transcluded content is provided.
*
- * <example module="transcludeFallbackContentExample">
+ * <example module="transcludeFallbackContentExample" name="ng-transclude">
* <file name="index.html">
* <script>
* angular.module('transcludeFallbackContentExample', [])
@@ -30618,7 +22149,7 @@ var ngSwitchDefaultDirective = ngDirective({
* </file>
* <file name="app.js">
* angular.module('multiSlotTranscludeExample', [])
- * .directive('pane', function(){
+ * .directive('pane', function() {
* return {
* restrict: 'E',
* transclude: {
@@ -30635,7 +22166,7 @@ var ngSwitchDefaultDirective = ngDirective({
* })
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.title = 'Lorem Ipsum';
- * $scope.link = "https://google.com";
+ * $scope.link = 'https://google.com';
* $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
* }]);
* </file>
@@ -30658,7 +22189,6 @@ var ngTranscludeMinErr = minErr('ngTransclude');
var ngTranscludeDirective = ['$compile', function($compile) {
return {
restrict: 'EAC',
- terminal: true,
compile: function ngTranscludeCompile(tElement) {
// Remove and cache any original content to act as a fallback
@@ -30691,7 +22221,7 @@ var ngTranscludeDirective = ['$compile', function($compile) {
}
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
- if (clone.length) {
+ if (clone.length && notWhitespace(clone)) {
$element.append(clone);
} else {
useFallbackContent();
@@ -30708,6 +22238,15 @@ var ngTranscludeDirective = ['$compile', function($compile) {
$element.append(clone);
});
}
+
+ function notWhitespace(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ var node = nodes[i];
+ if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) {
+ return true;
+ }
+ }
+ }
};
}
};
@@ -30729,7 +22268,7 @@ var ngTranscludeDirective = ['$compile', function($compile) {
* @param {string} id Cache name of the template.
*
* @example
- <example>
+ <example name="script-tag">
<file name="index.html">
<script type="text/ng-template" id="/tpl.html">
Content of the template.
@@ -30751,7 +22290,7 @@ var scriptDirective = ['$templateCache', function($templateCache) {
restrict: 'E',
terminal: true,
compile: function(element, attr) {
- if (attr.type == 'text/ng-template') {
+ if (attr.type === 'text/ng-template') {
var templateUrl = attr.id,
text = element[0].text;
@@ -30761,80 +22300,263 @@ var scriptDirective = ['$templateCache', function($templateCache) {
};
}];
+/* exported selectDirective, optionDirective */
+
var noopNgModelController = { $setViewValue: noop, $render: noop };
-function chromeHack(optionElement) {
- // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
- // Adding an <option selected="selected"> element to a <select required="required"> should
- // automatically select the new element
- if (optionElement[0].hasAttribute('selected')) {
- optionElement[0].selected = true;
- }
+function setOptionSelectedStatus(optionEl, value) {
+ optionEl.prop('selected', value);
+ /**
+ * When unselecting an option, setting the property to null / false should be enough
+ * However, screenreaders might react to the selected attribute instead, see
+ * https://github.com/angular/angular.js/issues/14419
+ * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false
+ * or null
+ */
+ optionEl.attr('selected', value);
}
/**
* @ngdoc type
* @name select.SelectController
+ *
* @description
- * The controller for the `<select>` directive. This provides support for reading
- * and writing the selected value(s) of the control and also coordinates dynamically
- * added `<option>` elements, perhaps by an `ngRepeat` directive.
+ * The controller for the {@link ng.select select} directive. The controller exposes
+ * a few utility methods that can be used to augment the behavior of a regular or an
+ * {@link ng.ngOptions ngOptions} select element.
+ *
+ * @example
+ * ### Set a custom error when the unknown option is selected
+ *
+ * This example sets a custom error "unknownValue" on the ngModelController
+ * when the select element's unknown option is selected, i.e. when the model is set to a value
+ * that is not matched by any option.
+ *
+ * <example name="select-unknown-value-error" module="staticSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="testSelect"> Single select: </label><br>
+ * <select name="testSelect" ng-model="selected" unknown-value-error>
+ * <option value="option-1">Option 1</option>
+ * <option value="option-2">Option 2</option>
+ * </select><br>
+ * <span class="error" ng-if="myForm.testSelect.$error.unknownValue">
+ * Error: The current model doesn't match any option</span><br>
+ *
+ * <button ng-click="forceUnknownOption()">Force unknown option</button><br>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.selected = null;
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.selected = 'nonsense';
+ * };
+ * }])
+ * .directive('unknownValueError', function() {
+ * return {
+ * require: ['ngModel', 'select'],
+ * link: function(scope, element, attrs, ctrls) {
+ * var ngModelCtrl = ctrls[0];
+ * var selectCtrl = ctrls[1];
+ *
+ * ngModelCtrl.$validators.unknownValue = function(modelValue, viewValue) {
+ * if (selectCtrl.$isUnknownOptionSelected()) {
+ * return false;
+ * }
+ *
+ * return true;
+ * };
+ * }
+ *
+ * };
+ * });
+ * </file>
+ *</example>
+ *
+ *
+ * @example
+ * ### Set the "required" error when the unknown option is selected.
+ *
+ * By default, the "required" error on the ngModelController is only set on a required select
+ * when the empty option is selected. This example adds a custom directive that also sets the
+ * error when the unknown option is selected.
+ *
+ * <example name="select-unknown-value-required" module="staticSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="testSelect"> Select: </label><br>
+ * <select name="testSelect" ng-model="selected" required unknown-value-required>
+ * <option value="option-1">Option 1</option>
+ * <option value="option-2">Option 2</option>
+ * </select><br>
+ * <span class="error" ng-if="myForm.testSelect.$error.required">Error: Please select a value</span><br>
+ *
+ * <button ng-click="forceUnknownOption()">Force unknown option</button><br>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.selected = null;
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.selected = 'nonsense';
+ * };
+ * }])
+ * .directive('unknownValueRequired', function() {
+ * return {
+ * priority: 1, // This directive must run after the required directive has added its validator
+ * require: ['ngModel', 'select'],
+ * link: function(scope, element, attrs, ctrls) {
+ * var ngModelCtrl = ctrls[0];
+ * var selectCtrl = ctrls[1];
+ *
+ * var originalRequiredValidator = ngModelCtrl.$validators.required;
+ *
+ * ngModelCtrl.$validators.required = function() {
+ * if (attrs.required && selectCtrl.$isUnknownOptionSelected()) {
+ * return false;
+ * }
+ *
+ * return originalRequiredValidator.apply(this, arguments);
+ * };
+ * }
+ * };
+ * });
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should show the error message when the unknown option is selected', function() {
+
+ var error = element(by.className('error'));
+
+ expect(error.getText()).toBe('Error: Please select a value');
+
+ element(by.cssContainingText('option', 'Option 1')).click();
+
+ expect(error.isPresent()).toBe(false);
+
+ element(by.tagName('button')).click();
+
+ expect(error.getText()).toBe('Error: Please select a value');
+ });
+ * </file>
+ *</example>
+ *
+ *
*/
var SelectController =
- ['$element', '$scope', function($element, $scope) {
+ ['$element', '$scope', /** @this */ function($element, $scope) {
var self = this,
- optionsMap = new HashMap();
+ optionsMap = new NgMap();
+
+ self.selectValueMap = {}; // Keys are the hashed values, values the original values
// If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
self.ngModelCtrl = noopNgModelController;
+ self.multiple = false;
// The "unknown" option is one that is prepended to the list if the viewValue
// does not match any of the options. When it is rendered the value of the unknown
// option is '? XXX ?' where XXX is the hashKey of the value that is not known.
//
+ // Support: IE 9 only
// We can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
self.unknownOption = jqLite(window.document.createElement('option'));
+
+ // The empty option is an option with the value '' that the application developer can
+ // provide inside the select. It is always selectable and indicates that a "null" selection has
+ // been made by the user.
+ // If the select has an empty option, and the model of the select is set to "undefined" or "null",
+ // the empty option is selected.
+ // If the model is set to a different unmatched value, the unknown option is rendered and
+ // selected, i.e both are present, because a "null" selection and an unknown value are different.
+ self.hasEmptyOption = false;
+ self.emptyOption = undefined;
+
self.renderUnknownOption = function(val) {
- var unknownVal = '? ' + hashKey(val) + ' ?';
+ var unknownVal = self.generateUnknownOptionValue(val);
self.unknownOption.val(unknownVal);
$element.prepend(self.unknownOption);
+ setOptionSelectedStatus(self.unknownOption, true);
$element.val(unknownVal);
};
- $scope.$on('$destroy', function() {
- // disable unknown option so that we don't do work when the whole select is being destroyed
- self.renderUnknownOption = noop;
- });
+ self.updateUnknownOption = function(val) {
+ var unknownVal = self.generateUnknownOptionValue(val);
+ self.unknownOption.val(unknownVal);
+ setOptionSelectedStatus(self.unknownOption, true);
+ $element.val(unknownVal);
+ };
+
+ self.generateUnknownOptionValue = function(val) {
+ return '? ' + hashKey(val) + ' ?';
+ };
self.removeUnknownOption = function() {
if (self.unknownOption.parent()) self.unknownOption.remove();
};
+ self.selectEmptyOption = function() {
+ if (self.emptyOption) {
+ $element.val('');
+ setOptionSelectedStatus(self.emptyOption, true);
+ }
+ };
+
+ self.unselectEmptyOption = function() {
+ if (self.hasEmptyOption) {
+ setOptionSelectedStatus(self.emptyOption, false);
+ }
+ };
+
+ $scope.$on('$destroy', function() {
+ // disable unknown option so that we don't do work when the whole select is being destroyed
+ self.renderUnknownOption = noop;
+ });
// Read the value of the select control, the implementation of this changes depending
// upon whether the select can have multiple values and whether ngOptions is at work.
self.readValue = function readSingleValue() {
- self.removeUnknownOption();
- return $element.val();
+ var val = $element.val();
+ // ngValue added option values are stored in the selectValueMap, normal interpolations are not
+ var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val;
+
+ if (self.hasOption(realVal)) {
+ return realVal;
+ }
+
+ return null;
};
// Write the value to the select control, the implementation of this changes depending
// upon whether the select can have multiple values and whether ngOptions is at work.
self.writeValue = function writeSingleValue(value) {
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ var currentlySelectedOption = $element[0].options[$element[0].selectedIndex];
+ if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false);
+
if (self.hasOption(value)) {
self.removeUnknownOption();
- $element.val(value);
- if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
+
+ var hashedVal = hashKey(value);
+ $element.val(hashedVal in self.selectValueMap ? hashedVal : value);
+
+ // Set selected attribute and property on selected option for screen readers
+ var selectedOption = $element[0].options[$element[0].selectedIndex];
+ setOptionSelectedStatus(jqLite(selectedOption), true);
} else {
- if (value == null && self.emptyOption) {
- self.removeUnknownOption();
- $element.val('');
- } else {
- self.renderUnknownOption(value);
- }
+ self.selectUnknownOrEmptyOption(value);
}
};
@@ -30846,12 +22568,14 @@ var SelectController =
assertNotHasOwnProperty(value, '"option value"');
if (value === '') {
+ self.hasEmptyOption = true;
self.emptyOption = element;
}
var count = optionsMap.get(value) || 0;
- optionsMap.put(value, count + 1);
- self.ngModelCtrl.$render();
- chromeHack(element);
+ optionsMap.set(value, count + 1);
+ // Only render at the end of a digest. This improves render performance when many options
+ // are added during a digest and ensures all relevant options are correctly marked as selected
+ scheduleRender();
};
// Tell the select control that an option, with the given value, has been removed
@@ -30859,12 +22583,13 @@ var SelectController =
var count = optionsMap.get(value);
if (count) {
if (count === 1) {
- optionsMap.remove(value);
+ optionsMap.delete(value);
if (value === '') {
+ self.hasEmptyOption = false;
self.emptyOption = undefined;
}
} else {
- optionsMap.put(value, count - 1);
+ optionsMap.set(value, count - 1);
}
}
};
@@ -30874,36 +22599,185 @@ var SelectController =
return !!optionsMap.get(value);
};
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$hasEmptyOption
+ *
+ * @description
+ *
+ * Returns `true` if the select element currently has an empty option
+ * element, i.e. an option that signifies that the select is empty / the selection is null.
+ *
+ */
+ self.$hasEmptyOption = function() {
+ return self.hasEmptyOption;
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isUnknownOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element's unknown option is selected. The unknown option is added
+ * and automatically selected whenever the select model doesn't match any option.
+ *
+ */
+ self.$isUnknownOptionSelected = function() {
+ // Presence of the unknown option means it is selected
+ return $element[0].options[0] === self.unknownOption[0];
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isEmptyOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element has an empty option and this empty option is currently
+ * selected. Returns `false` if the select element has no empty option or it is not selected.
+ *
+ */
+ self.$isEmptyOptionSelected = function() {
+ return self.hasEmptyOption && $element[0].options[$element[0].selectedIndex] === self.emptyOption[0];
+ };
+
+ self.selectUnknownOrEmptyOption = function(value) {
+ if (value == null && self.emptyOption) {
+ self.removeUnknownOption();
+ self.selectEmptyOption();
+ } else if (self.unknownOption.parent().length) {
+ self.updateUnknownOption(value);
+ } else {
+ self.renderUnknownOption(value);
+ }
+ };
+
+ var renderScheduled = false;
+ function scheduleRender() {
+ if (renderScheduled) return;
+ renderScheduled = true;
+ $scope.$$postDigest(function() {
+ renderScheduled = false;
+ self.ngModelCtrl.$render();
+ });
+ }
+
+ var updateScheduled = false;
+ function scheduleViewValueUpdate(renderAfter) {
+ if (updateScheduled) return;
+
+ updateScheduled = true;
+
+ $scope.$$postDigest(function() {
+ if ($scope.$$destroyed) return;
+
+ updateScheduled = false;
+ self.ngModelCtrl.$setViewValue(self.readValue());
+ if (renderAfter) self.ngModelCtrl.$render();
+ });
+ }
+
self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
- if (interpolateValueFn) {
+ if (optionAttrs.$attr.ngValue) {
+ // The value attribute is set by ngValue
+ var oldVal, hashedVal;
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
+ if (isDefined(hashedVal)) {
+ self.removeOption(oldVal);
+ delete self.selectValueMap[hashedVal];
+ removal = true;
+ }
+
+ hashedVal = hashKey(newVal);
+ oldVal = newVal;
+ self.selectValueMap[hashedVal] = newVal;
+ self.addOption(newVal, optionElement);
+ // Set the attribute directly instead of using optionAttrs.$set - this stops the observer
+ // from firing a second time. Other $observers on value will also get the result of the
+ // ngValue expression, not the hashed value
+ optionElement.attr('value', hashedVal);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
+
+ });
+ } else if (interpolateValueFn) {
// The value attribute is interpolated
- var oldVal;
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+ // This method is overwritten in ngOptions and has side-effects!
+ self.readValue();
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
if (isDefined(oldVal)) {
self.removeOption(oldVal);
+ removal = true;
}
oldVal = newVal;
self.addOption(newVal, optionElement);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
});
} else if (interpolateTextFn) {
// The text content is interpolated
optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
optionAttrs.$set('value', newVal);
+ var previouslySelected = optionElement.prop('selected');
if (oldVal !== newVal) {
self.removeOption(oldVal);
}
self.addOption(newVal, optionElement);
+
+ if (oldVal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
});
} else {
// The value attribute is static
self.addOption(optionAttrs.value, optionElement);
}
+
+ optionAttrs.$observe('disabled', function(newVal) {
+
+ // Since model updates will also select disabled options (like ngOptions),
+ // we only have to handle options becoming disabled, not enabled
+
+ if (newVal === 'true' || newVal && optionElement.prop('selected')) {
+ if (self.multiple) {
+ scheduleViewValueUpdate(true);
+ } else {
+ self.ngModelCtrl.$setViewValue(null);
+ self.ngModelCtrl.$render();
+ }
+ }
+ });
+
optionElement.on('$destroy', function() {
- self.removeOption(optionAttrs.value);
- self.ngModelCtrl.$render();
+ var currentValue = self.readValue();
+ var removeValue = optionAttrs.value;
+
+ self.removeOption(removeValue);
+ scheduleRender();
+
+ if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 ||
+ currentValue === removeValue
+ ) {
+ // When multiple (selected) options are destroyed at the same time, we don't want
+ // to run a model update for each of them. Instead, run a single update in the $$postDigest
+ scheduleViewValueUpdate(true);
+ }
});
};
}];
@@ -30914,7 +22788,7 @@ var SelectController =
* @restrict E
*
* @description
- * HTML `SELECT` element with angular data-binding.
+ * HTML `select` element with AngularJS data-binding.
*
* The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
* between the scope and the `<select>` control (including setting default values).
@@ -30924,14 +22798,27 @@ var SelectController =
* When an item in the `<select>` menu is selected, the value of the selected option will be bound
* to the model identified by the `ngModel` directive. With static or repeated options, this is
* the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing.
- * If you want dynamic value attributes, you can use interpolation inside the value attribute.
+ * Value and textContent can be interpolated.
*
- * <div class="alert alert-warning">
- * Note that the value of a `select` directive used without `ngOptions` is always a string.
- * When the model needs to be bound to a non-string value, you must either explicitly convert it
- * using a directive (see example below) or use `ngOptions` to specify the set of options.
- * This is because an option element can only be bound to string values at present.
- * </div>
+ * The {@link select.SelectController select controller} exposes utility functions that can be used
+ * to manipulate the select's behavior.
+ *
+ * ## Matching model and option values
+ *
+ * In general, the match between the model and an option is evaluated by strictly comparing the model
+ * value against the value of the available options.
+ *
+ * If you are setting the option value with the option's `value` attribute, or textContent, the
+ * value will always be a `string` which means that the model value must also be a string.
+ * Otherwise the `select` directive cannot match them correctly.
+ *
+ * To bind the model to a non-string value, you can use one of the following strategies:
+ * - the {@link ng.ngOptions `ngOptions`} directive
+ * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value})
+ * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be
+ * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example})
+ * - model $parsers / $formatters to convert the string value
+ * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example})
*
* If the viewValue of `ngModel` does not match any of the options, then the control
* will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
@@ -30940,16 +22827,20 @@ var SelectController =
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
* option. See example below for demonstration.
*
- * <div class="alert alert-info">
+ * ## Choosing between `ngRepeat` and `ngOptions`
+ *
* In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
- * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
- * comprehension expression, and additionally in reducing memory and increasing speed by not creating
- * a new scope for each repeated instance.
- * </div>
+ * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
*
+ * Specifically, select with repeated options slows down significantly starting at 2000 options in
+ * Chrome and Internet Explorer / Edge.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} multiple Allows multiple options to be selected. The selected values will be
* bound to the model as an array.
@@ -30957,10 +22848,13 @@ var SelectController =
* @param {string=} ngRequired Adds required attribute and required validation constraint to
* the element when the ngRequired expression evaluates to true. Use ngRequired instead of required
* when you want to data-bind to the required attribute.
- * @param {string=} ngChange Angular expression to be executed when selected option(s) changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when selected option(s) changes due to user
* interaction with the select element.
* @param {string=} ngOptions sets the options that the select is populated with and defines what is
* set on the model on selection. See {@link ngOptions `ngOptions`}.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
+ *
*
* @example
* ### Simple `select` elements with static options
@@ -31001,7 +22895,7 @@ var SelectController =
* $scope.data = {
* singleSelect: null,
* multipleSelect: [],
- * option1: 'option-1',
+ * option1: 'option-1'
* };
*
* $scope.forceUnknownOption = function() {
@@ -31011,36 +22905,70 @@ var SelectController =
* </file>
*</example>
*
+ * @example
* ### Using `ngRepeat` to generate `select` options
- * <example name="ngrepeat-select" module="ngrepeatSelect">
+ * <example name="select-ngrepeat" module="ngrepeatSelect">
* <file name="index.html">
* <div ng-controller="ExampleController">
* <form name="myForm">
* <label for="repeatSelect"> Repeat select: </label>
- * <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect">
+ * <select name="repeatSelect" id="repeatSelect" ng-model="data.model">
* <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option>
* </select>
* </form>
* <hr>
- * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/>
+ * <tt>model = {{data.model}}</tt><br/>
* </div>
* </file>
* <file name="app.js">
* angular.module('ngrepeatSelect', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.data = {
- * repeatSelect: null,
+ * model: null,
* availableOptions: [
* {id: '1', name: 'Option A'},
* {id: '2', name: 'Option B'},
* {id: '3', name: 'Option C'}
- * ],
+ * ]
* };
* }]);
* </file>
*</example>
*
+ * @example
+ * ### Using `ngValue` to bind the model to an array of objects
+ * <example name="select-ngvalue" module="ngvalueSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="ngvalueselect"> ngvalue select: </label>
+ * <select size="6" name="ngvalueselect" ng-model="data.model" multiple>
+ * <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option>
+ * </select>
+ * </form>
+ * <hr>
+ * <pre>model = {{data.model | json}}</pre><br/>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('ngvalueSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * model: null,
+ * availableOptions: [
+ {value: 'myString', name: 'string'},
+ {value: 1, name: 'integer'},
+ {value: true, name: 'boolean'},
+ {value: null, name: 'null'},
+ {value: {prop: 'value'}, name: 'object'},
+ {value: ['a'], name: 'array'}
+ * ]
+ * };
+ * }]);
+ * </file>
+ *</example>
*
+ * @example
* ### Using `select` with `ngOptions` and setting a default value
* See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
*
@@ -31072,7 +23000,7 @@ var SelectController =
* </file>
*</example>
*
- *
+ * @example
* ### Binding `select` to a non-string value via `ngModel` parsing / formatting
*
* <example name="select-with-non-string-options" module="nonStringSelect">
@@ -31105,7 +23033,6 @@ var SelectController =
* </file>
* <file name="protractor.js" type="protractor">
* it('should initialize to model', function() {
- * var select = element(by.css('select'));
* expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two');
* });
* </file>
@@ -31127,11 +23054,16 @@ var selectDirective = function() {
function selectPreLink(scope, element, attr, ctrls) {
- // if ngModel is not defined, we don't need to do anything
+ var selectCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
- if (!ngModelCtrl) return;
- var selectCtrl = ctrls[0];
+ // if ngModel is not defined, we don't need to do anything but set the registerOption
+ // function to noop, so options don't get added internally
+ if (!ngModelCtrl) {
+ selectCtrl.registerOption = noop;
+ return;
+ }
+
selectCtrl.ngModelCtrl = ngModelCtrl;
@@ -31139,6 +23071,7 @@ var selectDirective = function() {
// to the `readValue` method, which can be changed if the select can have multiple
// selected values or if the options are being generated by `ngOptions`
element.on('change', function() {
+ selectCtrl.removeUnknownOption();
scope.$apply(function() {
ngModelCtrl.$setViewValue(selectCtrl.readValue());
});
@@ -31149,13 +23082,15 @@ var selectDirective = function() {
// we have to add an extra watch since ngModel doesn't work well with arrays - it
// doesn't trigger rendering if only an item in the array changes.
if (attr.multiple) {
+ selectCtrl.multiple = true;
// Read value now needs to check each option to see if it is selected
selectCtrl.readValue = function readMultipleValue() {
var array = [];
forEach(element.find('option'), function(option) {
- if (option.selected) {
- array.push(option.value);
+ if (option.selected && !option.disabled) {
+ var val = option.value;
+ array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val);
}
});
return array;
@@ -31163,9 +23098,22 @@ var selectDirective = function() {
// Write value now needs to set the selected property of each matching option
selectCtrl.writeValue = function writeMultipleValue(value) {
- var items = new HashMap(value);
forEach(element.find('option'), function(option) {
- option.selected = isDefined(items.get(option.value));
+ var shouldBeSelected = !!value && (includes(value, option.value) ||
+ includes(value, selectCtrl.selectValueMap[option.value]));
+ var currentlySelected = option.selected;
+
+ // Support: IE 9-11 only, Edge 12-15+
+ // In IE and Edge adding options to the selection via shift+click/UP/DOWN
+ // will de-select already selected options if "selected" on those options was set
+ // more than once (i.e. when the options were already selected)
+ // So we only modify the selected property if necessary.
+ // Note: this behavior cannot be replicated via unit tests because it only shows in the
+ // actual user interface.
+ if (shouldBeSelected !== currentlySelected) {
+ setOptionSelectedStatus(jqLite(option), shouldBeSelected);
+ }
+
});
};
@@ -31216,13 +23164,17 @@ var optionDirective = ['$interpolate', function($interpolate) {
restrict: 'E',
priority: 100,
compile: function(element, attr) {
- if (isDefined(attr.value)) {
+ var interpolateValueFn, interpolateTextFn;
+
+ if (isDefined(attr.ngValue)) {
+ // Will be handled by registerOption
+ } else if (isDefined(attr.value)) {
// If the value attribute is defined, check if it contains an interpolation
- var interpolateValueFn = $interpolate(attr.value, true);
+ interpolateValueFn = $interpolate(attr.value, true);
} else {
// If the value attribute is not defined then we fall back to the
// text content of the option element, which may be interpolated
- var interpolateTextFn = $interpolate(element.text(), true);
+ interpolateTextFn = $interpolate(element.text(), true);
if (!interpolateTextFn) {
attr.$set('value', element.text());
}
@@ -31244,23 +23196,22 @@ var optionDirective = ['$interpolate', function($interpolate) {
};
}];
-var styleDirective = valueFn({
- restrict: 'E',
- terminal: false
-});
-
/**
* @ngdoc directive
* @name ngRequired
* @restrict A
*
+ * @param {expression} ngRequired AngularJS expression. If it evaluates to `true`, it sets the
+ * `required` attribute to the element and adds the `required`
+ * {@link ngModel.NgModelController#$validators `validator`}.
+ *
* @description
*
* ngRequired adds the required {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
* It is most often used for {@link input `input`} and {@link select `select`} controls, but can also be
* applied to custom controls.
*
- * The directive sets the `required` attribute on the element if the Angular expression inside
+ * The directive sets the `required` attribute on the element if the AngularJS expression inside
* `ngRequired` evaluates to true. A special directive for setting `required` is necessary because we
* cannot use interpolation inside `required`. See the {@link guide/interpolation interpolation guide}
* for more info.
@@ -31308,28 +23259,44 @@ var styleDirective = valueFn({
* </file>
* </example>
*/
-var requiredDirective = function() {
+var requiredDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
- attr.required = true; // force truthy in case we are on non input element
+ // For boolean attributes like required, presence means true
+ var value = attr.hasOwnProperty('required') || $parse(attr.ngRequired)(scope);
+
+ if (!attr.ngRequired) {
+ // force truthy in case we are on non input element
+ // (input elements do this automatically for boolean attributes like required)
+ attr.required = true;
+ }
ctrl.$validators.required = function(modelValue, viewValue) {
- return !attr.required || !ctrl.$isEmpty(viewValue);
+ return !value || !ctrl.$isEmpty(viewValue);
};
- attr.$observe('required', function() {
- ctrl.$validate();
+ attr.$observe('required', function(newVal) {
+
+ if (value !== newVal) {
+ value = newVal;
+ ctrl.$validate();
+ }
});
}
};
-};
+}];
/**
* @ngdoc directive
* @name ngPattern
+ * @restrict A
+ *
+ * @param {expression|RegExp} ngPattern AngularJS expression that must evaluate to a `RegExp` or a `String`
+ * parsable into a `RegExp`, or a `RegExp` literal. See above for
+ * more details.
*
* @description
*
@@ -31337,11 +23304,12 @@ var requiredDirective = function() {
* It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
*
* The validator sets the `pattern` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
- * does not match a RegExp which is obtained by evaluating the Angular expression given in the
- * `ngPattern` attribute value:
- * * If the expression evaluates to a RegExp object, then this is used directly.
- * * If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it
- * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * does not match a RegExp which is obtained from the `ngPattern` attribute value:
+ * - the value is an AngularJS expression:
+ * - If the expression evaluates to a RegExp object, then this is used directly.
+ * - If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it
+ * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * - If the value is a RegExp literal, e.g. `ngPattern="/^\d+$/"`, it is used directly.
*
* <div class="alert alert-info">
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
@@ -31402,40 +23370,68 @@ var requiredDirective = function() {
* </file>
* </example>
*/
-var patternDirective = function() {
+var patternDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
- link: function(scope, elm, attr, ctrl) {
- if (!ctrl) return;
-
- var regexp, patternExp = attr.ngPattern || attr.pattern;
- attr.$observe('pattern', function(regex) {
- if (isString(regex) && regex.length > 0) {
- regex = new RegExp('^' + regex + '$');
+ compile: function(tElm, tAttr) {
+ var patternExp;
+ var parseFn;
+
+ if (tAttr.ngPattern) {
+ patternExp = tAttr.ngPattern;
+
+ // ngPattern might be a scope expression, or an inlined regex, which is not parsable.
+ // We get value of the attribute here, so we can compare the old and the new value
+ // in the observer to avoid unnecessary validations
+ if (tAttr.ngPattern.charAt(0) === '/' && REGEX_STRING_REGEXP.test(tAttr.ngPattern)) {
+ parseFn = function() { return tAttr.ngPattern; };
+ } else {
+ parseFn = $parse(tAttr.ngPattern);
}
+ }
- if (regex && !regex.test) {
- throw minErr('ngPattern')('noregexp',
- 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
- regex, startingTag(elm));
+ return function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
+
+ var attrVal = attr.pattern;
+
+ if (attr.ngPattern) {
+ attrVal = parseFn(scope);
+ } else {
+ patternExp = attr.pattern;
}
- regexp = regex || undefined;
- ctrl.$validate();
- });
+ var regexp = parsePatternAttr(attrVal, patternExp, elm);
- ctrl.$validators.pattern = function(modelValue, viewValue) {
- // HTML5 pattern constraint validates the input value, so we validate the viewValue
- return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
+ attr.$observe('pattern', function(newVal) {
+ var oldRegexp = regexp;
+
+ regexp = parsePatternAttr(newVal, patternExp, elm);
+
+ if ((oldRegexp && oldRegexp.toString()) !== (regexp && regexp.toString())) {
+ ctrl.$validate();
+ }
+ });
+
+ ctrl.$validators.pattern = function(modelValue, viewValue) {
+ // HTML5 pattern constraint validates the input value, so we validate the viewValue
+ return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
+ };
};
}
+
};
-};
+}];
/**
* @ngdoc directive
* @name ngMaxlength
+ * @restrict A
+ *
+ * @param {expression} ngMaxlength AngularJS expression that must evaluate to a `Number` or `String`
+ * parsable into a `Number`. Used as value for the `maxlength`
+ * {@link ngModel.NgModelController#$validators validator}.
*
* @description
*
@@ -31443,7 +23439,7 @@ var patternDirective = function() {
* It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
*
* The validator sets the `maxlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
- * is longer than the integer obtained by evaluating the Angular expression given in the
+ * is longer than the integer obtained by evaluating the AngularJS expression given in the
* `ngMaxlength` attribute value.
*
* <div class="alert alert-info">
@@ -31499,29 +23495,38 @@ var patternDirective = function() {
* </file>
* </example>
*/
-var maxlengthDirective = function() {
+var maxlengthDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
- var maxlength = -1;
+ var maxlength = attr.maxlength || $parse(attr.ngMaxlength)(scope);
+ var maxlengthParsed = parseLength(maxlength);
+
attr.$observe('maxlength', function(value) {
- var intVal = toInt(value);
- maxlength = isNaN(intVal) ? -1 : intVal;
- ctrl.$validate();
+ if (maxlength !== value) {
+ maxlengthParsed = parseLength(value);
+ maxlength = value;
+ ctrl.$validate();
+ }
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
- return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
+ return (maxlengthParsed < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlengthParsed);
};
}
};
-};
+}];
/**
* @ngdoc directive
* @name ngMinlength
+ * @restrict A
+ *
+ * @param {expression} ngMinlength AngularJS expression that must evaluate to a `Number` or `String`
+ * parsable into a `Number`. Used as value for the `minlength`
+ * {@link ngModel.NgModelController#$validators validator}.
*
* @description
*
@@ -31529,7 +23534,7 @@ var maxlengthDirective = function() {
* It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
*
* The validator sets the `minlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
- * is shorter than the integer obtained by evaluating the Angular expression given in the
+ * is shorter than the integer obtained by evaluating the AngularJS expression given in the
* `ngMinlength` attribute value.
*
* <div class="alert alert-info">
@@ -31583,35 +23588,62 @@ var maxlengthDirective = function() {
* </file>
* </example>
*/
-var minlengthDirective = function() {
+var minlengthDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
- var minlength = 0;
+ var minlength = attr.minlength || $parse(attr.ngMinlength)(scope);
+ var minlengthParsed = parseLength(minlength) || -1;
+
attr.$observe('minlength', function(value) {
- minlength = toInt(value) || 0;
- ctrl.$validate();
+ if (minlength !== value) {
+ minlengthParsed = parseLength(value) || -1;
+ minlength = value;
+ ctrl.$validate();
+ }
+
});
ctrl.$validators.minlength = function(modelValue, viewValue) {
- return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
+ return ctrl.$isEmpty(viewValue) || viewValue.length >= minlengthParsed;
};
}
};
-};
+}];
+
+function parsePatternAttr(regex, patternExp, elm) {
+ if (!regex) return undefined;
+
+ if (isString(regex)) {
+ regex = new RegExp('^' + regex + '$');
+ }
+
+ if (!regex.test) {
+ throw minErr('ngPattern')('noregexp',
+ 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
+ regex, startingTag(elm));
+ }
+
+ return regex;
+}
+
+function parseLength(val) {
+ var intVal = toInt(val);
+ return isNumberNaN(intVal) ? -1 : intVal;
+}
if (window.angular.bootstrap) {
- //AngularJS is already loaded, so we can return here...
+ // AngularJS is already loaded, so we can return here...
if (window.console) {
- console.log('WARNING: Tried to load angular more than once.');
+ console.log('WARNING: Tried to load AngularJS more than once.');
}
return;
}
-//try to bind to jquery now so that one can write jqLite(document).ready()
-//but we will rebind on bootstrap again.
+// try to bind to jquery now so that one can write jqLite(fn)
+// but we will rebind on bootstrap again.
bindJQuery();
publishExternalAPI(angular);
@@ -31659,7 +23691,7 @@ $provide.value("$locale", {
"BC",
"AD"
],
- "FIRSTDAYOFWEEK": 6,
+ "FIRSTDAYOFWEEK": 0,
"MONTH": [
"January",
"February",
@@ -31715,13 +23747,13 @@ $provide.value("$locale", {
5,
6
],
- "fullDate": "EEEE, MMMM d, y",
- "longDate": "MMMM d, y",
- "medium": "MMM d, y h:mm:ss a",
- "mediumDate": "MMM d, y",
+ "fullDate": "EEEE, d MMMM y",
+ "longDate": "d MMMM y",
+ "medium": "d MMM y h:mm:ss a",
+ "mediumDate": "d MMM y",
"mediumTime": "h:mm:ss a",
- "short": "M/d/yy h:mm a",
- "shortDate": "M/d/yy",
+ "short": "dd/MM/y h:mm a",
+ "shortDate": "dd/MM/y",
"shortTime": "h:mm a"
},
"NUMBER_FORMATS": {
@@ -31753,16 +23785,16 @@ $provide.value("$locale", {
}
]
},
- "id": "en-us",
- "localeID": "en_US",
+ "id": "en-na",
+ "localeID": "en_NA",
"pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
- jqLite(window.document).ready(function() {
+ jqLite(function() {
angularInit(window.document, bootstrap);
});
})(window);
-!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>'); \ No newline at end of file
+!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(window.angular.element('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>'));
diff --git a/xstatic/pkg/angular/data/errors.json b/xstatic/pkg/angular/data/errors.json
index 51609a3..b12733f 100644
--- a/xstatic/pkg/angular/data/errors.json
+++ b/xstatic/pkg/angular/data/errors.json
@@ -1 +1 @@
-{"id":"ng","generated":"Fri Jul 22 2016 08:20:15 GMT-0700 (PDT)","errors":{"ng":{"areq":"Argument '{0}' is {1}","cpta":"Can't copy! TypedArray destination cannot be mutated.","test":"no injector found for element argument to getTestability","cpws":"Can't copy! Making copies of Window or Scope instances is not supported.","btstrpd":"App already bootstrapped with this element '{0}'","cpi":"Can't copy! Source and destination are identical.","badname":"hasOwnProperty is not a valid {0} name"},"$http":{"legacy":"The method `{0}` on the promise returned from `$http` has been disabled.","badreq":"Http request configuration url must be a string. Received: {0}"},"ngRepeat":{"badident":"alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.","iexp":"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.","dupes":"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}","iidexp":"'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'."},"$sce":{"imatcher":"Matchers may only be \"self\", string patterns or RegExp objects","icontext":"Attempted to trust a value in invalid context. Context: {0}; Value: {1}","iwcard":"Illegal sequence *** in string matcher. String: {0}","insecurl":"Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}","iequirks":"Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks mode. You can fix this by adding the text <!doctype html> to the top of your HTML document. See http://docs.angularjs.org/api/ng.$sce for more information.","unsafe":"Attempting to use an unsafe value in a safe context.","itype":"Attempted to trust a non-string value in a content requiring a string: Context: {0}"},"ngPattern":{"noregexp":"Expected {0} to be a RegExp but was {1}. Element: {2}"},"$controller":{"ctrlfmt":"Badly formed controller string '{0}'. Must match `__name__ as __id__` or `__name__`.","noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$parse":{"isecfn":"Referencing Function in Angular expressions is disallowed! Expression: {0}","isecwindow":"Referencing the Window in Angular expressions is disallowed! Expression: {0}","ueoe":"Unexpected end of expression: {0}","isecdom":"Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}","lexerr":"Lexer Error: {0} at column{1} in expression [{2}].","esc":"IMPOSSIBLE","isecobj":"Referencing Object in Angular expressions is disallowed! Expression: {0}","lval":"Trying to assign a value to a non l-value","isecff":"Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}","syntax":"Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].","isecaf":"Assigning to a constructor is disallowed! Expression: {0}","isecfld":"Attempting to access a disallowed field in Angular expressions! Expression: {0}"},"orderBy":{"notarray":"Expected array but received: {0}"},"jqLite":{"offargs":"jqLite#off() does not support the `selector` argument","onargs":"jqLite#on() does not support the `selector` or `eventData` parameters","nosel":"Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element"},"$animate":{"notcsel":"Expecting class selector starting with '.' got '{0}'.","nongcls":"$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the \"{0}\" CSS class."},"$q":{"norslvr":"Expected resolverFn, got '{0}'","qcycle":"Expected promise to be resolved with value other than itself '{0}'"},"$injector":{"pget":"Provider '{0}' must define $get factory method.","cdep":"Circular dependency found: {0}","nomod":"Module '{0}' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.","strictdi":"{0} is not using explicit annotation and cannot be invoked in strict mode","modulerr":"Failed to instantiate module {0} due to:\n{1}","undef":"Provider '{0}' must return a value from $get factory method.","unpr":"Unknown provider: {0}","itkn":"Incorrect injection token! Expected service name as string, got {0}"},"$templateRequest":{"tpload":"Failed to load template: {0} (HTTP status: {1} {2})"},"filter":{"notarray":"Expected array but received: {0}"},"ngTransclude":{"orphan":"Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: {0}"},"ngModel":{"nopromise":"Expected asynchronous validator to return a promise but got '{0}' instead.","nonassign":"Expression '{0}' is non-assignable. Element: {1}","datefmt":"Expected `{0}` to be a date","constexpr":"Expected constant expression for `{0}`, but saw `{1}`.","numfmt":"Expected `{0}` to be a number"},"$location":{"nostate":"History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API","ipthprfx":"Invalid url \"{0}\", missing path prefix \"{1}\".","isrcharg":"The first argument of the `$location#search()` call must be a string or an object.","nobase":"$location in HTML5 mode requires a <base> tag to be present!"},"$cacheFactory":{"iid":"CacheId '{0}' is already taken!"},"$interpolate":{"noconcat":"Error while interpolating: {0}\nStrict Contextual Escaping disallows interpolations that concatenate multiple expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce","interr":"Can't interpolate: {0}\n{1}","nochgmustache":"angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.","reqcomma":"Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”","untermstr":"The string beginning at line {0}, column {1} is unterminated in text “{2}”","badexpr":"Unexpected operator “{0}” at line {1}, column {2} in text. Was expecting “{3}”. Text: “{4}”","dupvalue":"The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”","unsafe":"Use of select/plural MessageFormat syntax is currently disallowed in a secure context ({0}). At line {1}, column {2} of text “{3}”","reqother":"“other” is a required option.","reqendinterp":"Expecting end of interpolation symbol, “{0}”, at line {1}, column {2} in text “{3}”","reqarg":"Expected one of “plural” or “select” at line {0}, column {1} of text “{2}”","wantstring":"Expected the beginning of a string at line {0}, column {1} in text “{2}”","logicbug":"The messageformat parser has encountered an internal error. Please file a github issue against the AngularJS project and provide this message text that triggers the bug. Text: “{0}”","reqopenbrace":"The plural choice “{0}” must be followed by a message in braces at line {1}, column {2} in text “{3}”","unknarg":"Unsupported keyword “{0}” at line {0}, column {1}. Only “plural” and “select” are currently supported. Text: “{3}”","reqendbrace":"The plural/select choice “{0}” message starting at line {1}, column {2} does not have an ending closing brace. Text “{3}”"},"ngOptions":{"iexp":"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}"},"$rootScope":{"inprog":"{0} already in progress","infdig":"{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}"},"$compile":{"noident":"Cannot bind to controller without identifier for directive '{0}'.","selmulti":"Binding to the 'multiple' attribute is not supported. Element: {0}","ctreq":"Controller '{0}', required by directive '{1}', can't be found!","tplrt":"Template for directive '{0}' must have exactly one root element. {1}","iscp":"Invalid {3} for directive '{0}'. Definition: {... {1}: '{2}' ...}","baddir":"Directive/Component name '{0}' is invalid. The name should not contain leading or trailing whitespaces","noctrl":"Cannot bind to controller without directive '{0}'s controller.","multidir":"Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}","uterdir":"Unterminated attribute, found '{0}' but no matching '{1}' found.","infchng":"{0} $onChanges() iterations reached. Aborting!\n","reqslot":"Required transclusion slot `{0}` was not filled.","nodomevents":"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- versions (such as ng-click instead of onclick) instead.","nonassign":"Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!","noslot":"No parent directive that requires a transclusion with slot name \"{0}\". Element: {1}"},"$resource":{"badargs":"Expected up to 4 arguments [params, data, success, error], got {0} arguments","badmember":"Dotted member path \"@{0}\" is invalid.","badname":"hasOwnProperty is not a valid parameter name.","badcfg":"Error in resource configuration for action `{0}`. Expected response to contain an {1} but got an {2} (Request: {3} {4})"},"$route":{"norout":"Tried updating route when with no current route"},"linky":{"notstring":"Expected string but received: {0}"},"$sanitize":{"noinert":"Can't create an inert html document","uinput":"Failed to sanitize html because the input is unstable"}}} \ No newline at end of file
+{"id":"ng","generated":"Wed Oct 21 2020 04:29:16 GMT-0700 (PDT)","errors":{"ng":{"areq":"Argument '{0}' is {1}","cpta":"Can't copy! TypedArray destination cannot be mutated.","test":"no injector found for element argument to getTestability","cpws":"Can't copy! Making copies of Window or Scope instances is not supported.","btstrpd":"App already bootstrapped with this element '{0}'","cpi":"Can't copy! Source and destination are identical.","badname":"hasOwnProperty is not a valid {0} name"},"$http":{"legacy":"The method `{0}` on the promise returned from `$http` has been disabled.","badreq":"Http request configuration url must be a string. Received: {0}"},"ngRepeat":{"badident":"alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.","iexp":"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.","dupes":"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}","iidexp":"'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'."},"$sce":{"imatcher":"Matchers may only be \"self\", string patterns or RegExp objects","icontext":"Attempted to trust a value in invalid context. Context: {0}; Value: {1}","iwcard":"Illegal sequence *** in string matcher. String: {0}","insecurl":"Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}","iequirks":"Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks mode. You can fix this by adding the text <!doctype html> to the top of your HTML document. See http://docs.angularjs.org/api/ng.$sce for more information.","unsafe":"Attempting to use an unsafe value in a safe context.","itype":"Attempted to trust a non-string value in a content requiring a string: Context: {0}"},"ngPattern":{"noregexp":"Expected {0} to be a RegExp but was {1}. Element: {2}"},"$controller":{"ctrlfmt":"Badly formed controller string '{0}'. Must match `__name__ as __id__` or `__name__`.","noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$parse":{"isecfn":"Referencing Function in Angular expressions is disallowed! Expression: {0}","isecwindow":"Referencing the Window in Angular expressions is disallowed! Expression: {0}","ueoe":"Unexpected end of expression: {0}","isecdom":"Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}","lexerr":"Lexer Error: {0} at column{1} in expression [{2}].","esc":"IMPOSSIBLE","isecobj":"Referencing Object in Angular expressions is disallowed! Expression: {0}","lval":"Trying to assign a value to a non l-value","isecff":"Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}","syntax":"Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].","isecaf":"Assigning to a constructor is disallowed! Expression: {0}","isecfld":"Attempting to access a disallowed field in Angular expressions! Expression: {0}"},"orderBy":{"notarray":"Expected array but received: {0}"},"jqLite":{"offargs":"jqLite#off() does not support the `selector` argument","onargs":"jqLite#on() does not support the `selector` or `eventData` parameters","nosel":"Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element"},"$animate":{"notcsel":"Expecting class selector starting with '.' got '{0}'.","nongcls":"$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the \"{0}\" CSS class."},"$q":{"norslvr":"Expected resolverFn, got '{0}'","qcycle":"Expected promise to be resolved with value other than itself '{0}'"},"$injector":{"pget":"Provider '{0}' must define $get factory method.","cdep":"Circular dependency found: {0}","nomod":"Module '{0}' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.","strictdi":"{0} is not using explicit annotation and cannot be invoked in strict mode","modulerr":"Failed to instantiate module {0} due to:\n{1}","undef":"Provider '{0}' must return a value from $get factory method.","unpr":"Unknown provider: {0}","itkn":"Incorrect injection token! Expected service name as string, got {0}"},"$templateRequest":{"tpload":"Failed to load template: {0} (HTTP status: {1} {2})"},"filter":{"notarray":"Expected array but received: {0}"},"ngTransclude":{"orphan":"Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: {0}"},"ngModel":{"nopromise":"Expected asynchronous validator to return a promise but got '{0}' instead.","nonassign":"Expression '{0}' is non-assignable. Element: {1}","datefmt":"Expected `{0}` to be a date","constexpr":"Expected constant expression for `{0}`, but saw `{1}`.","numfmt":"Expected `{0}` to be a number"},"$location":{"nostate":"History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API","ipthprfx":"Invalid url \"{0}\", missing path prefix \"{1}\".","isrcharg":"The first argument of the `$location#search()` call must be a string or an object.","nobase":"$location in HTML5 mode requires a <base> tag to be present!"},"$cacheFactory":{"iid":"CacheId '{0}' is already taken!"},"$interpolate":{"noconcat":"Error while interpolating: {0}\nStrict Contextual Escaping disallows interpolations that concatenate multiple expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce","interr":"Can't interpolate: {0}\n{1}","nochgmustache":"angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.","reqcomma":"Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”","untermstr":"The string beginning at line {0}, column {1} is unterminated in text “{2}”","badexpr":"Unexpected operator “{0}” at line {1}, column {2} in text. Was expecting “{3}”. Text: “{4}”","dupvalue":"The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”","unsafe":"Use of select/plural MessageFormat syntax is currently disallowed in a secure context ({0}). At line {1}, column {2} of text “{3}”","reqother":"“other” is a required option.","reqendinterp":"Expecting end of interpolation symbol, “{0}”, at line {1}, column {2} in text “{3}”","reqarg":"Expected one of “plural” or “select” at line {0}, column {1} of text “{2}”","wantstring":"Expected the beginning of a string at line {0}, column {1} in text “{2}”","logicbug":"The messageformat parser has encountered an internal error. Please file a github issue against the AngularJS project and provide this message text that triggers the bug. Text: “{0}”","reqopenbrace":"The plural choice “{0}” must be followed by a message in braces at line {1}, column {2} in text “{3}”","unknarg":"Unsupported keyword “{0}” at line {0}, column {1}. Only “plural” and “select” are currently supported. Text: “{3}”","reqendbrace":"The plural/select choice “{0}” message starting at line {1}, column {2} does not have an ending closing brace. Text “{3}”"},"ngOptions":{"iexp":"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}"},"$rootScope":{"inprog":"{0} already in progress","infdig":"{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}"},"$compile":{"noident":"Cannot bind to controller without identifier for directive '{0}'.","selmulti":"Binding to the 'multiple' attribute is not supported. Element: {0}","ctreq":"Controller '{0}', required by directive '{1}', can't be found!","tplrt":"Template for directive '{0}' must have exactly one root element. {1}","iscp":"Invalid {3} for directive '{0}'. Definition: {... {1}: '{2}' ...}","baddir":"Directive/Component name '{0}' is invalid. The name should not contain leading or trailing whitespaces","noctrl":"Cannot bind to controller without directive '{0}'s controller.","multidir":"Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}","uterdir":"Unterminated attribute, found '{0}' but no matching '{1}' found.","infchng":"{0} $onChanges() iterations reached. Aborting!\n","reqslot":"Required transclusion slot `{0}` was not filled.","nodomevents":"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- versions (such as ng-click instead of onclick) instead.","nonassign":"Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!","noslot":"No parent directive that requires a transclusion with slot name \"{0}\". Element: {1}"},"$resource":{"badargs":"Expected up to 4 arguments [params, data, success, error], got {0} arguments","badmember":"Dotted member path \"@{0}\" is invalid.","badname":"hasOwnProperty is not a valid parameter name.","badcfg":"Error in resource configuration for action `{0}`. Expected response to contain an {1} but got an {2} (Request: {3} {4})"},"$route":{"norout":"Tried updating route when with no current route"},"linky":{"notstring":"Expected string but received: {0}"},"$sanitize":{"noinert":"Can't create an inert html document","uinput":"Failed to sanitize html because the input is unstable"}}}
diff --git a/xstatic/pkg/angular/data/version.json b/xstatic/pkg/angular/data/version.json
index edd6186..693d417 100644
--- a/xstatic/pkg/angular/data/version.json
+++ b/xstatic/pkg/angular/data/version.json
@@ -1 +1 @@
-{"raw":"v1.5.8","major":1,"minor":5,"patch":8,"prerelease":[],"build":[],"version":"1.5.8","codeName":"arbitrary-fallbacks","full":"1.5.8","branch":"v1.5.x","cdn":{"raw":"v1.5.7","major":1,"minor":5,"patch":7,"prerelease":[],"build":[],"version":"1.5.7","docsUrl":"http://code.angularjs.org/1.5.7/docs"}} \ No newline at end of file
+{"raw":"v1.8.2","major":1,"minor":8,"patch":2,"prerelease":[],"build":[],"version":"1.8.2","codeName":"meteoric-mining","full":"1.8.2","branch":"v1.8.x","cdn":{"raw":"v1.8.2","major":1,"minor":8,"patch":2,"prerelease":[],"build":[],"version":"1.8.2","docsUrl":"http://code.angularjs.org/1.8.2/docs"}}
diff --git a/xstatic/pkg/angular/data/version.txt b/xstatic/pkg/angular/data/version.txt
index fa5512a..53adb84 100644
--- a/xstatic/pkg/angular/data/version.txt
+++ b/xstatic/pkg/angular/data/version.txt
@@ -1 +1 @@
-1.5.8 \ No newline at end of file
+1.8.2